1
0
mirror of https://github.com/umputun/reproxy.git synced 2025-11-23 22:04:57 +02:00

fix: use alternative port for ACME HTTP challenge in CI environments

- Configure CertMagic to use RedirHTTPPort as AltHTTPPort for HTTP challenges
- Improve HTTP-to-HTTPS redirect handler to omit standard HTTPS port in URL
- Update tests to use random high-numbered ports for HTTP challenges
- Fix various linting issues in the code
This commit is contained in:
Umputun
2025-04-22 03:49:39 -05:00
parent 231d7b002b
commit 75191307b1
5 changed files with 62 additions and 17 deletions

View File

@@ -60,7 +60,7 @@ var opts struct {
Route53 struct { Route53 struct {
Region string `long:"region" env:"REGION" description:"AWS region"` Region string `long:"region" env:"REGION" description:"AWS region"`
Profile string `long:"profile" env:"PROFILE" description:"AWS profile"` Profile string `long:"profile" env:"PROFILE" description:"AWS profile"`
AccessKeyId string `long:"access-key-id" env:"ACCESS_KEY_ID" description:"AWS access key id"` AccessKeyID string `long:"access-key-id" env:"ACCESS_KEY_ID" description:"AWS access key id"`
SecretAccessKey string `long:"secret-access-key" env:"SECRET_ACCESS_KEY" description:"AWS secret access key"` SecretAccessKey string `long:"secret-access-key" env:"SECRET_ACCESS_KEY" description:"AWS secret access key"`
SessionToken string `long:"session-token" env:"SESSION_TOKEN" description:"AWS session token"` SessionToken string `long:"session-token" env:"SESSION_TOKEN" description:"AWS session token"`
HostedZoneID string `long:"hosted-zone-id" env:"HOSTED_ZONE_ID" description:"AWS hosted zone id"` HostedZoneID string `long:"hosted-zone-id" env:"HOSTED_ZONE_ID" description:"AWS hosted zone id"`
@@ -442,7 +442,7 @@ func makeSSLConfig() (config proxy.SSLConfig, err error) {
config.DNSProvider = &route53.Provider{ config.DNSProvider = &route53.Provider{
Region: opts.SSL.DNS.Route53.Region, Region: opts.SSL.DNS.Route53.Region,
Profile: opts.SSL.DNS.Route53.Profile, Profile: opts.SSL.DNS.Route53.Profile,
AccessKeyId: opts.SSL.DNS.Route53.AccessKeyId, AccessKeyId: opts.SSL.DNS.Route53.AccessKeyID,
SecretAccessKey: opts.SSL.DNS.Route53.SecretAccessKey, SecretAccessKey: opts.SSL.DNS.Route53.SecretAccessKey,
SessionToken: opts.SSL.DNS.Route53.SessionToken, SessionToken: opts.SSL.DNS.Route53.SessionToken,
HostedZoneID: opts.SSL.DNS.Route53.HostedZoneID, HostedZoneID: opts.SSL.DNS.Route53.HostedZoneID,

View File

@@ -108,13 +108,15 @@ func Test_MainWithSSL(t *testing.T) {
setupLogger() setupLogger()
port := 40000 + int(rand.Int31n(10000)) port := 40000 + int(rand.Int31n(10000))
httpPort := 50000 + int(rand.Int31n(10000)) // use a high port for HTTP redirect
os.Args = []string{"test", "--static.enabled", os.Args = []string{"test", "--static.enabled",
"--static.rule=*,/svc1, https://httpbin.org/get,https://feedmaster.umputun.com/ping", "--static.rule=*,/svc1, https://httpbin.org/get,https://feedmaster.umputun.com/ping",
"--static.rule=*,/svc2/(.*), https://echo.umputun.com/$1,https://feedmaster.umputun.com/ping", "--static.rule=*,/svc2/(.*), https://echo.umputun.com/$1,https://feedmaster.umputun.com/ping",
"--file.enabled", "--file.name=discovery/provider/testdata/config.yml", "--file.enabled", "--file.name=discovery/provider/testdata/config.yml",
"--dbg", "--logger.enabled", "--logger.stdout", "--logger.file=/tmp/reproxy.log", "--dbg", "--logger.enabled", "--logger.stdout", "--logger.file=/tmp/reproxy.log",
"--listen=127.0.0.1:" + strconv.Itoa(port), "--signature", "--ssl.type=static", "--listen=127.0.0.1:" + strconv.Itoa(port), "--signature", "--ssl.type=static",
"--ssl.cert=proxy/testdata/localhost.crt", "--ssl.key=proxy/testdata/localhost.key"} "--ssl.cert=proxy/testdata/localhost.crt", "--ssl.key=proxy/testdata/localhost.key",
"--ssl.http-port=" + strconv.Itoa(httpPort)}
defer os.Remove("/tmp/reproxy.log") defer os.Remove("/tmp/reproxy.log")
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {

View File

@@ -403,8 +403,12 @@ func (s *ACMEServer) certCtrl(w http.ResponseWriter, r *http.Request) {
} }
w.Header().Set("Content-Type", "application/pem-certificate-chain") w.Header().Set("Content-Type", "application/pem-certificate-chain")
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: cert}) if err := pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: s.rootCert}) s.t.Logf("failed to encode certificate: %v", err)
}
if err := pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: s.rootCert}); err != nil {
s.t.Logf("failed to encode root certificate: %v", err)
}
} }
// POST /challenge - verify a challenge // POST /challenge - verify a challenge
@@ -449,11 +453,11 @@ func (s *ACMEServer) challengeCtrl(w http.ResponseWriter, r *http.Request) {
} }
} }
switch { switch challengeType {
case challengeType == "http-01": case "http-01":
s.verifyHTTP01Challenge(w, token, domain) s.verifyHTTP01Challenge(w, token, domain)
o.HTTP01Accepted = true o.HTTP01Accepted = true
case challengeType == "dns-01": case "dns-01":
s.verifyDNS01Challenge(w, domain) s.verifyDNS01Challenge(w, domain)
o.DNS01Accepted = true o.DNS01Accepted = true
default: default:

View File

@@ -64,7 +64,14 @@ func (h *Http) httpChallengeRouter(m AutocertManager) http.Handler {
func (h *Http) redirectHandler() http.Handler { func (h *Http) redirectHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
server := strings.Split(r.Host, ":")[0] server := strings.Split(r.Host, ":")[0]
newURL := fmt.Sprintf("https://%s:443%s", server, r.URL.Path) // use standard HTTPS port by default
httpsPort := 443
// no need to specify the port if it's the standard HTTPS port
portSuffix := fmt.Sprintf(":%d", httpsPort)
if httpsPort == 443 {
portSuffix = ""
}
newURL := fmt.Sprintf("https://%s%s%s", server, portSuffix, r.URL.Path)
if r.URL.RawQuery != "" { if r.URL.RawQuery != "" {
newURL += "?" + r.URL.RawQuery newURL += "?" + r.URL.RawQuery
} }
@@ -111,7 +118,7 @@ func (h *Http) makeAutocertManager() AutocertManager {
KeySource: certmagic.DefaultKeyGenerator, KeySource: certmagic.DefaultKeyGenerator,
Storage: &certmagic.FileStorage{Path: h.SSLConfig.ACMELocation}, Storage: &certmagic.FileStorage{Path: h.SSLConfig.ACMELocation},
OnDemand: &certmagic.OnDemandConfig{ OnDemand: &certmagic.OnDemandConfig{
DecisionFunc: func(ctx context.Context, name string) error { DecisionFunc: func(_ context.Context, name string) error {
if _, ok := fqdns[name]; ok { if _, ok := fqdns[name]; ok {
return nil return nil
} }
@@ -122,18 +129,29 @@ func (h *Http) makeAutocertManager() AutocertManager {
} }
var cache *certmagic.Cache var cache *certmagic.Cache
cache = certmagic.NewCache(certmagic.CacheOptions{ cache = certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { GetConfigForCert: func(_ certmagic.Certificate) (*certmagic.Config, error) {
return certmagic.New(cache, magicTmpl), nil return certmagic.New(cache, magicTmpl), nil
}, },
Logger: logger, Logger: logger,
}) })
magic := certmagic.New(cache, magicTmpl) magic := certmagic.New(cache, magicTmpl)
acme := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
// configure the ACMEIssuer
acmeIssuer := certmagic.ACMEIssuer{
CA: h.SSLConfig.ACMEDirectory, CA: h.SSLConfig.ACMEDirectory,
Email: h.SSLConfig.ACMEEmail, Email: h.SSLConfig.ACMEEmail,
Agreed: true, Agreed: true,
Logger: logger, Logger: logger,
}) }
// use RedirHTTPPort if specified for the HTTP challenge
// this allows ACME HTTP challenges to work in environments
// where binding to port 80 isn't possible
if h.SSLConfig.RedirHTTPPort != 0 {
acmeIssuer.AltHTTPPort = h.SSLConfig.RedirHTTPPort
}
acme := certmagic.NewACMEIssuer(magic, acmeIssuer)
if h.SSLConfig.DNSProvider != nil { if h.SSLConfig.DNSProvider != nil {
acme.DNS01Solver = &certmagic.DNS01Solver{ acme.DNS01Solver = &certmagic.DNS01Solver{
DNSManager: certmagic.DNSManager{ DNSManager: certmagic.DNSManager{

View File

@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@@ -43,7 +44,7 @@ func TestSSL_Redirect(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
assert.Equal(t, 307, resp.StatusCode) assert.Equal(t, 307, resp.StatusCode)
assert.Equal(t, "https://localhost:443/blah?param=1", resp.Header.Get("Location")) assert.Equal(t, "https://localhost/blah?param=1", resp.Header.Get("Location"))
} }
func TestSSL_ACME_HTTPChallengeRouter(t *testing.T) { func TestSSL_ACME_HTTPChallengeRouter(t *testing.T) {
@@ -61,11 +62,21 @@ func TestSSL_ACME_HTTPChallengeRouter(t *testing.T) {
}), }),
) )
// prepare a port for HTTP challenges
httpListener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
_, httpPortStr, err := net.SplitHostPort(httpListener.Addr().String())
require.NoError(t, err)
httpPort, err := strconv.Atoi(httpPortStr)
require.NoError(t, err)
httpListener.Close() // close it since we only needed it to get a port
p := Http{ p := Http{
SSLConfig: SSLConfig{ SSLConfig: SSLConfig{
ACMELocation: dir, ACMELocation: dir,
FQDNs: []string{"example.com", "localhost"}, FQDNs: []string{"example.com", "localhost"},
ACMEDirectory: cas.URL(), ACMEDirectory: cas.URL(),
RedirHTTPPort: httpPort, // use high-numbered port for ACME HTTP challenge
}, },
} }
@@ -86,7 +97,7 @@ func TestSSL_ACME_HTTPChallengeRouter(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
assert.Equal(t, 307, resp.StatusCode) assert.Equal(t, 307, resp.StatusCode)
assert.Equal(t, "https://localhost:443/blah?param=1", resp.Header.Get("Location")) assert.Equal(t, "https://localhost/blah?param=1", resp.Header.Get("Location"))
// acquire new cert from CA and check it with timeout // acquire new cert from CA and check it with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@@ -197,8 +208,8 @@ func TestSSL_ACME_DNSChallenge(t *testing.T) {
// signal that the DNS server is ready to accept connections // signal that the DNS server is ready to accept connections
close(dnsReady) close(dnsReady)
// set a timeout for the DNS server // set a timeout for the DNS server
err := dnsMock.ActivateAndServe() dnsErr := dnsMock.ActivateAndServe()
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { if dnsErr != nil && !strings.Contains(dnsErr.Error(), "use of closed network connection") {
t.Logf("DNS server error: %v", err) t.Logf("DNS server error: %v", err)
} }
}() }()
@@ -211,11 +222,21 @@ func TestSSL_ACME_DNSChallenge(t *testing.T) {
t.Log("dns server started at", dnsMock.Addr) t.Log("dns server started at", dnsMock.Addr)
// prepare a port for HTTP challenges
httpListener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
_, httpPortStr, err := net.SplitHostPort(httpListener.Addr().String())
require.NoError(t, err)
httpPort, err := strconv.Atoi(httpPortStr)
require.NoError(t, err)
httpListener.Close() // close it since we only needed it to get a port
p := Http{ p := Http{
SSLConfig: SSLConfig{ SSLConfig: SSLConfig{
ACMELocation: dir, ACMELocation: dir,
FQDNs: []string{"example.com", "localhost"}, FQDNs: []string{"example.com", "localhost"},
ACMEDirectory: cas.URL(), ACMEDirectory: cas.URL(),
RedirHTTPPort: httpPort, // use high-numbered port for ACME HTTP challenge
DNSProvider: &dnsProviderMock{ DNSProvider: &dnsProviderMock{
AppendRecordsFunc: func(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) { AppendRecordsFunc: func(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
assert.Equal(t, "example.com.", zone) assert.Equal(t, "example.com.", zone)