package engine import ( "fmt" "net" "net/http" "os" "testing" "time" ) // findFreePort asks the OS for an available TCP port on localhost. func findFreePort(t *testing.T) string { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("findFreePort: %v", err) } port := l.Addr().(*net.TCPAddr).Port l.Close() return fmt.Sprintf("127.0.0.1:%d", port) } // isPortListening reports whether a TCP connection can be made to addr within // the given timeout. A successful TCP connection means the listener is up, // even if the subsequent TLS handshake would fail. func isPortListening(addr string, timeout time.Duration) bool { conn, err := net.DialTimeout("tcp", addr, timeout) if err != nil { return false } conn.Close() return true } // TestCertMagicBypassesCertFilesWithCustomAddr verifies the fix for the bug // where --letsencrypt combined with --https-addr caused Algernon to try to // open cert.pem/key.pem (which don't exist) instead of using CertMagic. // // With the old code the TLS goroutine exited immediately with "open cert.pem: // no such file or directory" and the port never opened. // With the fix, ListenAndServeTLSConfig is used and the port is open. func TestCertMagicBypassesCertFilesWithCustomAddr(t *testing.T) { httpsAddr := findFreePort(t) ac := &Config{versionString: "test"} ac.serve.useCertMagic = true // Point at deliberately missing files — the fix must not try to open them. ac.serve.serverCert = "does-not-exist-cert.pem" ac.serve.serverKey = "does-not-exist-key.pem" ac.serve.portSettings = []PortSetting{ {Addr: httpsAddr, Protocol: "http2", TLS: true}, } done := make(chan bool, 1) ready := make(chan bool, 1) go func() { ac.servePortSettings(http.NewServeMux(), done, ready) }() select { case <-ready: case <-time.After(5 * time.Second): t.Fatal("server did not become ready within 5 s") } // Give the listener goroutine a moment to bind. time.Sleep(50 * time.Millisecond) if !isPortListening(httpsAddr, time.Second) { t.Errorf("HTTPS port %s is not listening — CertMagic TLS path was not taken (cert file fallback would have failed to open the cert)", httpsAddr) } done <- true } // TestHTTPSWithoutCertMagicRequiresCertFiles confirms that when CertMagic is // NOT enabled, the existing cert/key file path is still used (backwards // compatibility). With missing cert files and --domain, Algernon refuses to // start before any listener is spawned. func TestHTTPSWithoutCertMagicRequiresCertFiles(t *testing.T) { httpsAddr := findFreePort(t) ac := &Config{versionString: "test"} ac.serverAddDomain = true ac.serve.useCertMagic = false ac.serve.serverCert = "does-not-exist-cert.pem" ac.serve.serverKey = "does-not-exist-key.pem" ac.serve.httpsAddr = httpsAddr done := make(chan bool, 1) ready := make(chan bool, 1) // Serve should call fatalExit (which calls os.Exit). We cannot intercept // os.Exit in a unit test, so instead we verify that the port is never // bound by using the pre-serve TLS check directly. needsTLS := ac.serve.httpsAddr != "" if !needsTLS { t.Fatal("expected needsTLS to be true") } if _, err := os.Stat(ac.serve.serverCert); err == nil { t.Fatal("expected cert file to not exist") } // Confirm the port is not listening (nothing was started) if isPortListening(httpsAddr, 200*time.Millisecond) { t.Errorf("HTTPS port %s is listening but should not be — cert files are missing and CertMagic is disabled", httpsAddr) } _ = done _ = ready }