From 76bc3cc39667a6c79b709791d1ab2e0f2555f715 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Wed, 29 Apr 2026 22:48:12 +0200 Subject: [PATCH 1/2] chore: complete docstring coverage --- README.md | 2 +- cmd/config.go | 2 + cmd/root.go | 3 + internal/certinfo/certinfo.go | 57 +++++++++++------ internal/jwtinfo/jwtinfo.go | 27 +++++--- internal/jwtinfo/jwtinfo_test.go | 4 +- internal/requests/requests.go | 88 +++++++++++++++++--------- internal/requests/requests_handlers.go | 1 - internal/style/style.go | 32 +++++++++- 9 files changed, 150 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 5fa42e0..771b04b 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ Generate a JWKS with a SHA-256-derived KID: ❯ https-wrench jwks --public-key-file public.pem ``` -### Sample output +## Sample output
HTTPS Wrench requests, sample configuration output diff --git a/cmd/config.go b/cmd/config.go index ca9585a..ede8cc9 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -4,6 +4,7 @@ import ( "github.com/xenos76/https-wrench/internal/requests" ) +// HTTPSWrenchConfig represents the top-level configuration for the application. type HTTPSWrenchConfig struct { Debug bool `mapstructure:"debug"` Verbose bool `mapstructure:"verbose"` @@ -11,6 +12,7 @@ type HTTPSWrenchConfig struct { requests.RequestsMetaConfig `mapstructure:",squash"` } +// NewHTTPSWrenchConfig returns a new HTTPSWrenchConfig with default values. func NewHTTPSWrenchConfig() *HTTPSWrenchConfig { c := HTTPSWrenchConfig{} return &c diff --git a/cmd/root.go b/cmd/root.go index 1cd977a..5156f63 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -85,6 +85,8 @@ Distributed under an open-source license: https://github.com/xenOs76/https-wrenc }, } +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() error { err := rootCmd.Execute() if err != nil { @@ -135,6 +137,7 @@ func initConfig() { } } +// LoadConfig reads the configuration file and unmarshals it into an HTTPSWrenchConfig struct. func LoadConfig() (*HTTPSWrenchConfig, error) { config := NewHTTPSWrenchConfig() diff --git a/internal/certinfo/certinfo.go b/internal/certinfo/certinfo.go index 7e308f0..2ee6762 100644 --- a/internal/certinfo/certinfo.go +++ b/internal/certinfo/certinfo.go @@ -12,7 +12,9 @@ import ( ) const ( - TLSTimeout = 3 * time.Second + // TLSTimeout is the maximum time to wait for a TLS handshake. + TLSTimeout = 3 * time.Second + // CertExpWarnDays is the number of days before expiration to start showing warnings. CertExpWarnDays = 40 privateKeyPwEnvVar = "CERTINFO_PKEY_PW" emptyString = "" @@ -20,20 +22,34 @@ const ( // CertinfoConfig holds the configuration and results for certificate and key information retrieval. type CertinfoConfig struct { - CACertsPool *x509.CertPool - CACertsFilePath string - CertsBundle []*x509.Certificate - CertsBundleFilePath string - CertsBundleFromKey bool - PrivKey crypto.PrivateKey - PrivKeyFilePath string - TLSEndpoint string - TLSEndpointHost string - TLSEndpointPort string - TLSEndpointCerts []*x509.Certificate + // CACertsPool is the pool of root CA certificates used for verification. + CACertsPool *x509.CertPool + // CACertsFilePath is the path to the CA certificate bundle file. + CACertsFilePath string + // CertsBundle is a slice of certificates loaded from a local bundle. + CertsBundle []*x509.Certificate + // CertsBundleFilePath is the path to the certificate bundle file. + CertsBundleFilePath string + // CertsBundleFromKey indicates if the certificate was derived from a private key. + CertsBundleFromKey bool + // PrivKey is the loaded private key. + PrivKey crypto.PrivateKey + // PrivKeyFilePath is the path to the private key file. + PrivKeyFilePath string + // TLSEndpoint is the host:port string of the remote TLS endpoint. + TLSEndpoint string + // TLSEndpointHost is the hostname part of the TLS endpoint. + TLSEndpointHost string + // TLSEndpointPort is the port part of the TLS endpoint. + TLSEndpointPort string + // TLSEndpointCerts is the slice of certificates retrieved from the remote endpoint. + TLSEndpointCerts []*x509.Certificate + // TLSEndpointCertsFromKey indicates if the endpoint certificates were matched with a private key. TLSEndpointCertsFromKey bool - TLSServerName string - TLSInsecure bool + // TLSServerName is the ServerName used for SNI. + TLSServerName string + // TLSInsecure indicates if certificate verification should be skipped. + TLSInsecure bool } // Reader defines an interface for reading files and passwords. @@ -48,10 +64,11 @@ type ( ) var ( - //nolint:revive + // TLSServerName is the default ServerName to use for SNI. TLSServerName string - TLSInsecure bool - inputReader InputReader + // TLSInsecure indicates if certificate verification should be skipped globally. + TLSInsecure bool + inputReader InputReader ) // ReadFile reads the content of a file from the filesystem. @@ -84,6 +101,8 @@ func NewCertinfoConfig() (*CertinfoConfig, error) { } // SetCaPoolFromFile loads a CA certificate pool from the specified PEM bundle file. +// Note that x509.SystemCertPool is not used in this case. All certificates +// from the system certificate pool are excluded. func (c *CertinfoConfig) SetCaPoolFromFile(filePath string, fileReader Reader) error { if filePath != emptyString { caCertsPool, err := GetRootCertsFromFile( @@ -120,8 +139,8 @@ func (c *CertinfoConfig) SetCertsFromFile(filePath string, fileReader Reader) er } // SetPrivateKeyFromFile loads a private key from the specified PEM file. -// If the key is encrypted, it will attempt to retrieve the passphrase from an environment variable or -// interactive prompt. +// If the key is encrypted, it will attempt to retrieve the passphrase from the environment +// variable passed as argument or from the interactive prompt. func (c *CertinfoConfig) SetPrivateKeyFromFile( filePath string, keyPwEnvVar string, diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index 92eb532..207ce93 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -31,25 +31,32 @@ var ( // JwtTokenData holds the raw and parsed data for access and refresh tokens. type JwtTokenData struct { - AccessTokenRaw string `json:"access_token"` //nolint:tagliatelle // OAuth token field name - AccessTokenJwt *jwt.Token - AccessTokenHeader []byte - AccessTokenClaims []byte - RefreshTokenRaw string `json:"refresh_token"` //nolint:tagliatelle // OAuth token field name - RefreshTokenJwt *jwt.Token + // AccessTokenRaw is the raw base64-encoded access token string. + AccessTokenRaw string `json:"access_token"` //nolint:tagliatelle // OAuth token field name + // AccessTokenJwt is the parsed access token object. + AccessTokenJwt *jwt.Token + // AccessTokenHeader is the decoded JSON header of the access token. + AccessTokenHeader []byte + // AccessTokenClaims is the decoded JSON claims of the access token. + AccessTokenClaims []byte + // RefreshTokenRaw is the raw base64-encoded refresh token string. + RefreshTokenRaw string `json:"refresh_token"` //nolint:tagliatelle // OAuth token field name + // RefreshTokenJwt is the parsed refresh token object. + RefreshTokenJwt *jwt.Token + // RefreshTokenHeader is the decoded JSON header of the refresh token. RefreshTokenHeader []byte + // RefreshTokenClaims is the decoded JSON claims of the refresh token. RefreshTokenClaims []byte } -// allReader is a function type that reads all data from an io.Reader. -type allReader func(io.Reader) ([]byte, error) +// AllReader is a function type that reads all data from an io.Reader. +type AllReader func(io.Reader) ([]byte, error) // RequestToken makes an HTTP POST request to the given URL with the provided values // to retrieve a JWT token. It handles both application/jwt and application/json // response types. -// //nolint:revive -func RequestToken(ctx context.Context, reqURL string, reqValues map[string]string, client *http.Client, readAll allReader) (JwtTokenData, error) { +func RequestToken(ctx context.Context, reqURL string, reqValues map[string]string, client *http.Client, readAll AllReader) (JwtTokenData, error) { if reqURL == emptyString { return JwtTokenData{}, errors.New("empty string provided as request URL") } diff --git a/internal/jwtinfo/jwtinfo_test.go b/internal/jwtinfo/jwtinfo_test.go index 5cfbe0e..2dab137 100644 --- a/internal/jwtinfo/jwtinfo_test.go +++ b/internal/jwtinfo/jwtinfo_test.go @@ -322,7 +322,7 @@ func TestParseWithJWKS(t *testing.T) { user string pass string scope string - bodyReader allReader + bodyReader AllReader expError bool expReqError bool }{ @@ -814,7 +814,7 @@ func TestPrintTokenInfo(t *testing.T) { user string pass string scope string - bodyReader allReader + bodyReader AllReader skipValidation bool expError bool expReqError bool diff --git a/internal/requests/requests.go b/internal/requests/requests.go index 089f263..c9c41dd 100644 --- a/internal/requests/requests.go +++ b/internal/requests/requests.go @@ -41,6 +41,7 @@ const ( emptyString = "" ) +// ErrMethodNotFound is returned when an unsupported HTTP method is specified. var ErrMethodNotFound = errors.New("HTTP method not found") var allowedHTTPMethods = map[string]string{ @@ -69,7 +70,9 @@ var contentTypeMatchingItems = []struct { } type ( - URI string + // URI represents a partial URL path (e.g. /index.html). + URI string + // ResponseHeader represents a formatted string of HTTP response headers. ResponseHeader string ) @@ -87,23 +90,40 @@ type RequestHeader struct { // RequestConfig defines the configuration for a single HTTP request set. type RequestConfig struct { - Name string `mapstructure:"name"` - ClientTimeout int `mapstructure:"clientTimeout"` - UserAgent string `mapstructure:"userAgent"` - TransportOverrideURL string `mapstructure:"transportOverrideUrl"` - EnableProxyProtocolV2 bool `mapstructure:"enableProxyProtocolV2"` - Insecure bool `mapstructure:"insecure"` - RequestDebug bool `mapstructure:"requestDebug"` - RequestHeaders []RequestHeader `mapstructure:"requestHeaders"` - RequestMethod string `mapstructure:"requestMethod"` - RequestBody string `mapstructure:"requestBody"` - ResponseDebug bool `mapstructure:"responseDebug"` - ResponseHeadersFilter []string `mapstructure:"responseHeadersFilter"` - ResponseBodyMatchRegexp string `mapstructure:"responseBodyMatchRegexp"` - PrintResponseBody bool `mapstructure:"printResponseBody"` - PrintResponseHeaders bool `mapstructure:"printResponseHeaders"` - PrintResponseCertificates bool `mapstructure:"printResponseCertificates"` - Hosts []Host `mapstructure:"hosts"` + // Name is a descriptive name for this request configuration. + Name string `mapstructure:"name"` + // ClientTimeout is the timeout in seconds for the HTTP client. + ClientTimeout int `mapstructure:"clientTimeout"` + // UserAgent is the custom User-Agent string to use for the request. + UserAgent string `mapstructure:"userAgent"` + // TransportOverrideURL is an optional address to connect to instead of the target host. + TransportOverrideURL string `mapstructure:"transportOverrideUrl"` + // EnableProxyProtocolV2 enables PROXY protocol v2 headers for the connection. + EnableProxyProtocolV2 bool `mapstructure:"enableProxyProtocolV2"` + // Insecure skips TLS certificate verification. + Insecure bool `mapstructure:"insecure"` + // RequestDebug enables dumping the outgoing HTTP request. + RequestDebug bool `mapstructure:"requestDebug"` + // RequestHeaders is a slice of custom HTTP headers to include in the request. + RequestHeaders []RequestHeader `mapstructure:"requestHeaders"` + // RequestMethod is the HTTP method to use (GET, POST, etc.). + RequestMethod string `mapstructure:"requestMethod"` + // RequestBody is the body of the HTTP request. + RequestBody string `mapstructure:"requestBody"` + // ResponseDebug enables dumping the incoming HTTP response. + ResponseDebug bool `mapstructure:"responseDebug"` + // ResponseHeadersFilter is a list of header keys to display in the output. + ResponseHeadersFilter []string `mapstructure:"responseHeadersFilter"` + // ResponseBodyMatchRegexp is a regular expression to match against the response body. + ResponseBodyMatchRegexp string `mapstructure:"responseBodyMatchRegexp"` + // PrintResponseBody indicates if the response body should be printed to the output. + PrintResponseBody bool `mapstructure:"printResponseBody"` + // PrintResponseHeaders indicates if the response headers should be printed to the output. + PrintResponseHeaders bool `mapstructure:"printResponseHeaders"` + // PrintResponseCertificates indicates if the response TLS certificates should be printed. + PrintResponseCertificates bool `mapstructure:"printResponseCertificates"` + // Hosts is a list of target hosts and their URIs. + Hosts []Host `mapstructure:"hosts"` } // RequestHTTPClient wraps an http.Client with additional configuration for requests. @@ -116,24 +136,32 @@ type RequestHTTPClient struct { // ResponseData holds the results and metadata of an executed HTTP request. type ResponseData struct { - Request RequestConfig - TransportAddress string - URL string - ResponseBody string + // Request is the original configuration for the executed request. + Request RequestConfig + // TransportAddress is the network address the request was sent to. + TransportAddress string + // URL is the full URL requested. + URL string + // ResponseBody is the content of the HTTP response. + ResponseBody string + // ResponseBodyRegexpMatched indicates if the response body matched the configured regexp. ResponseBodyRegexpMatched bool - Response *http.Response - Error error + // Response is the raw HTTP response object. + Response *http.Response + // Error is any error encountered during the request. + Error error } // RequestsMetaConfig holds the global configuration and the list of requests to execute. type RequestsMetaConfig struct { - // TODO: can we remove the following - // three lines and just embed an "options" - // struct to take care of metadata? - RequestDebug bool + // RequestDebug enables global request debugging. + RequestDebug bool + // RequestVerbose enables global verbose output. RequestVerbose bool - CACertsPool *x509.CertPool - Requests []RequestConfig `mapstructure:"requests"` + // CACertsPool is the certificate pool used for validating server certificates. + CACertsPool *x509.CertPool + // Requests is the list of request configurations to execute. + Requests []RequestConfig `mapstructure:"requests"` } // NewRequestsMetaConfig creates a new RequestsMetaConfig with the system's certificate pool. diff --git a/internal/requests/requests_handlers.go b/internal/requests/requests_handlers.go index a89c401..9a0dd9c 100644 --- a/internal/requests/requests_handlers.go +++ b/internal/requests/requests_handlers.go @@ -285,7 +285,6 @@ func (rd *ResponseData) ImportResponseBody() { } // PrintResponseData prints the collected response data (status, headers, body) if verbose mode is enabled. -// //nolint:revive func (rd ResponseData) PrintResponseData(isVerbose bool) { if !isVerbose { diff --git a/internal/style/style.go b/internal/style/style.go index ce659d1..485b89f 100644 --- a/internal/style/style.go +++ b/internal/style/style.go @@ -35,83 +35,109 @@ var ( Cmd = lipgloss.NewStyle().Foreground(catBase).Background(catBlue). Bold(true).PaddingLeft(1).PaddingRight(1) + // TitleKey is the style for main titles' keys. TitleKey = lipgloss.NewStyle(). Foreground(catBlue).Bold(true) + // Title is the style for main titles' values. Title = lipgloss.NewStyle(). Foreground(catLavander).Bold(true). PaddingLeft(1) + // Title2 is an alternative style for titles with a background color. Title2 = lipgloss.NewStyle(). Foreground(catBase).Background(catPeach).Bold(true). PaddingLeft(1).PaddingRight(1) + // ItemKey is the style for item keys with minimal padding. ItemKey = lipgloss.NewStyle(). Foreground(catBlue). PaddingLeft(1).Bold(true) + // ItemKeyP3 is the style for item keys with more left padding. ItemKeyP3 = lipgloss.NewStyle(). Foreground(catBlue). PaddingLeft(3).Bold(true) + // HeadKeyP3 is the style for header keys in tables with padding. HeadKeyP3 = lipgloss.NewStyle(). Foreground(catFlamingo). PaddingLeft(3) + // HeadValue is the style for header values in tables. HeadValue = lipgloss.NewStyle(). Foreground(catSapphire) + // CertKeyP3 is the style for certificate keys with padding. CertKeyP3 = lipgloss.NewStyle(). Foreground(catLavander). PaddingLeft(3) + // CertKeyP4 is the style for certificate keys with more padding. CertKeyP4 = lipgloss.NewStyle(). Foreground(catLavander). PaddingLeft(4) + // CertKeyP5 is the style for certificate keys with maximum padding. CertKeyP5 = lipgloss.NewStyle(). Foreground(catLavander). PaddingLeft(5) + // CertValue is the style for certificate values. CertValue = lipgloss.NewStyle(). Foreground(catPeach) + // CertValueNotice is the style for highlighted certificate values (e.g. CA status). CertValueNotice = lipgloss.NewStyle(). Foreground(catMauve) + // Via is the style for "via" transport information. Via = lipgloss.NewStyle(). Foreground(catMauve).Italic(true). PaddingLeft(1) + // URL is the style for URLs. URL = lipgloss.NewStyle(). Foreground(catFlamingo).Bold(true) + // Status is the default style for HTTP status codes. Status = lipgloss.NewStyle(). Foreground(catSapphire) + // Status2xx is the style for 2xx HTTP status codes. Status2xx = lipgloss.NewStyle(). Foreground(catGreen) + // Status3xx is the style for 3xx HTTP status codes. Status3xx = lipgloss.NewStyle(). Foreground(catMauve) + // Status4xx is the style for 4xx HTTP status codes. Status4xx = lipgloss.NewStyle(). Foreground(catYellow) + // Status5xx is the style for 5xx HTTP status codes. Status5xx = lipgloss.NewStyle(). Foreground(catRed) + // StatusError is the style for error status codes (e.g. 0). StatusError = lipgloss.NewStyle(). Foreground(catRed) + // Error is the style for error messages. Error = lipgloss.NewStyle(). Foreground(catPink).Italic(true) + // Headers is the style for response headers. Headers = lipgloss.NewStyle().Italic(true).PaddingLeft(4). Foreground(catTeal) - BoolTrue = lipgloss.NewStyle().Foreground(catTeal) + // BoolTrue is the style for boolean true values. + BoolTrue = lipgloss.NewStyle().Foreground(catTeal) + // BoolFalse is the style for boolean false values. BoolFalse = lipgloss.NewStyle().Foreground(catYellow) - Warn = lipgloss.NewStyle().Foreground(catYellow) - Crit = lipgloss.NewStyle().Foreground(lgRed) + // Warn is the style for warning messages or near-expiration dates. + Warn = lipgloss.NewStyle().Foreground(catYellow) + // Crit is the style for critical errors or expired certificates. + Crit = lipgloss.NewStyle().Foreground(lgRed) ) From 2c76e9979df7c3d708f04cc283143e4c3dcec19e Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Wed, 29 Apr 2026 23:00:22 +0200 Subject: [PATCH 2/2] fix: check for nil pointer in RequestToken function --- internal/jwtinfo/jwtinfo.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index 207ce93..88c4e97 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -55,8 +55,13 @@ type AllReader func(io.Reader) ([]byte, error) // RequestToken makes an HTTP POST request to the given URL with the provided values // to retrieve a JWT token. It handles both application/jwt and application/json // response types. +// //nolint:revive func RequestToken(ctx context.Context, reqURL string, reqValues map[string]string, client *http.Client, readAll AllReader) (JwtTokenData, error) { + if readAll == nil { + return JwtTokenData{}, errors.New("nil body reader function") + } + if reqURL == emptyString { return JwtTokenData{}, errors.New("empty string provided as request URL") }