| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
- // SPDX-License-Identifier: Apache-2.0
- package client // import "miniflux.app/v2/internal/http/client"
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net"
- "net/http"
- "time"
- "miniflux.app/v2/internal/config"
- "miniflux.app/v2/internal/urllib"
- "miniflux.app/v2/internal/version"
- )
- const defaultRequestTimeout = 10 * time.Second
- // ErrPrivateNetwork is returned when a connection to a private network is blocked.
- var ErrPrivateNetwork = errors.New("client: connection to private network is blocked")
- // Options holds configuration for creating an HTTP client.
- type Options struct {
- Timeout time.Duration
- BlockPrivateNetworks bool
- }
- // NewClientWithOptions creates a new HTTP client with the specified options.
- func NewClientWithOptions(opts Options) *http.Client {
- if !opts.BlockPrivateNetworks {
- return &http.Client{Timeout: opts.Timeout}
- }
- dialer := &net.Dialer{
- Timeout: opts.Timeout,
- }
- transport := &http.Transport{
- // The check is performed at connect time on the actual resolved IP, which eliminates TOCTOU / DNS-rebinding vulnerabilities.
- DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
- host, port, err := net.SplitHostPort(addr)
- if err != nil {
- return nil, fmt.Errorf("client: unable to parse address %q: %w", addr, err)
- }
- ips, err := net.LookupIP(host)
- if err != nil {
- return nil, fmt.Errorf("client: unable to resolve host %q: %w", host, err)
- }
- var safeIP net.IP
- for _, ip := range ips {
- if !urllib.IsNonPublicIP(ip) {
- safeIP = ip
- break
- }
- }
- if safeIP == nil {
- return nil, fmt.Errorf("%w: host %q resolves to a non-public IP address", ErrPrivateNetwork, host)
- }
- safeAddr := net.JoinHostPort(safeIP.String(), port)
- return dialer.DialContext(ctx, network, safeAddr)
- },
- }
- return &http.Client{
- Timeout: opts.Timeout,
- Transport: transport,
- }
- }
- // requestBuilder builds and executes HTTP requests with the builder pattern.
- type requestBuilder struct {
- err error
- endpoint string
- method string
- body io.Reader
- headers http.Header
- }
- // NewRequestBuilder creates a new request builder for the given endpoint.
- func NewRequestBuilder(endpoint string) *requestBuilder {
- return &requestBuilder{
- endpoint: endpoint,
- method: http.MethodGet,
- headers: make(http.Header),
- }
- }
- // WithMethod sets the HTTP method.
- func (r *requestBuilder) WithMethod(method string) *requestBuilder {
- r.method = method
- return r
- }
- // WithHeader sets a header value.
- func (r *requestBuilder) WithHeader(key, value string) *requestBuilder {
- r.headers.Set(key, value)
- return r
- }
- // WithJSON marshals payload as JSON, sets the body and Content-Type.
- func (r *requestBuilder) WithJSON(payload any) *requestBuilder {
- requestBody, err := json.Marshal(payload)
- if err != nil {
- r.err = fmt.Errorf("unable to encode request body: %w", err)
- return r
- }
- return r.WithJSONBody(requestBody)
- }
- // WithJSONBody sets an already-marshaled JSON body and the Content-Type.
- // It is useful when the caller needs the encoded payload for another
- // purpose (e.g. computing a signature) to avoid marshaling it twice.
- func (r *requestBuilder) WithJSONBody(body []byte) *requestBuilder {
- r.body = bytes.NewReader(body)
- r.headers.Set("Content-Type", "application/json")
- return r
- }
- // Do builds and executes the request.
- //
- // Private networks are blocked unless explicitly allowed through the
- // INTEGRATION_ALLOW_PRIVATE_NETWORKS option.
- func (r *requestBuilder) Do() (*http.Response, error) {
- if r.err != nil {
- return nil, r.err
- }
- // The request is assembled lazily here rather than being stored as a
- // prebuilt *http.Request in the builder: http.NewRequest inspects the
- // body's concrete type (e.g. *bytes.Reader) to populate ContentLength and
- // GetBody. Constructing it only once the body is known yields a correct
- // Content-Length header and lets the client replay the body on redirects.
- req, err := http.NewRequest(r.method, r.endpoint, r.body)
- if err != nil {
- return nil, fmt.Errorf("unable to create request: %w", err)
- }
- for key, values := range r.headers {
- for _, value := range values {
- req.Header.Add(key, value)
- }
- }
- req.Header.Set("User-Agent", "Miniflux/"+version.Version)
- clientOptions := Options{
- Timeout: defaultRequestTimeout,
- BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks(),
- }
- response, err := NewClientWithOptions(clientOptions).Do(req)
- if err != nil {
- return nil, fmt.Errorf("unable to send request: %w", err)
- }
- return response, nil
- }
|