client.go 8.3 KB

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