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:
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user