1
0
mirror of https://github.com/umputun/reproxy.git synced 2025-09-16 08:46:17 +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 {
Region string `long:"region" env:"REGION" description:"AWS region"`
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"`
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"`
@@ -442,7 +442,7 @@ func makeSSLConfig() (config proxy.SSLConfig, err error) {
config.DNSProvider = &route53.Provider{
Region: opts.SSL.DNS.Route53.Region,
Profile: opts.SSL.DNS.Route53.Profile,
AccessKeyId: opts.SSL.DNS.Route53.AccessKeyId,
AccessKeyId: opts.SSL.DNS.Route53.AccessKeyID,
SecretAccessKey: opts.SSL.DNS.Route53.SecretAccessKey,
SessionToken: opts.SSL.DNS.Route53.SessionToken,
HostedZoneID: opts.SSL.DNS.Route53.HostedZoneID,

View File

@@ -108,13 +108,15 @@ func Test_MainWithSSL(t *testing.T) {
setupLogger()
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",
"--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",
"--file.enabled", "--file.name=discovery/provider/testdata/config.yml",
"--dbg", "--logger.enabled", "--logger.stdout", "--logger.file=/tmp/reproxy.log",
"--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")
done := make(chan struct{})
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")
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: s.rootCert})
if err := pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
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
@@ -449,11 +453,11 @@ func (s *ACMEServer) challengeCtrl(w http.ResponseWriter, r *http.Request) {
}
}
switch {
case challengeType == "http-01":
switch challengeType {
case "http-01":
s.verifyHTTP01Challenge(w, token, domain)
o.HTTP01Accepted = true
case challengeType == "dns-01":
case "dns-01":
s.verifyDNS01Challenge(w, domain)
o.DNS01Accepted = true
default:

View File

@@ -64,7 +64,14 @@ func (h *Http) httpChallengeRouter(m AutocertManager) http.Handler {
func (h *Http) redirectHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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 != "" {
newURL += "?" + r.URL.RawQuery
}
@@ -111,7 +118,7 @@ func (h *Http) makeAutocertManager() AutocertManager {
KeySource: certmagic.DefaultKeyGenerator,
Storage: &certmagic.FileStorage{Path: h.SSLConfig.ACMELocation},
OnDemand: &certmagic.OnDemandConfig{
DecisionFunc: func(ctx context.Context, name string) error {
DecisionFunc: func(_ context.Context, name string) error {
if _, ok := fqdns[name]; ok {
return nil
}
@@ -122,18 +129,29 @@ func (h *Http) makeAutocertManager() AutocertManager {
}
var cache *certmagic.Cache
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
},
Logger: logger,
})
magic := certmagic.New(cache, magicTmpl)
acme := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
// configure the ACMEIssuer
acmeIssuer := certmagic.ACMEIssuer{
CA: h.SSLConfig.ACMEDirectory,
Email: h.SSLConfig.ACMEEmail,
Agreed: true,
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 {
acme.DNS01Solver = &certmagic.DNS01Solver{
DNSManager: certmagic.DNSManager{

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"sync"
"testing"
@@ -43,7 +44,7 @@ func TestSSL_Redirect(t *testing.T) {
require.NoError(t, err)
defer resp.Body.Close()
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) {
@@ -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{
SSLConfig: SSLConfig{
ACMELocation: dir,
FQDNs: []string{"example.com", "localhost"},
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)
defer resp.Body.Close()
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
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
close(dnsReady)
// set a timeout for the DNS server
err := dnsMock.ActivateAndServe()
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
dnsErr := dnsMock.ActivateAndServe()
if dnsErr != nil && !strings.Contains(dnsErr.Error(), "use of closed network connection") {
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)
// 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{
SSLConfig: SSLConfig{
ACMELocation: dir,
FQDNs: []string{"example.com", "localhost"},
ACMEDirectory: cas.URL(),
RedirHTTPPort: httpPort, // use high-numbered port for ACME HTTP challenge
DNSProvider: &dnsProviderMock{
AppendRecordsFunc: func(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
assert.Equal(t, "example.com.", zone)