2023-10-22 04:50:29 +02: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"
)
const (
defaultHTTPClientTimeout = 20
defaultHTTPClientMaxBodySize = 15 * 1024 * 1024
2024-01-13 22:24:20 +01:00
defaultAcceptHeader = "application/xml, application/atom+xml, application/rss+xml, application/rdf+xml, application/feed+json, text/html, */*;q=0.9"
2023-10-22 04:50:29 +02:00
)
type RequestBuilder struct {
headers http . Header
clientProxyURL string
useClientProxy bool
clientTimeout int
withoutRedirects bool
ignoreTLSErrors bool
2024-02-25 07:08:23 +01:00
disableHTTP2 bool
2023-10-22 04:50:29 +02: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-16 04:12:00 +01:00
func ( r * RequestBuilder ) WithUserAgent ( userAgent string , defaultUserAgent string ) * RequestBuilder {
2023-10-22 04:50:29 +02:00
if userAgent != "" {
r . headers . Set ( "User-Agent" , userAgent )
} else {
2023-11-16 04:12:00 +01:00
r . headers . Set ( "User-Agent" , defaultUserAgent )
2023-10-22 04:50:29 +02: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
}
func ( r * RequestBuilder ) WithProxy ( proxyURL string ) * RequestBuilder {
r . clientProxyURL = proxyURL
return r
}
func ( r * RequestBuilder ) UseProxy ( value bool ) * RequestBuilder {
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-25 07:08:23 +01:00
func ( r * RequestBuilder ) DisableHTTP2 ( value bool ) * RequestBuilder {
r . disableHTTP2 = value
return r
}
2023-10-22 04:50:29 +02:00
func ( r * RequestBuilder ) IgnoreTLSErrors ( value bool ) * RequestBuilder {
r . ignoreTLSErrors = value
return r
}
func ( r * RequestBuilder ) ExecuteRequest ( requestURL string ) ( * http . Response , error ) {
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-22 04:50:29 +02: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 {
InsecureSkipVerify : r . ignoreTLSErrors ,
} ,
}
2024-02-25 07:08:23 +01: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 { }
}
2023-10-22 04:50:29 +02:00
if r . useClientProxy && r . clientProxyURL != "" {
if proxyURL , err := url . Parse ( r . clientProxyURL ) ; err != nil {
slog . Warn ( "Unable to parse proxy URL" ,
slog . String ( "proxy_url" , r . clientProxyURL ) ,
slog . Any ( "error" , err ) ,
)
} else {
transport . Proxy = http . ProxyURL ( proxyURL )
}
}
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-19 06:44:55 +02:00
req . Header . Set ( "Accept-Encoding" , "br, gzip" )
2024-01-13 22:24:20 +01:00
req . Header . Set ( "Accept" , defaultAcceptHeader )
2023-10-22 04:50:29 +02: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 ) ,
slog . Bool ( "with_proxy" , r . useClientProxy ) ,
slog . String ( "proxy_url" , r . clientProxyURL ) ,
2024-02-25 07:08:23 +01:00
slog . Bool ( "ignore_tls_errors" , r . ignoreTLSErrors ) ,
slog . Bool ( "disable_http2" , r . disableHTTP2 ) ,
2023-10-22 04:50:29 +02:00
) )
return client . Do ( req )
}