client.go 8.5 KB

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