Skip to content
Merged
4 changes: 2 additions & 2 deletions .github/workflows/codeChecks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ jobs:
run: go mod tidy

- name: Test with the Go CLI
run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./...
run: CGO_ENABLED=1 go test ./... -race

- name: Build
run: go build -v ./...
run: CGO_ENABLED=0 go build -v ./...

go_test_coverage_check:
needs: go_tests
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@

switch to Go 1.27.1

### Feat

Requests: pin Go 1.27 ML-KEM hybrid CurvePreferences (including P-521 fallback) and print the negotiated key exchange.

### Tests

Certinfo: GetRemoteCerts tests apply SetTLSInsecure before SetTLSEndpoint so endpoint certificate retrieval uses the intended TLS verification mode.

Requests: httptest TLS servers bind an ephemeral port so parallel cases do not collide on fixed listeners.

Cmd: re-bind viper flags after Reset so repeated test counts keep CLI flag bindings.

## 0.15.1 (2026-09-03)

### Feat
Expand Down
44 changes: 24 additions & 20 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,37 @@ func init() {
StringVar(&cfgFile, "config", "", "config file (default is $HOME/.https-wrench.yaml)")
rootCmd.PersistentFlags().Bool("version", false, "Display the version")

err := viper.BindPFlag("version", rootCmd.PersistentFlags().Lookup("version"))
if err != nil {
fmt.Printf("Error binding version flag: %v\n", err)
}

addCaBundleFlag(requestsCmd)
// addCertBundleFlag(requestsCmd)
// addKeyFileFlag(requestsCmd)

addCaBundleFlag(certinfoCmd)
addCertBundleFlag(certinfoCmd)
addKeyFileFlag(certinfoCmd)

bindViperFlags()
}

func bindViperFlags() {
if err := viper.BindPFlag("version", rootCmd.PersistentFlags().Lookup("version")); err != nil {
fmt.Printf("Error binding version flag: %v\n", err)
}

if err := viper.BindPFlag("ca-bundle", requestsCmd.Flags().Lookup("ca-bundle")); err != nil {
fmt.Printf("Error binding ca-bundle flag: %v\n", err)
}

if err := viper.BindPFlag("ca-bundle", certinfoCmd.Flags().Lookup("ca-bundle")); err != nil {
fmt.Printf("Error binding ca-bundle flag: %v\n", err)
}

if err := viper.BindPFlag("cert-bundle", certinfoCmd.Flags().Lookup("cert-bundle")); err != nil {
fmt.Printf("Error binding cert-bundle flag: %v\n", err)
}

if err := viper.BindPFlag("key-file", certinfoCmd.Flags().Lookup("key-file")); err != nil {
fmt.Printf("Error binding key-file flag: %v\n", err)
}
}

func initConfig() {
Expand Down Expand Up @@ -168,27 +187,12 @@ func isMCPCommand() bool {
func addCaBundleFlag(cmd *cobra.Command) {
cmd.Flags().StringVar(&caBundlePath, "ca-bundle", "", `Path to bundle file with CA certificates
to use for validation`)

err := viper.BindPFlag("ca-bundle", cmd.Flags().Lookup("ca-bundle"))
if err != nil {
fmt.Printf("Error binding ca-bundle flag: %v\n", err)
}
}

func addCertBundleFlag(cmd *cobra.Command) {
cmd.Flags().StringVar(&certBundlePath, "cert-bundle", "", "Path to PEM Certificate bundle file")

err := viper.BindPFlag("cert-bundle", cmd.Flags().Lookup("cert-bundle"))
if err != nil {
fmt.Printf("Error binding cert-bundle flag: %v\n", err)
}
}

func addKeyFileFlag(cmd *cobra.Command) {
cmd.Flags().StringVar(&keyFilePath, "key-file", "", "Path to PEM Key file")

err := viper.BindPFlag("key-file", cmd.Flags().Lookup("key-file"))
if err != nil {
fmt.Printf("Error binding key-file flag: %v\n", err)
}
}
39 changes: 26 additions & 13 deletions internal/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,28 @@ import (
"github.com/xenos76/https-wrench/internal/requests"
)

func resetPersistentFlag(name string) {
f := rootCmd.PersistentFlags().Lookup(name)
_ = f.Value.Set(f.DefValue)
f.Changed = false
}

func resetViper() {
resetPersistentFlag("version")
resetPersistentFlag("config")
viper.Reset()
bindViperFlags()
}

//nolint:revive
func TestRootCmd_LoadConfig(t *testing.T) {
t.Run("LoadConfig no config file", func(t *testing.T) {
oldCfg := cfgFile

t.Cleanup(func() {
cfgFile = oldCfg
resetViper()

viper.Reset()
cfgFile = oldCfg
})

var mc requests.RequestsMetaConfig
Expand All @@ -44,9 +57,9 @@ func TestRootCmd_LoadConfig(t *testing.T) {
oldCfg := cfgFile

t.Cleanup(func() {
cfgFile = oldCfg
resetViper()

viper.Reset()
cfgFile = oldCfg
})

var expectedCaCertsPool *x509.CertPool
Expand Down Expand Up @@ -79,9 +92,9 @@ func TestRootCmd_LoadConfig(t *testing.T) {
oldCfg := cfgFile

t.Cleanup(func() {
cfgFile = oldCfg
resetViper()

viper.Reset()
cfgFile = oldCfg
})

cfgFile = "../../assets/examples/https-wrench-k3s-anchor-and-aliases.yaml"
Expand Down Expand Up @@ -117,9 +130,9 @@ func TestRootCmd_LoadConfig(t *testing.T) {
oldCfg := cfgFile

t.Cleanup(func() {
cfgFile = oldCfg
resetViper()

viper.Reset()
cfgFile = oldCfg
})

// Make Unmarshal fail by setting a type mismatch
Expand Down Expand Up @@ -159,10 +172,10 @@ func TestRootCmd_Execute(t *testing.T) {
oldCfg := cfgFile

t.Cleanup(func() {
cfgFile = oldCfg

rootCmd.SetArgs(nil)
viper.Reset()
resetViper()

cfgFile = oldCfg
})

rootCmd.SetArgs([]string{"--config", "./embedded/config-example.yaml"})
Expand Down Expand Up @@ -228,9 +241,9 @@ func TestRootCmd(t *testing.T) {
oldCfg := cfgFile

t.Cleanup(func() {
cfgFile = oldCfg
resetViper()

viper.Reset()
cfgFile = oldCfg
})

buf := new(bytes.Buffer)
Expand Down
67 changes: 38 additions & 29 deletions internal/requests/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ type demoCertTemplate struct {

//nolint:revive
type demoHttpServerData struct {
serverAddr string
proxyprotoEnabled bool
serverName string
tlsCipherSuites []uint16
tlsMaxVersion uint16
listenHost string
proxyprotoEnabled bool
serverName string
tlsCipherSuites []uint16
tlsCurvePreferences []tls.CurveID
tlsMaxVersion uint16
}

var (
Expand Down Expand Up @@ -170,6 +171,16 @@ func printResponseBody(res *http.Response) {
fmt.Println(string(body))
}

func testServerHostPort(ts *httptest.Server) string {
return ts.Listener.Addr().String()
}

// NewHTTPSTestServer starts an httptest TLS server configured by data.
// Cipher suites default to the TLS 1.3 AEADs, CurvePreferences to the Go 1.27
// PQ hybrids plus classical fallbacks, and MaxVersion to TLS 1.3. Non-empty
// data.tlsCipherSuites, data.tlsCurvePreferences, or a non-zero data.tlsMaxVersion
// override those defaults. Optional data.listenHost and data.proxyprotoEnabled
// replace the listener. The caller must Close the returned server.
func NewHTTPSTestServer(data demoHttpServerData) (*httptest.Server, error) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "DemoHTTPSServer Handler - client output\n")
Expand All @@ -181,29 +192,21 @@ func NewHTTPSTestServer(data demoHttpServerData) (*httptest.Server, error) {
ts := httptest.NewUnstartedServer(handler)
ts.EnableHTTP2 = true

// fmt.Println("Inside NewDemoHTTPSServer()")

if data.serverAddr != emptyString && !data.proxyprotoEnabled {
listener, err := net.Listen("tcp", data.serverAddr)
if data.listenHost != emptyString {
ln, err := net.Listen("tcp", net.JoinHostPort(data.listenHost, "0"))
if err != nil {
fmt.Println("Error creating listener:", err)
return nil, fmt.Errorf("error creating listener: %w", err)
}

ts.Listener = listener
_ = ts.Listener.Close()
ts.Listener = ln
}

if data.serverAddr != emptyString && data.proxyprotoEnabled {
ln, err := net.Listen("tcp", data.serverAddr)
if err != nil {
panic(err)
}

proxyListener := &proxyproto.Listener{
Listener: ln,
if data.proxyprotoEnabled {
ts.Listener = &proxyproto.Listener{
Listener: ts.Listener,
ReadHeaderTimeout: 10 * time.Second,
}

ts.Listener = proxyListener
}

cert, err := tls.LoadX509KeyPair(
Expand All @@ -226,6 +229,12 @@ func NewHTTPSTestServer(data demoHttpServerData) (*httptest.Server, error) {
tlsCipherSuites = data.tlsCipherSuites
}

tlsCurvePreferences := defaultCurvePreferences

if len(data.tlsCurvePreferences) > 0 {
tlsCurvePreferences = data.tlsCurvePreferences
}

// Set default TLS MaxVersion to 1.3
var tlsMaxVersion uint16 = tls.VersionTLS13

Expand All @@ -234,9 +243,10 @@ func NewHTTPSTestServer(data demoHttpServerData) (*httptest.Server, error) {
}

ts.TLS = &tls.Config{
Certificates: []tls.Certificate{cert},
CipherSuites: tlsCipherSuites,
MaxVersion: tlsMaxVersion,
Certificates: []tls.Certificate{cert},
CipherSuites: tlsCipherSuites,
CurvePreferences: tlsCurvePreferences,
MaxVersion: tlsMaxVersion,
}

ts.StartTLS()
Expand Down Expand Up @@ -374,25 +384,24 @@ func TestMain(m *testing.M) {
func TestHTTPSTestServer(t *testing.T) {
tests := []struct {
testname string
serverAddr string
listenHost string
}{
{"localhostIPv4", "127.0.0.1:55667"},
{"localhostIPv4", "127.0.0.1"},
}

for _, tt := range tests {
testname := tt.testname
t.Run(testname, func(t *testing.T) {
t.Parallel()

httpSrvData := demoHttpServerData{serverAddr: tt.serverAddr}
// httpSrvData := demoHttpServerData{}
httpSrvData := demoHttpServerData{listenHost: tt.listenHost}

ts, err := NewHTTPSTestServer(httpSrvData)
if err != nil {
t.Fatal(err)
}

defer ts.Close()
t.Cleanup(ts.Close)

// fmt.Println("TestDemoHTTPSServer")
// fmt.Print("Client URL: ")
Expand Down
18 changes: 17 additions & 1 deletion internal/requests/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net/http"
"net/http/httputil"
"os"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -41,6 +42,18 @@ const (
emptyString = ""
)

// defaultCurvePreferences lists Go 1.27 TLS hybrids plus classical fallbacks.
// Explicit CurvePreferences keeps PQ on when GODEBUG=tlsmlkem=0 / tlssecpmlkem=0.
var defaultCurvePreferences = []tls.CurveID{
tls.X25519MLKEM768,
tls.SecP256r1MLKEM768,
tls.SecP384r1MLKEM1024,
tls.X25519,
tls.CurveP256,
tls.CurveP384,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
tls.CurveP521,
}

// ErrMethodNotFound is returned when an unsupported HTTP method is specified.
var ErrMethodNotFound = errors.New("HTTP method not found")

Expand Down Expand Up @@ -312,6 +325,7 @@ func (r *RequestConfig) printTLSInfo(w io.Writer, tlsState *tls.ConnectionState)
fmt.Fprintln(w, "TLS:")
fmt.Fprintf(w, "Version: %v\n", TLSVersionName(tlsState.Version))
fmt.Fprintf(w, "CipherSuite: %v\n", cipherSuiteName(tlsState.CipherSuite))
fmt.Fprintf(w, "Key Exchange: %v\n", tlsState.CurveID)

for i, cert := range tlsState.PeerCertificates {
fmt.Fprintf(w, "Certificate %d:\n", i)
Expand All @@ -330,7 +344,9 @@ func (r *RequestConfig) printTLSInfo(w io.Writer, tlsState *tls.ConnectionState)

// NewRequestHTTPClient creates a new RequestHTTPClient with default transport settings.
func NewRequestHTTPClient() *RequestHTTPClient {
tlsConfig := &tls.Config{}
tlsConfig := &tls.Config{
CurvePreferences: slices.Clone(defaultCurvePreferences),
}
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
Expand Down
4 changes: 4 additions & 0 deletions internal/requests/requests_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ func RenderTLSData(w io.Writer, r *http.Response, filter ...[]map[int][]string)
sl("CipherSuite"),
sv(cipherSuiteName(respTLS.CipherSuite)),
)
t.Row(
sl("Key Exchange"),
sv(respTLS.CurveID.String()),
)
fmt.Fprintln(w, t.Render())
t.ClearRows()

Expand Down
Loading
Loading