2023-10-21 19:50:29 -07:00
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package fetcher // import "miniflux.app/v2/internal/reader/fetcher"
import (
"crypto/tls"
"encoding/base64"
"log/slog"
"net"
"net/http"
"net/url"
"time"
2025-04-06 14:45:56 -07:00
"miniflux.app/v2/internal/proxyrotator"
2023-10-21 19:50:29 -07:00
)
const (
defaultHTTPClientTimeout = 20
defaultHTTPClientMaxBodySize = 15 * 1024 * 1024
2024-01-13 13:24:20 -08:00
defaultAcceptHeader = "application/xml, application/atom+xml, application/rss+xml, application/rdf+xml, application/feed+json, text/html, */*;q=0.9"
2023-10-21 19:50:29 -07:00
)
type RequestBuilder struct {
headers http . Header
2025-04-06 14:45:56 -07:00
clientProxyURL * url . URL
2023-10-21 19:50:29 -07:00
useClientProxy bool
clientTimeout int
withoutRedirects bool
ignoreTLSErrors bool
2024-02-24 22:08:23 -08:00
disableHTTP2 bool
2025-04-06 14:45:56 -07:00
proxyRotator * proxyrotator . ProxyRotator
2023-10-21 19:50:29 -07:00
}
func NewRequestBuilder ( ) * RequestBuilder {
return & RequestBuilder {
headers : make ( http . Header ) ,
clientTimeout : defaultHTTPClientTimeout ,
}
}
func ( r * RequestBuilder ) WithHeader ( key , value string ) * RequestBuilder {
r . headers . Set ( key , value )
return r
}
func ( r * RequestBuilder ) WithETag ( etag string ) * RequestBuilder {
if etag != "" {
r . headers . Set ( "If-None-Match" , etag )
}
return r
}
func ( r * RequestBuilder ) WithLastModified ( lastModified string ) * RequestBuilder {
if lastModified != "" {
r . headers . Set ( "If-Modified-Since" , lastModified )
}
return r
}
2023-11-15 22:12:00 -05:00
func ( r * RequestBuilder ) WithUserAgent ( userAgent string , defaultUserAgent string ) * RequestBuilder {
2023-10-21 19:50:29 -07:00
if userAgent != "" {
r . headers . Set ( "User-Agent" , userAgent )
} else {
2023-11-15 22:12:00 -05:00
r . headers . Set ( "User-Agent" , defaultUserAgent )
2023-10-21 19:50:29 -07:00
}
return r
}
func ( r * RequestBuilder ) WithCookie ( cookie string ) * RequestBuilder {
if cookie != "" {
r . headers . Set ( "Cookie" , cookie )
}
return r
}
func ( r * RequestBuilder ) WithUsernameAndPassword ( username , password string ) * RequestBuilder {
if username != "" && password != "" {
r . headers . Set ( "Authorization" , "Basic " + base64 . StdEncoding . EncodeToString ( [ ] byte ( username + ":" + password ) ) )
}
return r
}
2025-04-06 14:45:56 -07:00
func ( r * RequestBuilder ) WithProxyRotator ( proxyRotator * proxyrotator . ProxyRotator ) * RequestBuilder {
r . proxyRotator = proxyRotator
return r
}
func ( r * RequestBuilder ) WithCustomApplicationProxyURL ( proxyURL * url . URL ) * RequestBuilder {
2023-10-21 19:50:29 -07:00
r . clientProxyURL = proxyURL
return r
}
2025-04-06 14:45:56 -07:00
func ( r * RequestBuilder ) UseCustomApplicationProxyURL ( value bool ) * RequestBuilder {
2023-10-21 19:50:29 -07:00
r . useClientProxy = value
return r
}
func ( r * RequestBuilder ) WithTimeout ( timeout int ) * RequestBuilder {
r . clientTimeout = timeout
return r
}
func ( r * RequestBuilder ) WithoutRedirects ( ) * RequestBuilder {
r . withoutRedirects = true
return r
}
2024-02-24 22:08:23 -08:00
func ( r * RequestBuilder ) DisableHTTP2 ( value bool ) * RequestBuilder {
r . disableHTTP2 = value
return r
}
2023-10-21 19:50:29 -07:00
func ( r * RequestBuilder ) IgnoreTLSErrors ( value bool ) * RequestBuilder {
r . ignoreTLSErrors = value
return r
}
func ( r * RequestBuilder ) ExecuteRequest ( requestURL string ) ( * http . Response , error ) {
2024-06-13 13:09:47 +02:00
// We get the safe ciphers
ciphers := tls . CipherSuites ( )
if r . ignoreTLSErrors {
// and the insecure ones if we are ignoring TLS errors. This allows to connect to badly configured servers anyway
ciphers = append ( ciphers , tls . InsecureCipherSuites ( ) ... )
}
cipherSuites := [ ] uint16 { }
for _ , cipher := range ciphers {
cipherSuites = append ( cipherSuites , cipher . ID )
}
2023-10-21 19:50:29 -07:00
transport := & http . Transport {
Proxy : http . ProxyFromEnvironment ,
2023-12-15 16:40:04 +01:00
// Setting `DialContext` disables HTTP/2, this option forces the transport to try HTTP/2 regardless.
ForceAttemptHTTP2 : true ,
2023-10-21 19:50:29 -07:00
DialContext : ( & net . Dialer {
// Default is 30s.
Timeout : 10 * time . Second ,
// Default is 30s.
KeepAlive : 15 * time . Second ,
} ) . DialContext ,
// Default is 100.
MaxIdleConns : 50 ,
// Default is 90s.
IdleConnTimeout : 10 * time . Second ,
TLSClientConfig : & tls . Config {
2024-06-13 13:09:47 +02:00
CipherSuites : cipherSuites ,
2023-10-21 19:50:29 -07:00
InsecureSkipVerify : r . ignoreTLSErrors ,
} ,
}
2024-02-24 22:08:23 -08:00
if r . disableHTTP2 {
transport . ForceAttemptHTTP2 = false
// https://pkg.go.dev/net/http#hdr-HTTP_2
// Programs that must disable HTTP/2 can do so by setting [Transport.TLSNextProto] (for clients) or [Server.TLSNextProto] (for servers) to a non-nil, empty map.
transport . TLSNextProto = map [ string ] func ( string , * tls . Conn ) http . RoundTripper { }
}
2025-04-06 14:45:56 -07:00
var clientProxyURL * url . URL
if r . useClientProxy && r . clientProxyURL != nil {
clientProxyURL = r . clientProxyURL
} else if r . proxyRotator != nil && r . proxyRotator . HasProxies ( ) {
clientProxyURL = r . proxyRotator . GetNextProxy ( )
}
var clientProxyURLRedacted string
if clientProxyURL != nil {
transport . Proxy = http . ProxyURL ( clientProxyURL )
clientProxyURLRedacted = clientProxyURL . Redacted ( )
2023-10-21 19:50:29 -07:00
}
client := & http . Client {
Timeout : time . Duration ( r . clientTimeout ) * time . Second ,
}
if r . withoutRedirects {
client . CheckRedirect = func ( req * http . Request , via [ ] * http . Request ) error {
return http . ErrUseLastResponse
}
}
client . Transport = transport
req , err := http . NewRequest ( "GET" , requestURL , nil )
if err != nil {
return nil , err
}
req . Header = r . headers
2024-04-18 21:44:55 -07:00
req . Header . Set ( "Accept-Encoding" , "br, gzip" )
2024-01-13 13:24:20 -08:00
req . Header . Set ( "Accept" , defaultAcceptHeader )
2023-10-21 19:50:29 -07:00
req . Header . Set ( "Connection" , "close" )
slog . Debug ( "Making outgoing request" , slog . Group ( "request" ,
slog . String ( "method" , req . Method ) ,
slog . String ( "url" , req . URL . String ( ) ) ,
slog . Any ( "headers" , req . Header ) ,
slog . Bool ( "without_redirects" , r . withoutRedirects ) ,
2025-04-06 14:45:56 -07:00
slog . Bool ( "use_app_client_proxy" , r . useClientProxy ) ,
slog . String ( "client_proxy_url" , clientProxyURLRedacted ) ,
2024-02-24 22:08:23 -08:00
slog . Bool ( "ignore_tls_errors" , r . ignoreTLSErrors ) ,
slog . Bool ( "disable_http2" , r . disableHTTP2 ) ,
2023-10-21 19:50:29 -07:00
) )
return client . Do ( req )
}