diff --git a/app/proxy/proxy.go b/app/proxy/proxy.go index c8a289c..4f7dc72 100644 --- a/app/proxy/proxy.go +++ b/app/proxy/proxy.go @@ -213,8 +213,8 @@ func (h *Http) proxyHandler() http.HandlerFunc { keepHost := ctx.Value(ctxKeepHost).(bool) r.Header.Add("X-Forwarded-Host", r.Host) if h.SSLConfig.SSLMode == SSLAuto || h.SSLConfig.SSLMode == SSLStatic { - r.Header.Add("X-Forwarded-Proto", "https") - r.Header.Add("X-Forwarded-Port", "443") + h.setHeaderIfNotExists(r, "X-Forwarded-Proto", "https") + h.setHeaderIfNotExists(r, "X-Forwarded-Port", "443") } r.URL.Path = uu.Path r.URL.Host = uu.Host @@ -466,3 +466,9 @@ func (h *Http) discoveredServers(ctx context.Context, interval time.Duration) (s } return servers } + +func (h *Http) setHeaderIfNotExists(r *http.Request, key, value string) { + if _, ok := r.Header[key]; !ok { + r.Header.Set(key, value) + } +} diff --git a/app/proxy/proxy_test.go b/app/proxy/proxy_test.go index 7ada4a3..1079cc3 100644 --- a/app/proxy/proxy_test.go +++ b/app/proxy/proxy_test.go @@ -3,6 +3,7 @@ package proxy import ( "bytes" "context" + "crypto/tls" "fmt" "io" "math/rand" @@ -10,6 +11,7 @@ import ( "net/http/httptest" "net/url" "strconv" + "strings" "sync/atomic" "testing" "time" @@ -120,6 +122,111 @@ func TestHttp_Do(t *testing.T) { }) } +func TestHttp_DoWithSSL(t *testing.T) { + port := rand.Intn(10000) + 40000 + h := Http{Timeouts: Timeouts{ResponseHeader: 200 * time.Millisecond}, Address: fmt.Sprintf("localhost:%d", port), + AccessLog: io.Discard, Signature: true, ProxyHeaders: []string{"hh1:vv1", "hh2:vv2"}, StdOutEnabled: true, + Reporter: &ErrorReporter{Nice: true}, + SSLConfig: SSLConfig{SSLMode: SSLStatic, Cert: "testdata/localhost.crt", Key: "testdata/localhost.key"}, Insecure: true, + } + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + ds := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("req: %v", r) + w.Header().Add("h1", "v1") + require.Equal(t, "127.0.0.1", r.Header.Get("X-Real-IP")) + require.Equal(t, "127.0.0.1", r.Header.Get("X-Forwarded-For")) + require.Equal(t, "https", r.Header.Get("X-Forwarded-Proto")) // ssl auto only + require.Equal(t, "443", r.Header.Get("X-Forwarded-Port")) + fmt.Fprintf(w, "response %s", r.URL.String()) + })) + + svc := discovery.NewService([]discovery.Provider{ + &provider.Static{Rules: []string{ + "localhost,^/api/(.*)," + strings.Replace(ds.URL, "127.0.0.1", "localhost", 1) + "/123/$1,", + "127.0.0.1,^/api/(.*)," + strings.Replace(ds.URL, "127.0.0.1", "localhost", 1) + "/567/$1,", + }, + }}, time.Millisecond*10) + + go func() { + _ = svc.Run(context.Background()) + }() + + time.Sleep(50 * time.Millisecond) + h.Matcher = svc + h.Metrics = mgmt.NewMetrics() + + go func() { + _ = h.Run(ctx) + }() + time.Sleep(10 * time.Millisecond) + + client := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + t.Run("to localhost, good", func(t *testing.T) { + req, err := http.NewRequest("GET", "https://localhost:"+strconv.Itoa(port)+"/api/something", http.NoBody) + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Logf("%+v", resp.Header) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "response /123/something", string(body)) + assert.Equal(t, "reproxy", resp.Header.Get("App-Name")) + assert.Equal(t, "v1", resp.Header.Get("h1")) + assert.Equal(t, "vv1", resp.Header.Get("hh1")) + assert.Equal(t, "vv2", resp.Header.Get("hh2")) + }) + + t.Run("to localhost, request with X-Forwarded-Proto and X-Forwarded-Port", func(t *testing.T) { + req, err := http.NewRequest("GET", "https://localhost:"+strconv.Itoa(port)+"/api/something", http.NoBody) + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-Port", "443") + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Logf("%+v", resp.Header) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "response /123/something", string(body)) + assert.Equal(t, "reproxy", resp.Header.Get("App-Name")) + assert.Equal(t, "v1", resp.Header.Get("h1")) + assert.Equal(t, "vv1", resp.Header.Get("hh1")) + assert.Equal(t, "vv2", resp.Header.Get("hh2")) + }) + + t.Run("to 127.0.0.1", func(t *testing.T) { + req, err := http.NewRequest("GET", "https://127.0.0.1:"+strconv.Itoa(port)+"/api/something", http.NoBody) + req.Header.Set("X-Forwarded-Proto", "https") + req.Header.Set("X-Forwarded-Port", "443") + require.NoError(t, err) + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Logf("%+v", resp.Header) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "response /567/something", string(body)) + assert.Equal(t, "reproxy", resp.Header.Get("App-Name")) + assert.Equal(t, "v1", resp.Header.Get("h1")) + assert.Equal(t, "vv1", resp.Header.Get("hh1")) + assert.Equal(t, "vv2", resp.Header.Get("hh2")) + }) +} + func TestHttp_DoWithAssets(t *testing.T) { port := rand.Intn(10000) + 40000 cc := NewCacheControl(time.Hour * 12) diff --git a/app/proxy/testdata/localhost.crt b/app/proxy/testdata/localhost.crt index 2881df9..a8505c4 100644 --- a/app/proxy/testdata/localhost.crt +++ b/app/proxy/testdata/localhost.crt @@ -1,18 +1,21 @@ -----BEGIN CERTIFICATE----- -MIIC5TCCAc2gAwIBAgIJALiRj0+veRc9MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV -BAMMCWxvY2FsaG9zdDAeFw0yMTA0MjcwNzQ1MzlaFw0yMTA1MjcwNzQ1MzlaMBQx -EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAKku5qZGexf0uvgVY+u8DOSnq8W10w+yQ1wSkMR5T0ag3hyimIDmDhNB3dI0 -5/B9IsIC9ZwUxOGHVCjP+TODXAGYIGYRx3BQb8si2369UXTQYNvVNQzPFdThjdcN -gXUmLN08a3ZMdXDC62DcOcVv1+oHd1qbuugkHqwA9CEn/aMJeYPso/4cfbgXX0WD -LEYkzgYwkyVf4JxyDWjm5d5fh/Tgpuu3lgH8qgKi51BmuSdiMiWsZ+AZykjpwAY6 -FsNHHmi1NZ1gLj/bzXNjziK50SIgIBiqTIxYdQBrYliRYC3RD252M4Sjk+WAOkVs -RvLDvYLcl0kqclayzBr2qI/2RkECAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo -b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B -AQsFAAOCAQEARvUEtFQC/l73/cTyEy24Om3t7uwHWzYRBQDz2dkj2V+OHuXaXKQW -9ucv9DupNYxuXLVg93IPZgDGlFUZMlNnTvP5APBdQZyuoyZER7pqlJg8Sfo1BO5P -KUqaspgdIzd8BmIYMkOaDgd/kOgqjGKjXJwHVvSl7oBcZ8WrxDBMuopXuFosYHVK -z9ZsknqbxibgWBhwLKgDSOwLwVRIPIXlgJpSIyfDpQt0D3PbiFfcXYHqZ09ocZ/b -mIRqF7/OpWW/15Zdo3+gqohWr5qoDZIH8gtBWRs6Ai3cbE/D5jSNdfDrrjRs0HkN -GHmkcX4ABMze6SN7cWFpgeIvy1mxKfMoZw== ------END CERTIFICATE----- +MIIDhzCCAm+gAwIBAgIUKMrw5NqY2c4v/ziNzA+JGC7keMgwDQYJKoZIhvcNAQEL +BQAwUjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAklMMRAwDgYDVQQHDAdDaGljYWdv +MRAwDgYDVQQKDAdyZXByb3h5MRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjQwMjA2 +MDQ1NDIxWhgPMjA1MTA2MjQwNDU0MjFaMFIxCzAJBgNVBAYTAlVTMQswCQYDVQQI +DAJJTDEQMA4GA1UEBwwHQ2hpY2FnbzEQMA4GA1UECgwHcmVwcm94eTESMBAGA1UE +AwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxvql +fklS0YOZhy2KmP+LlGvYpt2j5GgUr7FWXS/nUdYPtT8GhTzhP6AuVO5a63vmUuoY +XP61DtnirC571eIpgxNiZSUzRYn/IFR75BT025hZ6hwpAU+6ccpJjbQs48O74cgS +xL+QML2/zqgWIvT9hZg5+/fi8gO76Gcwyi/r1pwk4Gac1HJTdicQes5AewUuFQt+ +AdN/VSklzDmQAn+mgWmXqxnTMgccGBlsiiYTjKkBSIrUB5TEXIpASzU12EzF8LCU +mG2g4/wUGgKAXEUXI/KGdhm83XR7a0vWRsKNZDokNIeFfohr0JI1h1uJNM+2bfN5 +bTaLEjnxxwnw7j+ApwIDAQABo1MwUTAdBgNVHQ4EFgQUYiSDgICi9jUqX3K5hDXz +sZlksBswHwYDVR0jBBgwFoAUYiSDgICi9jUqX3K5hDXzsZlksBswDwYDVR0TAQH/ +BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEArSpccPKHbnS4Xc0SUdqhBiEpM49v +u3yVXvUFaFq6drIJNeAjmMundw4qPuSl8w56YVwzHwmv8OSAyb15RUl+1mSpT//i +sIqn3Ph3y6AcnyrkobCYOPho4dZMTsuSPfa9WRuFdvpUuZcn9JlvzlPZeYs6ba32 +wZfGBu6We26u89dcUKstKLEWZOI6rvTwViDiNafpnQc2JmP8U27wkyQe0uRuwUUY +nLlB44QmWWHgct2YJz7wIJ0R9LQB32rhvLvkDnpl2VRA87xl8Q4z6yGfZVSzkRtq +2K8HX+VuCB3NB61tC+eYbNEehkcdfJmuXzO/fLcoMWKEH5Q1ExmPMn+BKQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/app/proxy/testdata/localhost.key b/app/proxy/testdata/localhost.key index 4fed723..18312c2 100644 --- a/app/proxy/testdata/localhost.key +++ b/app/proxy/testdata/localhost.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCpLuamRnsX9Lr4 -FWPrvAzkp6vFtdMPskNcEpDEeU9GoN4copiA5g4TQd3SNOfwfSLCAvWcFMThh1Qo -z/kzg1wBmCBmEcdwUG/LItt+vVF00GDb1TUMzxXU4Y3XDYF1JizdPGt2THVwwutg -3DnFb9fqB3dam7roJB6sAPQhJ/2jCXmD7KP+HH24F19FgyxGJM4GMJMlX+Cccg1o -5uXeX4f04Kbrt5YB/KoCoudQZrknYjIlrGfgGcpI6cAGOhbDRx5otTWdYC4/281z -Y84iudEiICAYqkyMWHUAa2JYkWAt0Q9udjOEo5PlgDpFbEbyw72C3JdJKnJWsswa -9qiP9kZBAgMBAAECggEAcCZ7F3ZZWwQMfTAQ0NAT6++KWsGxbBJLvNlBxjx0ZOl2 -05ylY60dX36mQRZ5Ol55kArOLe1GpgpDq9pR4+gMMbJap87ZWoa31P0Ca/2r5bfM -vW2UgS0116y9jfWR/8qSqwXGZuFAaMONrOPQGCWQB79zS0k4mXJ4MqVfMCuGY3Bs -IkeyT96/pcYg36KeTGfvZOrX1GvH3udht5x2BxwuWdNjLEqL391OetnZe1H3NArs -fklboxgc/G0kRfx6X9/pqB+zytqeYec1nqc13I+3z6xjD1a2dK7rldY6CAp0IhwU -54hmu8MO/Laq3159xbB0BR48ClIwox+4z0pT5hDtEQKBgQDbfVMOma9TYmWGd7Or -/iuOVS64Jm2vrGySg/JlSBFskogy1vTtzwuAWe/3NLy5FVt1W7WC2Wt8OM1LIsh1 -lBiWM4Fz2bqLAgu1Y48Y0DsNVtckgo1jmwT4i1Ub664NMVvsaXLNtiSWzf53Jmh9 -J5zyg9ZiLXA7q+P5QDpZdskmowKBgQDFU1jC0LoaQaw+vH06eIoXVWYYwU+xpAK+ -AXZv7N3nBafhYTaTfWT/j54xoJr4kCnavXADMZq3oeHzR20B9oc4F1qT/6rEzYlu -pi7EtxXHf406Huf1uLOU8VckYEZd31i9dM0By7RD+EopYcI1TEIN1UR5XYv/bwHa -tZHjAFoBywKBgQCjnrZG4QxRFb3nUt6OrYgcr6WHQ6Zq2heJ1XDiTaonjMiZVaL6 -kGjbgrAfUaIKO0CVqQsTgy7cSJ/JjiFvfToi5jxvd3TXYWwHCTPIZJpQ5Fa3cdci -1JINEhkdGkECtrP29djOPyThgqhafDhSbDBUnTE4uPS8lvP4gAe/X4yuDwKBgQC6 -AGaavMWwGleSi3o/s3/3nrgufYnxmPg8woQx3MUPD3XALTKUtI6Pl4E2pn1t7/aE -Ci2b1RZSInYqLBnEz+2GIf1vpIAEIvp5IozTQQF2m/Uz5A4iwYgFzbimwVmTAwVT -ENZt6uZxa4n8l/nI46kgAPgaruNYU/sbfiuWHq65IQKBgCMYfLML4oeRt+Xm5wcy -T+PrP0qKt20wW/nqn4TVjuO606ywD2aLS/AJ96k04maEEAebahy6ODoEjNb3erQW -VGsMydDBZ0QlMs6x7LDh3JLP0+JNETCezd1Nl20ykjpaoZ8Unzf/gpgaVjZON+wF -ON1xMW7NBrs9xvcjhBVgtn6r ------END PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDG+qV+SVLRg5mH +LYqY/4uUa9im3aPkaBSvsVZdL+dR1g+1PwaFPOE/oC5U7lrre+ZS6hhc/rUO2eKs +LnvV4imDE2JlJTNFif8gVHvkFPTbmFnqHCkBT7pxykmNtCzjw7vhyBLEv5Awvb/O +qBYi9P2FmDn79+LyA7voZzDKL+vWnCTgZpzUclN2JxB6zkB7BS4VC34B039VKSXM +OZACf6aBaZerGdMyBxwYGWyKJhOMqQFIitQHlMRcikBLNTXYTMXwsJSYbaDj/BQa +AoBcRRcj8oZ2GbzddHtrS9ZGwo1kOiQ0h4V+iGvQkjWHW4k0z7Zt83ltNosSOfHH +CfDuP4CnAgMBAAECggEAVx8xzOyf5XqAg26OS9VAMTlTQCS1ePGVdSPpk53A49Ud +RZeV7EquuWQSRT+j8Y1rWIyFJFqlvh3qoMctk4WV9X1MTMsP+vekDGzRXhlK6Md5 +PwcbcSaOlPokYHYuXX+7SO2IQjs6EA1U6VAxeRbZ1l/Dq65q4Np/sQ9VjoGS+oDX +ApTYm5lKiKjBzf4xJ9jE61YaC9phxE099Tl8CEDRUUCEFL7fd6I+cPpCZE6094P5 +OHjMJYXAONclhXCXmPeGjtio9VnGgrGaclnBzjHs94XcW+slXBZdzDubXsi5Uhb3 +6P+GVpuyhpgFLEqtAbl6HUcGtS/Gbq4uYW+2FhlpWQKBgQD5pmKiJ7P2W2a3gZto +/Rb9r6Q2YnbtAPigKCaN6Ntzahrej1we7Qqi4lYS1z9YmRaXxyvySdNiTbYo/qLm +XEK2F8AnJUpaEK0+QJ7EQbTww8bElhPnpG9246+qMTbUwSgf6sRZ+RToGMQ5EBTY +deFgz4H0qXrDj2RQkOC1ayvDRQKBgQDMClBjsZa+FKoTWBGnHFlBzQQ9X0jAaSno +sPPsd9N8pcHZ2P3NehSEGeCeRc8xcZZib4TK1APnRGg+56tX+QSvwGT4P8C5GHvM +S3T8xojgpFK6JGai/jrcBWqOCa3xXrP3miiF8Es01Q2Wy3EuMffkBlUBgsW66PPU +A3W4UwWc+wKBgGtz4iBJVnxC+wMhFfMqfCrU3qlJ2EZKlLjajz2lbE9Q7B+/NLda +76kMImAZpXpM6hyJ7bBrdkBpkm4yq4rbSxt1PY+bzVTWuLqCtdNjNK4slfEnZ4nc +jN1vQrzOUftg6BRUyA6x1v3PKyYkddR1aHxy1EyqZdyma1cCBLYRWtTBAoGAUpD5 +5t2+OjzyddF1k0INfGsSBCPCtNnZc6fnjREQK6iHwTflvHhiRPKTynhFV6S3Ti4C +dnFFAxjTdmEZHQhPtS8NrMdfnYci0ZDXTlKooP7d2yVPwzVNbCtk6wVPthS0jsV7 +EHgkdsSgMx0wN5lQzp0hWPMqQHBz+p9Ly8MMynECgYEAr+zCEd1v3n6okfaZKFV9 +czy1jigLgXOdDSzqAe8FE53hFQiCkYn5fj7byNRYrCmUb1dtoQJsM7AmN1LkWVv3 +5hC8FKHS78SBkeCRplWIr329z+xoBrV+9b4DubUQXACW7zQh911404TdbZMjGohW +oJdJyZA2raAm/gaOyTIqDz0= +-----END PRIVATE KEY----- \ No newline at end of file