feat: initial goget release — modern IPv6-first download utility

This commit is contained in:
2026-06-26 21:21:58 +02:00
commit 53db81e1f7
147 changed files with 45931 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
//go:build linux || freebsd
// +build linux freebsd
package auth
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// OAuthConfig configuration for OAuth 2.0
type OAuthConfig struct {
ClientID string `json:"client_id,omitempty" toml:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty" toml:"client_secret,omitempty"`
TokenURL string `json:"token_url,omitempty" toml:"token_url,omitempty"`
AuthURL string `json:"auth_url,omitempty" toml:"auth_url,omitempty"`
RedirectURI string `json:"redirect_uri,omitempty" toml:"redirect_uri,omitempty"`
Scopes []string `json:"scopes,omitempty" toml:"scopes,omitempty"`
AccessToken string `json:"access_token,omitempty" toml:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty" toml:"refresh_token,omitempty"`
TokenType string `json:"token_type,omitempty" toml:"token_type,omitempty"`
Expiry time.Time `json:"expiry,omitempty" toml:"expiry,omitempty"`
GrantType string `json:"grant_type,omitempty" toml:"grant_type,omitempty"` // authorization_code, client_credentials, password, refresh_token
}
// TokenResponse response from a token endpoint
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
Scope string `json:"scope,omitempty"`
Error string `json:"error,omitempty"`
ErrorDesc string `json:"error_description,omitempty"`
}
// OAuthClient client for OAuth 2.0 authentication
type OAuthClient struct {
config *OAuthConfig
client *http.Client
}
// NewOAuthClient creates a new OAuth client
func NewOAuthClient(cfg *OAuthConfig) *OAuthClient {
if cfg.TokenType == "" {
cfg.TokenType = "Bearer"
}
return &OAuthClient{
config: cfg,
client: &http.Client{Timeout: 30 * time.Second},
}
}
// IsTokenValid checks whether the token is valid
func (c *OAuthClient) IsTokenValid() bool {
if c.config.AccessToken == "" {
return false
}
if c.config.Expiry.IsZero() {
return true // No expiry, assume valid
}
// Token is valid if it expires in more than 30 seconds
return time.Now().Add(30 * time.Second).Before(c.config.Expiry)
}
// RequestToken gets new access token
func (c *OAuthClient) RequestToken(ctx context.Context) (*TokenResponse, error) {
if c.config.TokenURL != "" {
u, err := url.Parse(c.config.TokenURL)
if err != nil {
return nil, fmt.Errorf("invalid oauth token URL: %w", err)
}
if u.Scheme != "https" && u.Hostname() != "localhost" && u.Hostname() != "127.0.0.1" && u.Hostname() != "::1" {
return nil, fmt.Errorf("oauth token URL must use HTTPS for security")
}
}
formData := url.Values{}
grantType := c.config.GrantType
if grantType == "" {
grantType = "client_credentials"
}
formData.Set("grant_type", grantType)
switch grantType {
case "client_credentials":
formData.Set("client_id", c.config.ClientID)
formData.Set("client_secret", c.config.ClientSecret)
if len(c.config.Scopes) > 0 {
formData.Set("scope", strings.Join(c.config.Scopes, " "))
}
case "password":
formData.Set("client_id", c.config.ClientID)
formData.Set("client_secret", c.config.ClientSecret)
// Username and password would need to be passed separately
// This is handled by caller
case "authorization_code":
formData.Set("client_id", c.config.ClientID)
formData.Set("client_secret", c.config.ClientSecret)
// Code and redirect_uri would need to be passed separately
case "refresh_token":
formData.Set("client_id", c.config.ClientID)
formData.Set("client_secret", c.config.ClientSecret)
formData.Set("refresh_token", c.config.RefreshToken)
}
req, err := http.NewRequestWithContext(ctx, "POST", c.config.TokenURL, strings.NewReader(formData.Encode()))
if err != nil {
return nil, fmt.Errorf("failed to create token request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("token request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read token response: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("token endpoint returned %s", formatOAuthError(resp.StatusCode, body))
}
var tokenResp TokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("failed to parse token response: %w", err)
}
if tokenResp.Error != "" {
return nil, fmt.Errorf("oauth error: %s - %s", tokenResp.Error, tokenResp.ErrorDesc)
}
return &tokenResp, nil
}
// ApplyToken applies the token to the HTTP request
func (c *OAuthClient) ApplyToken(req *http.Request) {
if c.config.AccessToken == "" {
return
}
tokenType := c.config.TokenType
if tokenType == "" {
tokenType = "Bearer"
}
req.Header.Set("Authorization", fmt.Sprintf("%s %s", tokenType, c.config.AccessToken))
}
// RefreshToken refreshes access token using a refresh token
func (c *OAuthClient) RefreshToken(ctx context.Context) error {
if c.config.RefreshToken == "" {
return fmt.Errorf("no refresh token available")
}
oldGrantType := c.config.GrantType
c.config.GrantType = "refresh_token"
defer func() { c.config.GrantType = oldGrantType }()
tokenResp, err := c.RequestToken(ctx)
if err != nil {
return err
}
c.UpdateToken(tokenResp)
return nil
}
// UpdateToken updates internal token from response
func (c *OAuthClient) UpdateToken(tokenResp *TokenResponse) {
c.config.AccessToken = tokenResp.AccessToken
c.config.TokenType = tokenResp.TokenType
if tokenResp.RefreshToken != "" {
c.config.RefreshToken = tokenResp.RefreshToken
}
if tokenResp.ExpiresIn > 0 {
c.config.Expiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
}
}
// GetAuthorizationURL returns the authorization URL (for authorization_code flow)
func (c *OAuthClient) GetAuthorizationURL(state string) string {
if c.config.AuthURL == "" {
return ""
}
params := url.Values{}
params.Set("client_id", c.config.ClientID)
params.Set("redirect_uri", c.config.RedirectURI)
params.Set("response_type", "code")
params.Set("state", state)
if len(c.config.Scopes) > 0 {
params.Set("scope", strings.Join(c.config.Scopes, " "))
}
baseURL := c.config.AuthURL
if strings.Contains(baseURL, "?") {
return baseURL + "&" + params.Encode()
}
return baseURL + "?" + params.Encode()
}
// ExchangeCode exchanges an authorization code for an access token
func (c *OAuthClient) ExchangeCode(ctx context.Context, code string) (*TokenResponse, error) {
formData := url.Values{}
formData.Set("grant_type", "authorization_code")
formData.Set("code", code)
formData.Set("redirect_uri", c.config.RedirectURI)
formData.Set("client_id", c.config.ClientID)
formData.Set("client_secret", c.config.ClientSecret)
req, err := http.NewRequestWithContext(ctx, "POST", c.config.TokenURL, strings.NewReader(formData.Encode()))
if err != nil {
return nil, fmt.Errorf("failed to create code exchange request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("code exchange request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read code exchange response: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("code exchange endpoint returned %s", formatOAuthError(resp.StatusCode, body))
}
var tokenResp TokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("failed to parse code exchange response: %w", err)
}
if tokenResp.Error != "" {
return nil, fmt.Errorf("oauth error: %s - %s", tokenResp.Error, tokenResp.ErrorDesc)
}
return &tokenResp, nil
}
// formatOAuthError converts a non-2xx OAuth response into a human-readable
// error string while avoiding leaking sensitive data. The OAuth provider's
// raw response body is never returned verbatim: we first try to extract
// the structured `error` and `error_description` fields defined in RFC
// 6749 §5.2 (which is what well-behaved providers return), and only fall
// back to a 512-byte truncated snippet of the raw body if the body is
// not structured. This prevents upstream providers that echo back
// secrets, stack traces, or other sensitive payloads in their error
// bodies from leaking those payloads into our logs and error displays.
func formatOAuthError(statusCode int, body []byte) string {
if len(body) == 0 {
return fmt.Sprintf("status %d with empty body", statusCode)
}
var structured struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
if err := json.Unmarshal(body, &structured); err == nil && structured.Error != "" {
if structured.ErrorDescription != "" {
return fmt.Sprintf("status %d: %s - %s", statusCode, structured.Error, structured.ErrorDescription)
}
return fmt.Sprintf("status %d: %s", statusCode, structured.Error)
}
const maxSnippet = 512
snippet := string(body)
if len(snippet) > maxSnippet {
snippet = snippet[:maxSnippet] + "... (truncated)"
}
return fmt.Sprintf("status %d: %s", statusCode, snippet)
}