diff options
| author | Jeroen Wijenbergh <jeroen.wijenbergh@geant.org> | 2026-02-12 12:34:08 +0100 |
|---|---|---|
| committer | Jeroen Wijenbergh <jeroen.wijenbergh@geant.org> | 2026-02-12 12:59:03 +0100 |
| commit | a30ef6b27e578a4cf0a674b24f5b52b4c1516c63 (patch) | |
| tree | 27c7321cbceac2a487c1ba17151711de3d438a53 /internal/http/http.go | |
| parent | b00ce8214479c50e137db73c77b0cc1393c5e7d4 (diff) | |
All: Rename packages that sound useless or clash with std
Diffstat (limited to 'internal/http/http.go')
| -rw-r--r-- | internal/http/http.go | 288 |
1 files changed, 0 insertions, 288 deletions
diff --git a/internal/http/http.go b/internal/http/http.go deleted file mode 100644 index 7b9b70d..0000000 --- a/internal/http/http.go +++ /dev/null @@ -1,288 +0,0 @@ -// Package http defines higher level helpers for the net/http package -package http - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "io" - "log/slog" - "net/http" - "net/url" - "path" - "strings" - "time" - - "codeberg.org/eduVPN/eduvpn-common/internal/version" -) - -// UserAgent is the user agent that is used for requests -var UserAgent string - -// URLParameters is a type used for the parameters in the URL. -type URLParameters map[string]string - -// OptionalParams is a structure that defines the optional parameters that are given when making a HTTP call. -type OptionalParams struct { - Headers http.Header - URLParameters URLParameters - Body url.Values - Timeout time.Duration -} - -func cleanPath(u *url.URL, trailing bool) string { - if u.Path != "" { - // Clean the path - // https://pkg.go.dev/path#Clean - u.Path = path.Clean(u.Path) - } - - str := u.String() - - // Make sure the URL ends with a / - if trailing && str[len(str)-1:] != "/" { - str += "/" - } - return str -} - -// EnsureValidURL ensures that the input URL is valid to be used internally -// It does the following -// - Sets the scheme to https if none is given -// - It 'cleans' up the path using path.Clean -// - It makes sure that the URL ends with a / -// It returns an error if the URL cannot be parsed. -func EnsureValidURL(s string, trailing bool) (string, error) { - u, err := url.Parse(s) - if err != nil { - return "", fmt.Errorf("failed parsing url with error: %w", err) - } - - // Make sure the scheme is always https - if u.Scheme != "https" { - u.Scheme = "https" - } - return cleanPath(u, trailing), nil -} - -// JoinURLPath joins url's path, in go 1.19 we can use url.JoinPath -func JoinURLPath(u string, p string) (string, error) { - pu, err := url.Parse(u) - if err != nil { - return "", fmt.Errorf("failed to parse url for joining paths with error: %w", err) - } - pp, err := url.Parse(p) - if err != nil { - return "", fmt.Errorf("failed to parse path for joining paths with error: %w", err) - } - fp := pu.ResolveReference(pp) - - // We also clean the path for consistency - return cleanPath(fp, false), nil -} - -// ConstructURL creates a URL with the included parameters. -func ConstructURL(u *url.URL, params URLParameters) (string, error) { - q := u.Query() - - for p, value := range params { - q.Set(p, value) - } - u.RawQuery = q.Encode() - return u.String(), nil -} - -// optionalURL ensures that the URL contains the optional parameters -// it returns the url (with parameters if success) and an error indicating success. -func optionalURL(urlStr string, opts *OptionalParams) (string, error) { - u, err := url.Parse(urlStr) - if err != nil { - return "", fmt.Errorf("failed to construct parse url '%s' with error: %w", urlStr, err) - } - // Make sure the scheme is always set to HTTPS - if u.Scheme != "https" { - u.Scheme = "https" - } - - if opts == nil { - return u.String(), nil - } - - return ConstructURL(u, opts.URLParameters) -} - -// optionalHeaders ensures that the HTTP request uses the optional headers if defined. -func optionalHeaders(req *http.Request, opts *OptionalParams) { - // Add headers - if opts != nil && req != nil && opts.Headers != nil { - for k, v := range opts.Headers { - for _, cv := range v { - req.Header.Add(k, cv) - } - } - } -} - -// optionalBodyReader returns a HTTP body reader if there is a body, otherwise nil. -func optionalBodyReader(opts *OptionalParams) io.Reader { - if opts != nil && opts.Body != nil { - return strings.NewReader(opts.Body.Encode()) - } - return nil -} - -// Client is a wrapper around http.Client with some convenience features -// - A default timeout of 5 seconds -// - A read limiter to prevent servers from sending large amounts of data -// - Checking on http code with custom errors -type Client struct { - // Client is the HTTP Client that sends the request - Client *http.Client - // ReadLimit denotes the maximum amount of bytes that are read in HTTP responses - // This is used to prevent servers from sending huge amounts of data - // A limit of 16MB, although maybe much larger than needed, ensures that we do not run into problems - ReadLimit int64 - - // Timeout denotes the default timeout for each request - Timeout time.Duration -} - -// tls13Transport returns a http.Transport with the minimum TLS version set to 1.3 -func tls13Transport() *http.Transport { - tr := http.DefaultTransport.(*http.Transport).Clone() - tr.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS13} - return tr -} - -// DefaultTransport is the default HTTP transport to use -// by default it is a transport that only allows TLS 1.3 -var DefaultTransport = tls13Transport() - -// NewClient returns a HTTP client with some default settings -func NewClient(client *http.Client) *Client { - c := client - if c == nil { - c = &http.Client{ - Transport: DefaultTransport, - } - } - // if a client is non-nil it uses its own transport - // for the OAuth client we also make sure TLS 1.3 is set - // TODO: Should we double verify that MinVersion is 1.3 or is that overkill? - - // ReadLimit denotes the maximum amount of bytes that are read in HTTP responses - // This is used to prevent servers from sending huge amounts of data - // A limit of 16MB, although maybe much larger than needed, ensures that we do not run into problems - // The timeout is 10 seconds by default. We pass it here and not in the http client because we want to do it per request - return &Client{Client: c, ReadLimit: 16 << 20, Timeout: 10 * time.Second} -} - -// Get creates a Get request and returns the headers, body and an error. -func (c *Client) Get(ctx context.Context, url string) (http.Header, []byte, error) { - return c.Do(ctx, http.MethodGet, url, nil) -} - -// Do sends a HTTP request using a method (e.g. GET, POST), an url and optional parameters -// It returns the HTTP headers, the body and an error if there is one. -func (c *Client) Do(ctx context.Context, method string, urlStr string, opts *OptionalParams) (http.Header, []byte, error) { - // Make sure the url contains all the parameters - // This can return an error, - // it already has the right error, so we don't wrap it further - urlStr, err := optionalURL(urlStr, opts) - if err != nil { - // No further type wrapping is needed here - return nil, nil, err - } - - // The timeout is configurable for each request - timeout := c.Timeout - if opts != nil && opts.Timeout.Seconds() > 0 { - timeout = opts.Timeout - } - - ctx, cncl := context.WithTimeout(ctx, timeout) - defer cncl() - - slog.Debug("sending request", "method", method, "url", urlStr) - - // Create request object with the body reader generated from the optional arguments - req, err := http.NewRequestWithContext(ctx, method, urlStr, optionalBodyReader(opts)) - if err != nil { - return nil, nil, fmt.Errorf("failed HTTP request with method: '%s', url: '%s' and error: %w", method, urlStr, err) - } - if UserAgent != "" { - req.Header.Add("User-Agent", UserAgent) - } - - // Make sure the headers contain all the parameters - optionalHeaders(req, opts) - - // Do request - res, err := c.Client.Do(req) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - return nil, nil, &TimeoutError{URL: urlStr, Method: method} - } - return nil, nil, fmt.Errorf("failed HTTP request with method: '%s', url: '%s' and error: %w", method, urlStr, err) - } - - // Request successful, make sure body is closed at the end - defer func() { - _ = res.Body.Close() - }() - - // Return a string - // A max bytes reader is normally used for request bodies with a writer - // However, this is still nice to use because unlike a limitreader, it returns an error if the body is too large - // We use this function without a writer so we pass nil - // We impose a limit because servers could be malicious and send huge amounts of data - r := http.MaxBytesReader(nil, res.Body, c.ReadLimit) - body, err := io.ReadAll(r) - if err != nil { - return res.Header, nil, fmt.Errorf("failed HTTP request with method: '%s', url: '%s', max bytes size: '%v' and error: %w", method, urlStr, c.ReadLimit, err) - } - if res.StatusCode < 200 || res.StatusCode > 299 { - return res.Header, body, fmt.Errorf("failed HTTP request with method: '%s' due to a status error: %w", method, &StatusError{URL: urlStr, Body: string(body), Status: res.StatusCode}) - } - - // Return the body in bytes and signal the status error if there was one - return res.Header, body, nil -} - -// TimeoutError indicates that we have gotten a timeout -type TimeoutError struct { - URL string - Method string -} - -// Error returns the TimeoutError as an error string. -func (e *TimeoutError) Error() string { - return fmt.Sprintf( - "timeout in obtaining HTTP resource: '%s' with method: '%s'", - e.URL, - e.Method, - ) -} - -// StatusError indicates that we have received a HTTP status error. -type StatusError struct { - URL string - Body string - Status int -} - -// Error returns the StatusError as an error string. -func (e *StatusError) Error() string { - return fmt.Sprintf( - "failed obtaining HTTP resource: '%s' as it gave an unsuccessful status code: '%d'. Body: '%s'", - e.URL, - e.Status, - e.Body, - ) -} - -// RegisterAgent registers the user agent for client and version -func RegisterAgent(client string, verApp string) { - UserAgent = fmt.Sprintf("%s/%s eduvpn-common/%s", client, verApp, version.Version) -} |
