client.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package client // import "miniflux.app/v2/internal/http/client"
  4. import (
  5. "bytes"
  6. "context"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net"
  12. "net/http"
  13. "time"
  14. "miniflux.app/v2/internal/config"
  15. "miniflux.app/v2/internal/urllib"
  16. "miniflux.app/v2/internal/version"
  17. )
  18. const defaultRequestTimeout = 10 * time.Second
  19. // ErrPrivateNetwork is returned when a connection to a private network is blocked.
  20. var ErrPrivateNetwork = errors.New("client: connection to private network is blocked")
  21. // Options holds configuration for creating an HTTP client.
  22. type Options struct {
  23. Timeout time.Duration
  24. BlockPrivateNetworks bool
  25. }
  26. // NewClientWithOptions creates a new HTTP client with the specified options.
  27. func NewClientWithOptions(opts Options) *http.Client {
  28. if !opts.BlockPrivateNetworks {
  29. return &http.Client{Timeout: opts.Timeout}
  30. }
  31. dialer := &net.Dialer{
  32. Timeout: opts.Timeout,
  33. }
  34. transport := &http.Transport{
  35. // The check is performed at connect time on the actual resolved IP, which eliminates TOCTOU / DNS-rebinding vulnerabilities.
  36. DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
  37. host, port, err := net.SplitHostPort(addr)
  38. if err != nil {
  39. return nil, fmt.Errorf("client: unable to parse address %q: %w", addr, err)
  40. }
  41. ips, err := net.LookupIP(host)
  42. if err != nil {
  43. return nil, fmt.Errorf("client: unable to resolve host %q: %w", host, err)
  44. }
  45. var safeIP net.IP
  46. for _, ip := range ips {
  47. if !urllib.IsNonPublicIP(ip) {
  48. safeIP = ip
  49. break
  50. }
  51. }
  52. if safeIP == nil {
  53. return nil, fmt.Errorf("%w: host %q resolves to a non-public IP address", ErrPrivateNetwork, host)
  54. }
  55. safeAddr := net.JoinHostPort(safeIP.String(), port)
  56. return dialer.DialContext(ctx, network, safeAddr)
  57. },
  58. }
  59. return &http.Client{
  60. Timeout: opts.Timeout,
  61. Transport: transport,
  62. }
  63. }
  64. // requestBuilder builds and executes HTTP requests with the builder pattern.
  65. type requestBuilder struct {
  66. err error
  67. endpoint string
  68. method string
  69. body io.Reader
  70. headers http.Header
  71. }
  72. // NewRequestBuilder creates a new request builder for the given endpoint.
  73. func NewRequestBuilder(endpoint string) *requestBuilder {
  74. return &requestBuilder{
  75. endpoint: endpoint,
  76. method: http.MethodGet,
  77. headers: make(http.Header),
  78. }
  79. }
  80. // WithMethod sets the HTTP method.
  81. func (r *requestBuilder) WithMethod(method string) *requestBuilder {
  82. r.method = method
  83. return r
  84. }
  85. // WithHeader sets a header value.
  86. func (r *requestBuilder) WithHeader(key, value string) *requestBuilder {
  87. r.headers.Set(key, value)
  88. return r
  89. }
  90. // WithJSON marshals payload as JSON, sets the body and Content-Type.
  91. func (r *requestBuilder) WithJSON(payload any) *requestBuilder {
  92. requestBody, err := json.Marshal(payload)
  93. if err != nil {
  94. r.err = fmt.Errorf("unable to encode request body: %w", err)
  95. return r
  96. }
  97. return r.WithJSONBody(requestBody)
  98. }
  99. // WithJSONBody sets an already-marshaled JSON body and the Content-Type.
  100. // It is useful when the caller needs the encoded payload for another
  101. // purpose (e.g. computing a signature) to avoid marshaling it twice.
  102. func (r *requestBuilder) WithJSONBody(body []byte) *requestBuilder {
  103. r.body = bytes.NewReader(body)
  104. r.headers.Set("Content-Type", "application/json")
  105. return r
  106. }
  107. // Do builds and executes the request.
  108. //
  109. // Private networks are blocked unless explicitly allowed through the
  110. // INTEGRATION_ALLOW_PRIVATE_NETWORKS option.
  111. func (r *requestBuilder) Do() (*http.Response, error) {
  112. if r.err != nil {
  113. return nil, r.err
  114. }
  115. // The request is assembled lazily here rather than being stored as a
  116. // prebuilt *http.Request in the builder: http.NewRequest inspects the
  117. // body's concrete type (e.g. *bytes.Reader) to populate ContentLength and
  118. // GetBody. Constructing it only once the body is known yields a correct
  119. // Content-Length header and lets the client replay the body on redirects.
  120. req, err := http.NewRequest(r.method, r.endpoint, r.body)
  121. if err != nil {
  122. return nil, fmt.Errorf("unable to create request: %w", err)
  123. }
  124. for key, values := range r.headers {
  125. for _, value := range values {
  126. req.Header.Add(key, value)
  127. }
  128. }
  129. req.Header.Set("User-Agent", "Miniflux/"+version.Version)
  130. clientOptions := Options{
  131. Timeout: defaultRequestTimeout,
  132. BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks(),
  133. }
  134. response, err := NewClientWithOptions(clientOptions).Do(req)
  135. if err != nil {
  136. return nil, fmt.Errorf("unable to send request: %w", err)
  137. }
  138. return response, nil
  139. }