client.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package client // import "miniflux.app/http/client"
  4. import (
  5. "bytes"
  6. "crypto/tls"
  7. "crypto/x509"
  8. "encoding/json"
  9. "fmt"
  10. "io"
  11. "net"
  12. "net/http"
  13. "net/url"
  14. "strings"
  15. "time"
  16. "miniflux.app/config"
  17. "miniflux.app/errors"
  18. "miniflux.app/logger"
  19. "miniflux.app/timer"
  20. )
  21. const (
  22. defaultHTTPClientTimeout = 20
  23. defaultHTTPClientMaxBodySize = 15 * 1024 * 1024
  24. )
  25. var (
  26. errInvalidCertificate = "Invalid SSL certificate (original error: %q)"
  27. errNetworkOperation = "This website is unreachable (original error: %q)"
  28. errRequestTimeout = "Website unreachable, the request timed out after %d seconds"
  29. )
  30. // Client builds and executes HTTP requests.
  31. type Client struct {
  32. inputURL string
  33. requestEtagHeader string
  34. requestLastModifiedHeader string
  35. requestAuthorizationHeader string
  36. requestUsername string
  37. requestPassword string
  38. requestUserAgent string
  39. requestCookie string
  40. useProxy bool
  41. doNotFollowRedirects bool
  42. ClientTimeout int
  43. ClientMaxBodySize int64
  44. ClientProxyURL string
  45. AllowSelfSignedCertificates bool
  46. }
  47. // New initializes a new HTTP client.
  48. func New(url string) *Client {
  49. return &Client{
  50. inputURL: url,
  51. ClientTimeout: defaultHTTPClientTimeout,
  52. ClientMaxBodySize: defaultHTTPClientMaxBodySize,
  53. }
  54. }
  55. // NewClientWithConfig initializes a new HTTP client with application config options.
  56. func NewClientWithConfig(url string, opts *config.Options) *Client {
  57. return &Client{
  58. inputURL: url,
  59. requestUserAgent: opts.HTTPClientUserAgent(),
  60. ClientTimeout: opts.HTTPClientTimeout(),
  61. ClientMaxBodySize: opts.HTTPClientMaxBodySize(),
  62. ClientProxyURL: opts.HTTPClientProxy(),
  63. }
  64. }
  65. func (c *Client) String() string {
  66. etagHeader := c.requestEtagHeader
  67. if c.requestEtagHeader == "" {
  68. etagHeader = "None"
  69. }
  70. lastModifiedHeader := c.requestLastModifiedHeader
  71. if c.requestLastModifiedHeader == "" {
  72. lastModifiedHeader = "None"
  73. }
  74. return fmt.Sprintf(
  75. `InputURL=%q ETag=%s LastMod=%s Auth=%v UserAgent=%q Verify=%v`,
  76. c.inputURL,
  77. etagHeader,
  78. lastModifiedHeader,
  79. c.requestAuthorizationHeader != "" || (c.requestUsername != "" && c.requestPassword != ""),
  80. c.requestUserAgent,
  81. !c.AllowSelfSignedCertificates,
  82. )
  83. }
  84. // WithCredentials defines the username/password for HTTP Basic authentication.
  85. func (c *Client) WithCredentials(username, password string) *Client {
  86. if username != "" && password != "" {
  87. c.requestUsername = username
  88. c.requestPassword = password
  89. }
  90. return c
  91. }
  92. // WithAuthorization defines the authorization HTTP header value.
  93. func (c *Client) WithAuthorization(authorization string) *Client {
  94. c.requestAuthorizationHeader = authorization
  95. return c
  96. }
  97. // WithCacheHeaders defines caching headers.
  98. func (c *Client) WithCacheHeaders(etagHeader, lastModifiedHeader string) *Client {
  99. c.requestEtagHeader = etagHeader
  100. c.requestLastModifiedHeader = lastModifiedHeader
  101. return c
  102. }
  103. // WithProxy enables proxy for the current HTTP request.
  104. func (c *Client) WithProxy() *Client {
  105. c.useProxy = true
  106. return c
  107. }
  108. // WithoutRedirects disables HTTP redirects.
  109. func (c *Client) WithoutRedirects() *Client {
  110. c.doNotFollowRedirects = true
  111. return c
  112. }
  113. // WithUserAgent defines the User-Agent header to use for HTTP requests.
  114. func (c *Client) WithUserAgent(userAgent string) *Client {
  115. if userAgent != "" {
  116. c.requestUserAgent = userAgent
  117. }
  118. return c
  119. }
  120. // WithCookie defines the Cookies to use for HTTP requests.
  121. func (c *Client) WithCookie(cookie string) *Client {
  122. if cookie != "" {
  123. c.requestCookie = cookie
  124. }
  125. return c
  126. }
  127. // Get performs a GET HTTP request.
  128. func (c *Client) Get() (*Response, error) {
  129. request, err := c.buildRequest(http.MethodGet, nil)
  130. if err != nil {
  131. return nil, err
  132. }
  133. return c.executeRequest(request)
  134. }
  135. // PostForm performs a POST HTTP request with form encoded values.
  136. func (c *Client) PostForm(values url.Values) (*Response, error) {
  137. request, err := c.buildRequest(http.MethodPost, strings.NewReader(values.Encode()))
  138. if err != nil {
  139. return nil, err
  140. }
  141. request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
  142. return c.executeRequest(request)
  143. }
  144. // PostJSON performs a POST HTTP request with a JSON payload.
  145. func (c *Client) PostJSON(data interface{}) (*Response, error) {
  146. b, err := json.Marshal(data)
  147. if err != nil {
  148. return nil, err
  149. }
  150. request, err := c.buildRequest(http.MethodPost, bytes.NewReader(b))
  151. if err != nil {
  152. return nil, err
  153. }
  154. request.Header.Add("Content-Type", "application/json")
  155. return c.executeRequest(request)
  156. }
  157. func (c *Client) executeRequest(request *http.Request) (*Response, error) {
  158. defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient] inputURL=%s", c.inputURL))
  159. logger.Debug("[HttpClient:Before] Method=%s %s",
  160. request.Method,
  161. c.String(),
  162. )
  163. client := c.buildClient()
  164. resp, err := client.Do(request)
  165. if resp != nil {
  166. defer resp.Body.Close()
  167. }
  168. if err != nil {
  169. if uerr, ok := err.(*url.Error); ok {
  170. switch uerr.Err.(type) {
  171. case x509.CertificateInvalidError, x509.HostnameError:
  172. err = errors.NewLocalizedError(errInvalidCertificate, uerr.Err)
  173. case *net.OpError:
  174. err = errors.NewLocalizedError(errNetworkOperation, uerr.Err)
  175. case net.Error:
  176. nerr := uerr.Err.(net.Error)
  177. if nerr.Timeout() {
  178. err = errors.NewLocalizedError(errRequestTimeout, c.ClientTimeout)
  179. }
  180. }
  181. }
  182. return nil, err
  183. }
  184. if resp.ContentLength > c.ClientMaxBodySize {
  185. return nil, fmt.Errorf("client: response too large (%d bytes)", resp.ContentLength)
  186. }
  187. buf, err := io.ReadAll(resp.Body)
  188. if err != nil {
  189. return nil, fmt.Errorf("client: error while reading body %v", err)
  190. }
  191. response := &Response{
  192. Body: bytes.NewReader(buf),
  193. StatusCode: resp.StatusCode,
  194. EffectiveURL: resp.Request.URL.String(),
  195. LastModified: resp.Header.Get("Last-Modified"),
  196. ETag: resp.Header.Get("ETag"),
  197. Expires: resp.Header.Get("Expires"),
  198. ContentType: resp.Header.Get("Content-Type"),
  199. ContentLength: resp.ContentLength,
  200. }
  201. logger.Debug("[HttpClient:After] Method=%s %s; Response => %s",
  202. request.Method,
  203. c.String(),
  204. response,
  205. )
  206. // Ignore caching headers for feeds that do not want any cache.
  207. if resp.Header.Get("Expires") == "0" {
  208. logger.Debug("[HttpClient] Ignore caching headers for %q", response.EffectiveURL)
  209. response.ETag = ""
  210. response.LastModified = ""
  211. }
  212. return response, err
  213. }
  214. func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, error) {
  215. request, err := http.NewRequest(method, c.inputURL, body)
  216. if err != nil {
  217. return nil, err
  218. }
  219. request.Header = c.buildHeaders()
  220. if c.requestUsername != "" && c.requestPassword != "" {
  221. request.SetBasicAuth(c.requestUsername, c.requestPassword)
  222. }
  223. return request, nil
  224. }
  225. func (c *Client) buildClient() http.Client {
  226. client := http.Client{
  227. Timeout: time.Duration(c.ClientTimeout) * time.Second,
  228. }
  229. transport := &http.Transport{
  230. Proxy: http.ProxyFromEnvironment,
  231. DialContext: (&net.Dialer{
  232. // Default is 30s.
  233. Timeout: 10 * time.Second,
  234. // Default is 30s.
  235. KeepAlive: 15 * time.Second,
  236. }).DialContext,
  237. // Default is 100.
  238. MaxIdleConns: 50,
  239. // Default is 90s.
  240. IdleConnTimeout: 10 * time.Second,
  241. }
  242. if c.AllowSelfSignedCertificates {
  243. transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
  244. }
  245. if c.doNotFollowRedirects {
  246. client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
  247. return http.ErrUseLastResponse
  248. }
  249. }
  250. if c.useProxy && c.ClientProxyURL != "" {
  251. proxyURL, err := url.Parse(c.ClientProxyURL)
  252. if err != nil {
  253. logger.Error("[HttpClient] Proxy URL error: %v", err)
  254. } else {
  255. logger.Debug("[HttpClient] Use proxy: %s", proxyURL)
  256. transport.Proxy = http.ProxyURL(proxyURL)
  257. }
  258. }
  259. client.Transport = transport
  260. return client
  261. }
  262. func (c *Client) buildHeaders() http.Header {
  263. headers := make(http.Header)
  264. headers.Add("Accept", "*/*")
  265. if c.requestUserAgent != "" {
  266. headers.Add("User-Agent", c.requestUserAgent)
  267. }
  268. if c.requestEtagHeader != "" {
  269. headers.Add("If-None-Match", c.requestEtagHeader)
  270. }
  271. if c.requestLastModifiedHeader != "" {
  272. headers.Add("If-Modified-Since", c.requestLastModifiedHeader)
  273. }
  274. if c.requestAuthorizationHeader != "" {
  275. headers.Add("Authorization", c.requestAuthorizationHeader)
  276. }
  277. if c.requestCookie != "" {
  278. headers.Add("Cookie", c.requestCookie)
  279. }
  280. headers.Add("Connection", "close")
  281. return headers
  282. }