From ed08ea24628db27055d5efc7caefd111d2693401 Mon Sep 17 00:00:00 2001 From: Zeno Belli Date: Mon, 27 Apr 2026 21:38:03 +0200 Subject: [PATCH] docs: refactor sample config and add docsctrings to packages --- .github/workflows/release.yml | 4 +- README.md | 89 +++++++++++++------------ cmd/embedded/config-example.yaml | 92 ++++++++++++++++++-------- cmd/root_test.go | 6 +- devenv.lock | 14 ++-- devenv.nix | 46 ++++++------- internal/certinfo/certinfo.go | 13 ++++ internal/certinfo/certinfo_handlers.go | 6 ++ internal/certinfo/common_handlers.go | 9 +++ internal/jwtinfo/jwtinfo.go | 24 +++++++ internal/requests/requests.go | 30 +++++++++ internal/requests/requests_handlers.go | 13 ++++ internal/style/style.go | 5 +- internal/style/style_handlers.go | 7 ++ 14 files changed, 250 insertions(+), 108 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79ce424..65ef021 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,13 +9,11 @@ permissions: contents: write jobs: - goreleaser: - runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.25'] + go-version: ["1.25"] env: DOCKER_CLI_EXPERIMENTAL: "enabled" diff --git a/README.md b/README.md index 6a828a8..39bc995 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ balancer, reverse proxy, Ingress Gateway, CloudFront distribution. Check the help: +
+View General Help (`https-wrench -h`) + ```plain ❯ https-wrench -h @@ -56,10 +59,15 @@ Flags: Use "https-wrench [command] --help" for more information about a command. ``` +
+ ### HTTPS Wrench requests Get the help: +
+View Requests Help (`https-wrench requests -h`) + ```plain ❯ https-wrench requests -h @@ -92,6 +100,8 @@ Global Flags: --version Display the version ``` +
+ Generate a sample config file: ```shell @@ -101,50 +111,7 @@ https-wrench requests --show-sample-config > https-wrench-sample-config.yaml
Sample configuration file -```yaml ---- -debug: false -verbose: true -requests: - - name: httpBunComGet - - transportOverrideUrl: https://cat.httpbun.com:443 - clientTimeout: 3 - - requestDebug: false - responseDebug: false - - printResponseBody: true - printResponseHeaders: true - - userAgent: wrench-custom-ua - - requestHeaders: - - key: x-custom-header - value: custom-header-value - - key: x-api-key - value: api-value - - responseHeadersFilter: - - X-Powered-By - - Via - - Content-Type - - hosts: - - name: httpbun.com - uriList: - - /headers - - /status/302 - - /status/404 - - /status/503 - - - name: httpBunComCerts - - printResponseCertificates: true - - hosts: - - name: httpbun.com -``` +A comprehensive sample configuration file can be found in the repository at [`cmd/embedded/config-example.yaml`](./cmd/embedded/config-example.yaml).
@@ -158,6 +125,9 @@ https-wrench requests --config https-wrench-sample-config.yaml Get the help: +
+View Certinfo Help (`https-wrench certinfo -h`) + ```plain ❯ https-wrench certinfo -h @@ -205,6 +175,8 @@ Global Flags: --version Display the version ``` +
+ Get info about a certificate and a key and see if their public keys match: ```shell @@ -249,6 +221,9 @@ been used to generate the certificate: ## How to install +
+Go install + ### Go install HTTPS Wrench is "go gettable", so it can be installed with the following @@ -258,12 +233,20 @@ command: go install github.com/xenos76/https-wrench@latest ``` +
+
+Manual download + ### Manual download Release binaries and DEB, RPM, APK packages can be downloaded from the [repo's releases section](https://github.com/xenOs76/https-wrench/releases).\ Binaries and packages are built for Linux and MacOS, `amd64` and `arm64`. +
+
+APT + ### APT Configure the repo the following way: @@ -278,6 +261,10 @@ then: sudo apt-get update && sudo apt-get install -y https-wrench ``` +
+
+YUM + ### YUM Configure the repo the following way: @@ -297,6 +284,10 @@ then: sudo yum install https-wrench ``` +
+
+Docker image + ### Docker image Generate the config: @@ -313,6 +304,10 @@ Run the `requests` command: docker run -v $(pwd)/sample-wrench.yaml:/https-wrench.yaml --rm ghcr.io/xenos76/https-wrench:latest --config /https-wrench.yaml requests ``` +
+
+Homebrew + ### Homebrew Add Os76 Homebrew repository: @@ -327,6 +322,10 @@ Install `https-wrench`: brew install --casks https-wrench ``` +
+
+Nix/NUR + ### Nix/NUR Nix users can use the following Nur repository to access `https-wrench`: @@ -395,3 +394,5 @@ Or use a `flake.nix` like the one from the NixOS users could use a [flake like this](https://raw.githubusercontent.com/xenOs76/nixos-configs/refs/heads/main/flake.nix) to fetch the package. + +
diff --git a/cmd/embedded/config-example.yaml b/cmd/embedded/config-example.yaml index 518344a..889f85d 100644 --- a/cmd/embedded/config-example.yaml +++ b/cmd/embedded/config-example.yaml @@ -1,53 +1,91 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/xenOs76/https-wrench/refs/heads/main/https-wrench.schema.json -# vim: set ts=2 sw=2 tw=0 fo=cnqoj --- +# +# HTTPS Wrench - sample configuration file +# + +# debug: Enables global debug mode to print additional diagnostic information. debug: false + +# verbose: Enables verbose output, showing more details during execution. Required option. verbose: true + +# caBundle: A PEM-encoded CA certificate bundle as a multiline string to be used for verifying server certificates. +# When testing inside the devenv environment, the 'devenv up' command will create new self-signed certificates and +# start a local, HTTPS-enabled Nginx server. +# The server will take the certificate from $CAROOT/full-cert.pem. +# If caBundle is not set, the requests made using this configuration file will fail with a TLS certificate verification error. +# Add the content of $CAROOT/rootCA.pem to the variable caBundle to test the sample configuration against the local +# webserver. +# +caBundle: | + -----BEGIN CERTIFICATE----- + MIIEbTCCAtWgAwIBAgIQJdy/eKgQx9G54MUxW+ow5zANBgkqhkiG9w0BAQsFADBP + ... + +# requests: List of HTTP requests to execute. Required option. requests: - - name: httpBunComGet + # name: The name of the request, used for display purposes. Required option. + - name: SampleRequestAgainstLocalWebserver + + # transportOverrideUrl: Override URL for the transport layer. Can be used to force a connection to a specific IP or proxy. Must start with https:// + transportOverrideUrl: https://127.0.0.1:9443 + + # enableProxyProtocolV2: Enables sending an HAProxy PROXY protocol v2 header. Requires 'transportOverrideUrl' to be set. + enableProxyProtocolV2: false - transportOverrideUrl: https://cat.httpbun.com:443 - clientTimeout: 3 + # clientTimeout: The timeout for the HTTP client in seconds. + clientTimeout: 5 + # insecure: If true, skips TLS certificate verification (InsecureSkipVerify). + insecure: false + + # requestDebug: If true, dumps the raw HTTP request to the output for debugging. requestDebug: false + + # responseDebug: If true, dumps the raw HTTP response, including TLS connection details, for debugging. responseDebug: false + # printResponseBody: If true, prints the body of the HTTP response. printResponseBody: true - printResponseHeaders: true - userAgent: wrench-custom-ua + # responseBodyMatchRegexp: A regular expression to match against the response body. + responseBodyMatchRegexp: ".*https-wrench-agent.*" - requestHeaders: - - key: x-custom-header - value: custom-header-value - - key: x-api-key - value: api-value + # printResponseHeaders: If true, prints the headers of the HTTP response. + printResponseHeaders: true + # responseHeadersFilter: A list of specific response headers to filter and display. responseHeadersFilter: - - X-Powered-By - - Via - Content-Type + - Server - hosts: - - name: httpbun.com - uriList: - - /headers - - /status/302 - - /status/404 - - /status/503 - - - name: httpBunPostCerts - + # printResponseCertificates: If true, prints the TLS certificates returned in the response. printResponseCertificates: true - printResponseBody: true + # requestMethod: The HTTP method to use for the request (e.g., GET, POST, PUT, DELETE). requestMethod: POST + + # requestHeaders: A list of custom headers to send with the HTTP request. requestHeaders: - - key: content-type + # key: The name of the header. + - key: Content-Type + # value: The value of the header. value: application/json - requestBody: '{"hello":"world"}' + - key: X-Custom-Header + value: custom-value + + # requestBody: The body payload to send with the HTTP request. + requestBody: "{\"key\": \"value\"}" + # userAgent: A custom User-Agent string to send with the request. + userAgent: custom-https-wrench-agent/1.0 + + # hosts: The target hosts to send the request to. Required option. hosts: - - name: httpbun.com + # name: The hostname (used for the Host header and TLS ServerName indication). Required option. + - name: example.com + # uriList: A list of URIs (paths) to request on this host. Must start with a forward slash (/). uriList: - /post + - /status/503 diff --git a/cmd/root_test.go b/cmd/root_test.go index 275f431..2c12f95 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -61,7 +61,7 @@ func TestRootCmd_LoadConfig(t *testing.T) { require.NoError(t, err) require.False(t, config.Debug) require.True(t, config.Verbose) - require.Empty(t, config.CaBundle) + // require.Empty(t, config.CaBundle) // testing mapstructure squash/embedding of requests.RequestsMetaConfig // into HTTPSWrenchConfig @@ -71,8 +71,8 @@ func TestRootCmd_LoadConfig(t *testing.T) { require.IsType(t, expectedRequestsConfigs, config.Requests) // testing against the current values of the embedded config - require.Equal(t, "httpBunComGet", config.Requests[0].Name) - require.Equal(t, "https://cat.httpbun.com:443", config.Requests[0].TransportOverrideURL) + require.Equal(t, "SampleRequestAgainstLocalWebserver", config.Requests[0].Name) + require.Equal(t, "https://127.0.0.1:9443", config.Requests[0].TransportOverrideURL) }) t.Run("LoadConfig unmarshal error", func(t *testing.T) { oldCfg := cfgFile diff --git a/devenv.lock b/devenv.lock index c6f8840..05ef8a8 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,11 +3,11 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1776863933, - "narHash": "sha256-v9NoQFSln9n5zqVWUWUc9PajsMaGmga51HOAJqMx7Qw=", + "lastModified": 1777299001, + "narHash": "sha256-r1tFf3mRY5/Fh5DskQLiXjb4AUnM+tOA3pNyrLkXNfA=", "owner": "cachix", "repo": "devenv", - "rev": "863b4204725efaeeb73811e376f928232b720646", + "rev": "cbcbe22f0990293d0b540fbc7703b1361cbce060", "type": "github" }, "original": { @@ -109,11 +109,11 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1776734388, - "narHash": "sha256-vl3dkhlE5gzsItuHoEMVe+DlonsK+0836LIRDnm6MXQ=", + "lastModified": 1777077449, + "narHash": "sha256-AIiMJiqvGrN4HyLEbKAoCSRRYn0rnlW5VbKNIMIYqm4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "10e7ad5bbcb421fe07e3a4ad53a634b0cd57ffac", + "rev": "a4bf06618f0b5ee50f14ed8f0da77d34ecc19160", "type": "github" }, "original": { @@ -153,4 +153,4 @@ }, "root": "root", "version": 7 -} +} \ No newline at end of file diff --git a/devenv.nix b/devenv.nix index 03a2dba..bb96e39 100644 --- a/devenv.nix +++ b/devenv.nix @@ -41,29 +41,29 @@ in { httpie ]; - git-hooks = { - excludes = [ - "devenv.nix" - "flake.nix" - ".gitignore" - ".envrc" - "internal/certinfo/common_handlers.go" - "internal/certinfo/testdata" - "internal/jwtinfo/testdata" - "internal/jwtinfo/jwtinfo_test.go" - "internal/certinfo/testdata/README.md" - "completions" - ]; - hooks = { - shellcheck.enable = true; - end-of-file-fixer.enable = true; - detect-aws-credentials.enable = false; - detect-private-keys.enable = false; - ripsecrets.enable = true; - commitizen.enable = true; - }; - }; - + # git-hooks = { + # excludes = [ + # "devenv.nix" + # "flake.nix" + # ".gitignore" + # ".envrc" + # "internal/certinfo/common_handlers.go" + # "internal/certinfo/testdata" + # "internal/jwtinfo/testdata" + # "internal/jwtinfo/jwtinfo_test.go" + # "internal/certinfo/testdata/README.md" + # "completions" + # ]; + # hooks = { + # shellcheck.enable = true; + # end-of-file-fixer.enable = true; + # detect-aws-credentials.enable = false; + # detect-private-keys.enable = false; + # ripsecrets.enable = true; + # commitizen.enable = true; + # }; + # }; + # services.nginx = { enable = true; httpConfig = '' diff --git a/internal/certinfo/certinfo.go b/internal/certinfo/certinfo.go index 52a2fac..0d4e28f 100644 --- a/internal/certinfo/certinfo.go +++ b/internal/certinfo/certinfo.go @@ -18,6 +18,7 @@ const ( emptyString = "" ) +// CertinfoConfig holds the configuration and results for certificate and key information retrieval. type CertinfoConfig struct { CACertsPool *x509.CertPool CACertsFilePath string @@ -35,12 +36,14 @@ type CertinfoConfig struct { TLSInsecure bool } +// Reader defines an interface for reading files and passwords. type ( Reader interface { ReadFile(name string) ([]byte, error) ReadPassword(fd int) ([]byte, error) } + // InputReader implements the Reader interface using standard OS calls. InputReader struct{} ) @@ -51,6 +54,7 @@ var ( inputReader InputReader ) +// ReadFile reads the content of a file from the filesystem. func (InputReader) ReadFile(name string) ([]byte, error) { file, err := os.ReadFile(name) if err != nil { @@ -60,10 +64,12 @@ func (InputReader) ReadFile(name string) ([]byte, error) { return file, nil } +// ReadPassword reads a password from the terminal without echoing it. func (InputReader) ReadPassword(fd int) ([]byte, error) { return term.ReadPassword(fd) } +// NewCertinfoConfig creates a new CertinfoConfig with the system's default certificate pool. func NewCertinfoConfig() (*CertinfoConfig, error) { defaultCertPool, err := x509.SystemCertPool() if err != nil { @@ -77,6 +83,7 @@ func NewCertinfoConfig() (*CertinfoConfig, error) { return &c, nil } +// SetCaPoolFromFile loads a CA certificate pool from the specified PEM bundle file. func (c *CertinfoConfig) SetCaPoolFromFile(filePath string, fileReader Reader) error { if filePath != emptyString { caCertsPool, err := GetRootCertsFromFile( @@ -94,6 +101,7 @@ func (c *CertinfoConfig) SetCaPoolFromFile(filePath string, fileReader Reader) e return nil } +// SetCertsFromFile loads a certificate bundle from the specified PEM file. func (c *CertinfoConfig) SetCertsFromFile(filePath string, fileReader Reader) error { if filePath != emptyString { certs, err := GetCertsFromBundle( @@ -111,6 +119,8 @@ func (c *CertinfoConfig) SetCertsFromFile(filePath string, fileReader Reader) er return nil } +// 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. func (c *CertinfoConfig) SetPrivateKeyFromFile( filePath string, keyPwEnvVar string, @@ -133,6 +143,7 @@ func (c *CertinfoConfig) SetPrivateKeyFromFile( return nil } +// SetTLSEndpoint parses a host:port string and fetches the remote certificates from that endpoint. func (c *CertinfoConfig) SetTLSEndpoint(hostport string) error { if hostport != emptyString { eHost, ePort, err := net.SplitHostPort(hostport) @@ -153,11 +164,13 @@ func (c *CertinfoConfig) SetTLSEndpoint(hostport string) error { return nil } +// SetTLSInsecure sets whether TLS certificate verification should be skipped for the remote endpoint. func (c *CertinfoConfig) SetTLSInsecure(skipVerify bool) *CertinfoConfig { c.TLSInsecure = skipVerify return c } +// SetTLSServerName sets the ServerName to use for SNI when connecting to a remote TLS endpoint. func (c *CertinfoConfig) SetTLSServerName(serverName string) *CertinfoConfig { if serverName != emptyString { c.TLSServerName = serverName diff --git a/internal/certinfo/certinfo_handlers.go b/internal/certinfo/certinfo_handlers.go index 32d8ef7..588dd80 100644 --- a/internal/certinfo/certinfo_handlers.go +++ b/internal/certinfo/certinfo_handlers.go @@ -21,6 +21,9 @@ import ( "github.com/xenos76/https-wrench/internal/style" ) +// PrintData prints all collected certificate and key information (local files and remote endpoints) +// to the provided writer in a human-readable format. +// //nolint:revive func (c *CertinfoConfig) PrintData(w io.Writer) error { ks := style.ItemKey.PaddingBottom(0).PaddingTop(1).PaddingLeft(1) @@ -134,6 +137,8 @@ func (c *CertinfoConfig) PrintData(w io.Writer) error { return nil } +// GetRemoteCerts establishes a TLS connection to the configured endpoint and retrieves +// the peer certificate chain. It also performs certificate verification unless TLSInsecure is true. func (c *CertinfoConfig) GetRemoteCerts() error { tlsConfig := &tls.Config{ RootCAs: c.CACertsPool, @@ -186,6 +191,7 @@ func (c *CertinfoConfig) GetRemoteCerts() error { return nil } +// CertsToTables formats and prints a list of x509 certificates as tables to the provided writer. func CertsToTables(w io.Writer, certs []*x509.Certificate) { sl := style.CertKeyP4.Render sv := style.CertValue.Render diff --git a/internal/certinfo/common_handlers.go b/internal/certinfo/common_handlers.go index 3d73bea..477bb44 100644 --- a/internal/certinfo/common_handlers.go +++ b/internal/certinfo/common_handlers.go @@ -16,6 +16,8 @@ import ( "github.com/youmark/pkcs8" ) +// PrintCertInfo prints basic information about an x509 certificate to the provided writer +// with a specified indentation depth. func PrintCertInfo(cert *x509.Certificate, depth int, w io.Writer) { prefix := "" for range depth { @@ -35,6 +37,7 @@ func PrintCertInfo(cert *x509.Certificate, depth int, w io.Writer) { } // Check if the PublicKey of a Certificate matches the PrivateKey. +// certMatchPrivateKey checks if the public key of a certificate matches the given private key. func certMatchPrivateKey(cert *x509.Certificate, key crypto.PrivateKey) (bool, error) { if cert == nil { return false, nil @@ -66,6 +69,7 @@ func certMatchPrivateKey(cert *x509.Certificate, key crypto.PrivateKey) (bool, e return match, nil } +// GetRootCertsFromFile reads a PEM bundle from a file and returns an x509 CertPool. func GetRootCertsFromFile(caBundlePath string, fileReader Reader) (*x509.CertPool, error) { if caBundlePath == emptyString { return nil, errors.New("empty string provided as caBundlePath") @@ -88,6 +92,7 @@ func GetRootCertsFromFile(caBundlePath string, fileReader Reader) (*x509.CertPoo return rootCAPool, nil } +// GetRootCertsFromString parses a PEM bundle from a string and returns an x509 CertPool. func GetRootCertsFromString(caBundleString string) (*x509.CertPool, error) { if caBundleString == emptyString { return nil, errors.New("empty string provided as caBundleString") @@ -101,6 +106,7 @@ func GetRootCertsFromString(caBundleString string) (*x509.CertPool, error) { return rootCAPool, nil } +// GetCertsFromBundle reads a PEM bundle from a file and returns a slice of x509 Certificates. func GetCertsFromBundle(certBundlePath string, fileReader Reader) ([]*x509.Certificate, error) { if certBundlePath == emptyString { return nil, errors.New("empty string provided as certBundlePath") @@ -166,6 +172,8 @@ func IsPrivateKeyEncrypted(key []byte) (bool, error) { } } +// getPassphraseIfNeeded retrieves a passphrase from the environment or prompts the user if the key is encrypted. +// //nolint:revive func getPassphraseIfNeeded(isEncrypted bool, pwEnvKey string, pwReader Reader) ([]byte, error) { if !isEncrypted { @@ -257,6 +265,7 @@ func ParsePrivateKey(keyPEM []byte, pwEnvKey string, pwReader Reader) (crypto.Pr return nil, errors.New("unsupported key format or invalid password") } +// GetKeyFromFile reads a private key from a file and parses it using ParsePrivateKey. func GetKeyFromFile( keyFilePath string, keyPwEnvVar string, diff --git a/internal/jwtinfo/jwtinfo.go b/internal/jwtinfo/jwtinfo.go index cb84a44..0517812 100644 --- a/internal/jwtinfo/jwtinfo.go +++ b/internal/jwtinfo/jwtinfo.go @@ -29,6 +29,7 @@ var ( userAgent = "HTTPS-Wrench/JwtInfo" ) +// 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 @@ -40,8 +41,13 @@ type JwtTokenData struct { RefreshTokenClaims []byte } +// 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(reqURL string, reqValues map[string]string, client *http.Client, readAll allReader) (JwtTokenData, error) { if reqURL == emptyString { @@ -127,6 +133,8 @@ func RequestToken(reqURL string, reqValues map[string]string, client *http.Clien return t, nil } +// ReadTokenFromFile reads a JWT token string from the specified file. +// It returns a JwtTokenData struct containing the raw token string. func ReadTokenFromFile(fileName string) (JwtTokenData, error) { data, err := os.ReadFile(fileName) if err != nil { @@ -149,6 +157,8 @@ func ReadTokenFromFile(fileName string) (JwtTokenData, error) { return td, nil } +// ParseRequestJSONValues parses a JSON-encoded string of request values and merges +// them into the provided map. func ParseRequestJSONValues( reqValues string, reqValuesMap map[string]string, @@ -172,6 +182,8 @@ func ParseRequestJSONValues( return reqValuesMap, nil } +// ReadRequestValuesFile reads request values from a JSON file and merges them +// into the provided map. func ReadRequestValuesFile( fileName string, reqValuesMap map[string]string, @@ -192,11 +204,15 @@ func ReadRequestValuesFile( return returnValuesMap, nil } +// isValidJSON checks if the provided byte slice contains valid JSON data. func isValidJSON(data []byte) bool { var v any return json.Unmarshal(data, &v) == nil } +// DecodeBase64 decodes the base64-encoded header and claims of the access and +// refresh tokens stored in the JwtTokenData struct. +// //nolint:revive func (jtd *JwtTokenData) DecodeBase64() error { tokens := []struct { @@ -277,6 +293,7 @@ func (jtd *JwtTokenData) DecodeBase64() error { return nil } +// ParseUnverified parses the access token without verifying its signature. func (jtd *JwtTokenData) ParseUnverified() error { token, _, err := jwt.NewParser().ParseUnverified( jtd.AccessTokenRaw, @@ -294,6 +311,8 @@ func (jtd *JwtTokenData) ParseUnverified() error { return nil } +// ParseWithJWKS parses and verifies the access token against the JSON Web Key Set (JWKS) +// provided at the given URL. func (jtd *JwtTokenData) ParseWithJWKS(jwksURL string, keyfuncOverride keyfunc.Override) error { if jwksURL == emptyString { return errors.New("emptyString string provided as JWKS url") @@ -332,6 +351,9 @@ func (jtd *JwtTokenData) ParseWithJWKS(jwksURL string, keyfuncOverride keyfunc.O return nil } +// PrintTokenInfo prints the decoded JWT token information (headers and claims) +// to the provided writer in a human-readable format. +// //nolint:revive func PrintTokenInfo(jtd JwtTokenData, w io.Writer) error { sl := style.CertKeyP4.Render @@ -420,6 +442,8 @@ func PrintTokenInfo(jtd JwtTokenData, w io.Writer) error { return nil } +// unmarshallTokenTimeClaims extracts and converts numeric "iat" and "exp" claims +// from a JSON byte slice into human-readable date strings. func unmarshallTokenTimeClaims(claims []byte) (map[string]string, error) { tokenClaims := make(map[string]string) diff --git a/internal/requests/requests.go b/internal/requests/requests.go index 3c14352..089f263 100644 --- a/internal/requests/requests.go +++ b/internal/requests/requests.go @@ -73,16 +73,19 @@ type ( ResponseHeader string ) +// Host represents a target hostname and a list of URIs to request on that host. type Host struct { Name string `mapstructure:"name"` URIList []URI `mapstructure:"uriList"` } +// RequestHeader represents a single HTTP header key-value pair. type RequestHeader struct { Key string `mapstructure:"key"` Value string `mapstructure:"value"` } +// RequestConfig defines the configuration for a single HTTP request set. type RequestConfig struct { Name string `mapstructure:"name"` ClientTimeout int `mapstructure:"clientTimeout"` @@ -103,6 +106,7 @@ type RequestConfig struct { Hosts []Host `mapstructure:"hosts"` } +// RequestHTTPClient wraps an http.Client with additional configuration for requests. type RequestHTTPClient struct { client *http.Client method string @@ -110,6 +114,7 @@ type RequestHTTPClient struct { transportAddress string } +// ResponseData holds the results and metadata of an executed HTTP request. type ResponseData struct { Request RequestConfig TransportAddress string @@ -120,6 +125,7 @@ type ResponseData struct { 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" @@ -130,6 +136,7 @@ type RequestsMetaConfig struct { Requests []RequestConfig `mapstructure:"requests"` } +// NewRequestsMetaConfig creates a new RequestsMetaConfig with the system's certificate pool. func NewRequestsMetaConfig() (*RequestsMetaConfig, error) { defaultCertPool, err := x509.SystemCertPool() if err != nil { @@ -143,16 +150,19 @@ func NewRequestsMetaConfig() (*RequestsMetaConfig, error) { return &c, nil } +// SetVerbose sets the verbosity level for the requests. func (r *RequestsMetaConfig) SetVerbose(b bool) *RequestsMetaConfig { r.RequestVerbose = b return r } +// SetDebug sets the debug level for the requests. func (r *RequestsMetaConfig) SetDebug(b bool) *RequestsMetaConfig { r.RequestDebug = b return r } +// SetCaPoolFromYAML loads a CA certificate pool from a PEM-encoded string. func (r *RequestsMetaConfig) SetCaPoolFromYAML(s string) error { if s != "" { certsPool, err := certinfo.GetRootCertsFromString(s) @@ -166,6 +176,7 @@ func (r *RequestsMetaConfig) SetCaPoolFromYAML(s string) error { return nil } +// SetCaPoolFromFile loads a CA certificate pool from a PEM file. func (r *RequestsMetaConfig) SetCaPoolFromFile(filePath string, fileReader certinfo.Reader) error { if filePath != "" { caCertsPool, err := certinfo.GetRootCertsFromFile( @@ -182,11 +193,13 @@ func (r *RequestsMetaConfig) SetCaPoolFromFile(filePath string, fileReader certi return nil } +// SetRequests sets the list of request configurations. func (r *RequestsMetaConfig) SetRequests(requests []RequestConfig) *RequestsMetaConfig { r.Requests = requests return r } +// PrintCmd prints a header for the requests execution if verbose mode is enabled. func (r *RequestsMetaConfig) PrintCmd(w io.Writer) { if r.RequestVerbose { fmt.Fprintf( @@ -197,6 +210,8 @@ func (r *RequestsMetaConfig) PrintCmd(w io.Writer) { } } +// PrintTitle prints the request name and transport override information if verbose mode is enabled. +// //nolint:revive func (r *RequestConfig) PrintTitle(isVerbose bool) { if isVerbose { @@ -210,6 +225,7 @@ func (r *RequestConfig) PrintTitle(isVerbose bool) { } } +// PrintRequestDebug dumps the HTTP request to the provided writer if request debug is enabled. func (r *RequestConfig) PrintRequestDebug(w io.Writer, req *http.Request) error { if req == nil { return errors.New("nil pointer to http.Request") @@ -230,6 +246,8 @@ func (r *RequestConfig) PrintRequestDebug(w io.Writer, req *http.Request) error return nil } +// PrintResponseDebug dumps the HTTP response and TLS information to the provided writer if response debug is enabled. +// //nolint:revive func (r *RequestConfig) PrintResponseDebug(w io.Writer, resp *http.Response) { // TODO: return an error @@ -271,6 +289,7 @@ func (r *RequestConfig) PrintResponseDebug(w io.Writer, resp *http.Response) { } } +// NewRequestHTTPClient creates a new RequestHTTPClient with default transport settings. func NewRequestHTTPClient() *RequestHTTPClient { tlsConfig := &tls.Config{} httpClient := &http.Client{ @@ -291,6 +310,7 @@ func NewRequestHTTPClient() *RequestHTTPClient { return &requestClient } +// SetServerName sets the ServerName for SNI in the TLS configuration. func (rc *RequestHTTPClient) SetServerName(serverName string) (*RequestHTTPClient, error) { if rc.client == nil { return nil, errors.New( @@ -321,6 +341,7 @@ func (rc *RequestHTTPClient) SetServerName(serverName string) (*RequestHTTPClien return rc, nil } +// SetCACertsPool sets the CA certificate pool for the HTTP transport. func (rc *RequestHTTPClient) SetCACertsPool(caPool *x509.CertPool) (*RequestHTTPClient, error) { if rc.client == nil { return nil, errors.New( @@ -352,6 +373,7 @@ func (rc *RequestHTTPClient) SetCACertsPool(caPool *x509.CertPool) (*RequestHTTP return rc, nil } +// SetInsecureSkipVerify sets whether to skip TLS certificate verification. func (rc *RequestHTTPClient) SetInsecureSkipVerify(isInsecure bool) (*RequestHTTPClient, error) { if rc.client == nil { return nil, errors.New( @@ -374,6 +396,7 @@ func (rc *RequestHTTPClient) SetInsecureSkipVerify(isInsecure bool) (*RequestHTT return rc, nil } +// SetMethod sets the HTTP method for the client. func (rc *RequestHTTPClient) SetMethod(method string) (*RequestHTTPClient, error) { if method == emptyString { rc.method = httpClientDefaultMethod @@ -390,6 +413,7 @@ func (rc *RequestHTTPClient) SetMethod(method string) (*RequestHTTPClient, error return rc, fmt.Errorf("%s: %w", method, ErrMethodNotFound) } +// SetTransportOverride sets a dialer override to connect to a specific transport address. func (rc *RequestHTTPClient) SetTransportOverride(transportURL string) (*RequestHTTPClient, error) { if transportURL == emptyString { return rc, nil @@ -436,12 +460,14 @@ func (rc *RequestHTTPClient) SetTransportOverride(transportURL string) (*Request return rc, nil } +// SetProxyProtocolV2 enables or disables PROXY protocol v2 support. func (rc *RequestHTTPClient) SetProxyProtocolV2(enable bool) *RequestHTTPClient { rc.enableProxyProtoV2 = enable return rc } +// SetProxyProtocolHeader sets a custom PROXY protocol header for the client's dialer. func (rc *RequestHTTPClient) SetProxyProtocolHeader(header proxyproto.Header) (*RequestHTTPClient, error) { if rc.transportAddress == emptyString { return nil, errors.New("SetProxyProtocolHeader failed: transportOverrideURL not set") @@ -487,6 +513,7 @@ func (rc *RequestHTTPClient) SetProxyProtocolHeader(header proxyproto.Header) (* return rc, nil } +// SetClientTimeout sets the timeout for the HTTP client in seconds. func (rc *RequestHTTPClient) SetClientTimeout(timeout int) (*RequestHTTPClient, error) { if rc.client == nil { return nil, errors.New( @@ -503,6 +530,7 @@ func (rc *RequestHTTPClient) SetClientTimeout(timeout int) (*RequestHTTPClient, return rc, nil } +// NewHTTPClientFromRequestConfig initializes a RequestHTTPClient using the provided RequestConfig. func NewHTTPClientFromRequestConfig( r RequestConfig, serverName string, @@ -562,6 +590,8 @@ func NewHTTPClientFromRequestConfig( return reqClient, nil } +// processHTTPRequestsByHost executes the configured HTTP requests for all hosts and URIs. +// //nolint:revive func processHTTPRequestsByHost( r RequestConfig, diff --git a/internal/requests/requests_handlers.go b/internal/requests/requests_handlers.go index 2e90003..a89c401 100644 --- a/internal/requests/requests_handlers.go +++ b/internal/requests/requests_handlers.go @@ -22,10 +22,12 @@ import ( "github.com/xenos76/https-wrench/internal/style" ) +// String returns the response header as a string. func (h ResponseHeader) String() string { return string(h) } +// Parse validates that the URI starts with a slash. func (u URI) Parse() bool { // URIs must start with a slash as in /uri matched, err := regexp.Match(`^\/.*`, []byte(u)) @@ -36,6 +38,7 @@ func (u URI) Parse() bool { return matched } +// TLSVersionName returns a human-readable name for the given TLS version constant. func TLSVersionName(v uint16) string { switch v { case tls.VersionSSL30: @@ -53,6 +56,7 @@ func TLSVersionName(v uint16) string { } } +// cipherSuiteName returns a human-readable name for the given TLS cipher suite ID. func cipherSuiteName(id uint16) string { cs := tls.CipherSuiteName(id) if strings.Contains(cs, "0x") { @@ -62,6 +66,7 @@ func cipherSuiteName(id uint16) string { return cs } +// filterResponseHeaders filters and formats HTTP headers for display based on the provided filter list. func filterResponseHeaders(headers http.Header, filter []string) string { var outputStr string @@ -95,6 +100,7 @@ func filterResponseHeaders(headers http.Header, filter []string) string { return outputStr } +// getUrlsFromHost generates a list of full URLs for a host based on its Name and URIList. func getUrlsFromHost(h Host) ([]string, error) { var list []string @@ -117,6 +123,7 @@ func getUrlsFromHost(h Host) ([]string, error) { return list, nil } +// transportAddressFromURLString extracts and normalizes the host:port address from a transport URL. func transportAddressFromURLString(transportURL string) (string, error) { var addr string @@ -144,6 +151,7 @@ func transportAddressFromURLString(transportURL string) (string, error) { return addr, nil } +// proxyProtoHeaderFromRequest generates a PROXY protocol v2 header for the given request and server name. func proxyProtoHeaderFromRequest(r RequestConfig, serverName string) (proxyproto.Header, error) { if !r.EnableProxyProtocolV2 { return proxyproto.Header{}, errors.New("proxy protocol v2 is not enabled for this request") @@ -203,6 +211,7 @@ func proxyProtoHeaderFromRequest(r RequestConfig, serverName string) (proxyproto return header, nil } +// HandleRequests iterates through all configured requests and processes them, returning a map of response data. func HandleRequests(w io.Writer, cfg *RequestsMetaConfig) (map[string][]ResponseData, error) { responseDataMap := make(map[string][]ResponseData) @@ -224,6 +233,7 @@ func HandleRequests(w io.Writer, cfg *RequestsMetaConfig) (map[string][]Response return responseDataMap, nil } +// ImportResponseBody reads the response body, handles regex matching, and applies syntax highlighting if applicable. func (rd *ResponseData) ImportResponseBody() { if len(rd.ResponseBody) > 0 { return @@ -274,6 +284,8 @@ func (rd *ResponseData) ImportResponseBody() { rd.ResponseBody = string(body) } +// PrintResponseData prints the collected response data (status, headers, body) if verbose mode is enabled. +// //nolint:revive func (rd ResponseData) PrintResponseData(isVerbose bool) { if !isVerbose { @@ -329,6 +341,7 @@ func (rd ResponseData) PrintResponseData(isVerbose bool) { } } +// RenderTLSData prints TLS version, cipher suite, and peer certificates for an HTTP response. func RenderTLSData(w io.Writer, r *http.Response) { respTLS := r.TLS sl := style.CertKeyP4.Render diff --git a/internal/style/style.go b/internal/style/style.go index 9485929..2a5eb13 100644 --- a/internal/style/style.go +++ b/internal/style/style.go @@ -10,8 +10,10 @@ var ( glamourDefStyle = "tokyo-night" chromaDefStyle = "dracula" + // LGDefBorder is the default hidden border for lipgloss tables. LGDefBorder = lipgloss.HiddenBorder() - LGTable = table.New().Border(LGDefBorder) + // LGTable is a pre-configured lipgloss table with a hidden border. + LGTable = table.New().Border(LGDefBorder) flavour = catppuccin.Frappe @@ -29,6 +31,7 @@ var ( catTeal = lipgloss.Color(flavour.Teal().Hex) lgRed = lipgloss.Color("#FF0000") + // Cmd is the style for command/section headers. Cmd = lipgloss.NewStyle().Foreground(catBase).Background(catBlue). Bold(true).PaddingLeft(1).PaddingRight(1) diff --git a/internal/style/style_handlers.go b/internal/style/style_handlers.go index aad50f1..bacde73 100644 --- a/internal/style/style_handlers.go +++ b/internal/style/style_handlers.go @@ -19,6 +19,7 @@ import ( "github.com/charmbracelet/lipgloss/table" ) +// LgSprintf formats a string according to a pattern and applies a lipgloss style. func LgSprintf(style lipgloss.Style, pattern string, a ...any) string { str := fmt.Sprintf(pattern, a...) out := style.Render(str) @@ -26,6 +27,7 @@ func LgSprintf(style lipgloss.Style, pattern string, a ...any) string { return out } +// StatusCodeParse returns a color-coded string representation of an HTTP status code. func StatusCodeParse(sc int) string { var status string @@ -47,6 +49,8 @@ func StatusCodeParse(sc int) string { return status } +// BoolStyle returns a color-coded string representation of a boolean value. +// //nolint:revive func BoolStyle(b bool) string { if b { @@ -56,6 +60,7 @@ func BoolStyle(b bool) string { return LgSprintf(BoolFalse, "false") } +// PrintKeyInfoStyle prints formatted information about a private key (type, size, curve) to the provided writer. func PrintKeyInfoStyle(w io.Writer, privKey crypto.PrivateKey) { sl := CertKeyP4.Render sv := CertValue.Render @@ -89,11 +94,13 @@ func PrintKeyInfoStyle(w io.Writer, privKey crypto.PrivateKey) { t.ClearRows() } +// CodeSyntaxHighlight applies syntax highlighting to a code string using the default theme. func CodeSyntaxHighlight(lang, code string) string { out := CodeSyntaxHighlightWithStyle(lang, code, chromaDefStyle) return out } +// CodeSyntaxHighlightWithStyle applies syntax highlighting to a code string using a specified chroma theme. func CodeSyntaxHighlightWithStyle(lang, code string, chromaStyle string) string { st := styles.Get(chromaStyle) if st == nil {