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 {