mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-28 03:51:08 +01:00
refactor: move some io, http and string utils to separate repo
This commit is contained in:
@@ -1,69 +0,0 @@
|
||||
package httpheaders
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AppendCSP appends a CSP header to specific directives in the response writer.
|
||||
//
|
||||
// Directives other than the ones in cspDirectives will be kept as is.
|
||||
//
|
||||
// It will replace 'none' with the sources.
|
||||
//
|
||||
// It will append 'self' to the sources if it's not already present.
|
||||
func AppendCSP(w http.ResponseWriter, r *http.Request, cspDirectives []string, sources []string) {
|
||||
csp := make(map[string]string)
|
||||
cspValues := r.Header.Values("Content-Security-Policy")
|
||||
if len(cspValues) == 1 {
|
||||
cspValues = strings.Split(cspValues[0], ";")
|
||||
for i, cspString := range cspValues {
|
||||
cspValues[i] = strings.TrimSpace(cspString)
|
||||
}
|
||||
}
|
||||
|
||||
for _, cspString := range cspValues {
|
||||
parts := strings.SplitN(cspString, " ", 2)
|
||||
if len(parts) == 2 {
|
||||
csp[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
for _, directive := range cspDirectives {
|
||||
value, ok := csp[directive]
|
||||
if !ok {
|
||||
value = "'self'"
|
||||
}
|
||||
switch value {
|
||||
case "'self'":
|
||||
csp[directive] = value + " " + strings.Join(sources, " ")
|
||||
case "'none'":
|
||||
csp[directive] = strings.Join(sources, " ")
|
||||
default:
|
||||
for _, source := range sources {
|
||||
if !strings.Contains(value, source) {
|
||||
value += " " + source
|
||||
}
|
||||
}
|
||||
if !strings.Contains(value, "'self'") {
|
||||
value = "'self' " + value
|
||||
}
|
||||
csp[directive] = value
|
||||
}
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(csp))
|
||||
for directive, value := range csp {
|
||||
values = append(values, directive+" "+value)
|
||||
}
|
||||
|
||||
// Remove existing CSP header, case insensitive
|
||||
for k := range w.Header() {
|
||||
if strings.EqualFold(k, "Content-Security-Policy") {
|
||||
delete(w.Header(), k)
|
||||
}
|
||||
}
|
||||
|
||||
// Set new CSP header
|
||||
w.Header()["Content-Security-Policy"] = values
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package httpheaders
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendCSP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
initialHeaders map[string][]string
|
||||
sources []string
|
||||
directives []string
|
||||
expectedCSP map[string]string
|
||||
}{
|
||||
{
|
||||
name: "No CSP header",
|
||||
initialHeaders: map[string][]string{},
|
||||
sources: []string{},
|
||||
directives: []string{"default-src", "script-src", "frame-src", "style-src", "connect-src"},
|
||||
expectedCSP: map[string]string{"default-src": "'self'", "script-src": "'self'", "frame-src": "'self'", "style-src": "'self'", "connect-src": "'self'"},
|
||||
},
|
||||
{
|
||||
name: "No CSP header with sources",
|
||||
initialHeaders: map[string][]string{},
|
||||
sources: []string{"https://example.com"},
|
||||
directives: []string{"default-src", "script-src", "frame-src", "style-src", "connect-src"},
|
||||
expectedCSP: map[string]string{"default-src": "'self' https://example.com", "script-src": "'self' https://example.com", "frame-src": "'self' https://example.com", "style-src": "'self' https://example.com", "connect-src": "'self' https://example.com"},
|
||||
},
|
||||
{
|
||||
name: "replace 'none' with sources",
|
||||
initialHeaders: map[string][]string{
|
||||
"Content-Security-Policy": {"default-src 'none'"},
|
||||
},
|
||||
sources: []string{"https://example.com"},
|
||||
directives: []string{"default-src"},
|
||||
expectedCSP: map[string]string{"default-src": "https://example.com"},
|
||||
},
|
||||
{
|
||||
name: "CSP header with some directives",
|
||||
initialHeaders: map[string][]string{
|
||||
"Content-Security-Policy": {"default-src 'none'", "script-src 'unsafe-inline'"},
|
||||
},
|
||||
sources: []string{"https://example.com"},
|
||||
directives: []string{"script-src"},
|
||||
expectedCSP: map[string]string{
|
||||
"default-src": "'none",
|
||||
"script-src": "'unsafe-inline' https://example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CSP header with some directives with self",
|
||||
initialHeaders: map[string][]string{
|
||||
"Content-Security-Policy": {"default-src 'self'", "connect-src 'self'"},
|
||||
},
|
||||
sources: []string{"https://api.example.com"},
|
||||
directives: []string{"default-src", "connect-src"},
|
||||
expectedCSP: map[string]string{
|
||||
"default-src": "'self' https://api.example.com",
|
||||
"connect-src": "'self' https://api.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AppendCSP sources conflict with existing CSP header",
|
||||
initialHeaders: map[string][]string{
|
||||
"Content-Security-Policy": {"default-src 'self' https://cdn.example.com", "script-src 'unsafe-inline'"},
|
||||
},
|
||||
sources: []string{"https://cdn.example.com", "https://api.example.com"},
|
||||
directives: []string{"default-src", "script-src"},
|
||||
expectedCSP: map[string]string{
|
||||
"default-src": "'self' https://cdn.example.com https://api.example.com",
|
||||
"script-src": "'unsafe-inline' https://cdn.example.com https://api.example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Non-standard CSP directive",
|
||||
initialHeaders: map[string][]string{
|
||||
"Content-Security-Policy": {
|
||||
"default-src 'self'",
|
||||
"script-src 'unsafe-inline'",
|
||||
"img-src 'self'", // img-src is not in cspDirectives list
|
||||
},
|
||||
},
|
||||
sources: []string{"https://example.com"},
|
||||
directives: []string{"default-src", "script-src"},
|
||||
expectedCSP: map[string]string{
|
||||
"default-src": "'self' https://example.com",
|
||||
"script-src": "'unsafe-inline' https://example.com",
|
||||
// img-src should not be present in response as it's not in cspDirectives
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create a test request with initial headers
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for header, values := range tc.initialHeaders {
|
||||
req.Header[header] = values
|
||||
}
|
||||
|
||||
// Create a test response recorder
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Call the function under test
|
||||
AppendCSP(w, req, tc.directives, tc.sources)
|
||||
|
||||
// Check the resulting CSP headers
|
||||
respHeaders := w.Header()
|
||||
cspValues, exists := respHeaders["Content-Security-Policy"]
|
||||
|
||||
// If we expect no CSP headers, verify none exist
|
||||
if len(tc.expectedCSP) == 0 {
|
||||
if exists && len(cspValues) > 0 {
|
||||
t.Errorf("Expected no CSP header, but got %v", cspValues)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Verify CSP headers exist when expected
|
||||
if !exists || len(cspValues) == 0 {
|
||||
t.Errorf("Expected CSP header to be set, but it was not")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the CSP response and verify each directive
|
||||
foundDirectives := make(map[string]string)
|
||||
for _, cspValue := range cspValues {
|
||||
parts := strings.Split(cspValue, ";")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
directiveParts := strings.SplitN(part, " ", 2)
|
||||
if len(directiveParts) != 2 {
|
||||
t.Errorf("Invalid CSP directive format: %s", part)
|
||||
continue
|
||||
}
|
||||
|
||||
directive := directiveParts[0]
|
||||
value := directiveParts[1]
|
||||
foundDirectives[directive] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Verify expected directives
|
||||
for directive, expectedValue := range tc.expectedCSP {
|
||||
actualValue, ok := foundDirectives[directive]
|
||||
if !ok {
|
||||
t.Errorf("Expected directive %s not found in response", directive)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if all expected sources are in the actual value
|
||||
expectedSources := strings.SplitSeq(expectedValue, " ")
|
||||
for source := range expectedSources {
|
||||
if !strings.Contains(actualValue, source) {
|
||||
t.Errorf("Directive %s missing expected source %s. Got: %s", directive, source, actualValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package httpheaders
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderXForwardedMethod = "X-Forwarded-Method"
|
||||
HeaderXForwardedFor = "X-Forwarded-For"
|
||||
HeaderXForwardedProto = "X-Forwarded-Proto"
|
||||
HeaderXForwardedHost = "X-Forwarded-Host"
|
||||
HeaderXForwardedPort = "X-Forwarded-Port"
|
||||
HeaderXForwardedURI = "X-Forwarded-Uri"
|
||||
HeaderXRealIP = "X-Real-IP"
|
||||
|
||||
HeaderContentType = "Content-Type"
|
||||
HeaderContentLength = "Content-Length"
|
||||
|
||||
HeaderGoDoxyCheckRedirect = "X-Godoxy-Check-Redirect"
|
||||
)
|
||||
|
||||
// Hop-by-hop headers. These are removed when sent to the backend.
|
||||
// As of RFC 7230, hop-by-hop headers are required to appear in the
|
||||
// Connection header field. These are the headers defined by the
|
||||
// obsoleted RFC 2616 (section 13.5.1) and are used for backward
|
||||
// compatibility.
|
||||
var hopHeaders = []string{
|
||||
"Connection",
|
||||
"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
|
||||
"Keep-Alive",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Authorization",
|
||||
"Te", // canonicalized version of "TE"
|
||||
"Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
}
|
||||
|
||||
func UpgradeType(h http.Header) string {
|
||||
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
|
||||
return ""
|
||||
}
|
||||
return h.Get("Upgrade")
|
||||
}
|
||||
|
||||
// RemoveHopByHopHeaders removes hop-by-hop headers.
|
||||
func RemoveHopByHopHeaders(h http.Header) {
|
||||
// RFC 7230, section 6.1: Remove headers listed in the "Connection" header.
|
||||
for _, f := range h["Connection"] {
|
||||
for sf := range strings.SplitSeq(f, ",") {
|
||||
if sf = textproto.TrimString(sf); sf != "" {
|
||||
h.Del(sf)
|
||||
}
|
||||
}
|
||||
}
|
||||
// RFC 2616, section 13.5.1: Remove a set of known hop-by-hop headers.
|
||||
// This behavior is superseded by the RFC 7230 Connection header, but
|
||||
// preserve it for backwards compatibility.
|
||||
for _, f := range hopHeaders {
|
||||
h.Del(f)
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveHop(h http.Header) {
|
||||
reqUpType := UpgradeType(h)
|
||||
RemoveHopByHopHeaders(h)
|
||||
|
||||
if reqUpType != "" {
|
||||
h.Set("Connection", "Upgrade")
|
||||
h.Set("Upgrade", reqUpType)
|
||||
} else {
|
||||
h.Del("Connection")
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveServiceHeaders(h http.Header) {
|
||||
h.Del("X-Powered-By")
|
||||
h.Del("Server")
|
||||
}
|
||||
|
||||
func CopyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FilterHeaders(h http.Header, allowed []string) http.Header {
|
||||
if len(allowed) == 0 {
|
||||
return h
|
||||
}
|
||||
|
||||
filtered := make(http.Header)
|
||||
|
||||
for i, header := range allowed {
|
||||
values := h.Values(header)
|
||||
if len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
filtered[http.CanonicalHeaderKey(allowed[i])] = append([]string(nil), values...)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func HeaderToMap(h http.Header) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for k, v := range h {
|
||||
if len(v) > 0 {
|
||||
result[k] = v[0] // Take the first value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package httpheaders
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func IsWebsocket(h http.Header) bool {
|
||||
return UpgradeType(h) == "websocket"
|
||||
}
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/gperr"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/godoxy/internal/task"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/godoxy/internal/utils/pool"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
)
|
||||
|
||||
// TODO: stats of each server.
|
||||
|
||||
@@ -12,12 +12,11 @@ import (
|
||||
|
||||
"github.com/yusing/godoxy/internal/entrypoint"
|
||||
. "github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/reverseproxy"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
"github.com/yusing/godoxy/internal/route"
|
||||
routeTypes "github.com/yusing/godoxy/internal/route/types"
|
||||
"github.com/yusing/godoxy/internal/task"
|
||||
expect "github.com/yusing/godoxy/internal/utils/testing"
|
||||
"github.com/yusing/goutils/http/reverseproxy"
|
||||
)
|
||||
|
||||
func noOpHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
@@ -102,8 +101,10 @@ func (f fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
func TestReverseProxyBypass(t *testing.T) {
|
||||
rp := reverseproxy.NewReverseProxy("test", nettypes.MustParseURL("http://example.com"), fakeRoundTripper{})
|
||||
err := PatchReverseProxy(rp, map[string]OptionsRaw{
|
||||
url, err := url.Parse("http://example.com")
|
||||
expect.NoError(t, err)
|
||||
rp := reverseproxy.NewReverseProxy("test", url, fakeRoundTripper{})
|
||||
err = PatchReverseProxy(rp, map[string]OptionsRaw{
|
||||
"response": {
|
||||
"bypass": "path /test/* | path /api",
|
||||
"set_headers": map[string]string{
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
"github.com/yusing/godoxy/internal/utils/atomic"
|
||||
"github.com/yusing/godoxy/internal/utils/strutils"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
type cloudflareRealIP struct {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware/errorpage"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
)
|
||||
|
||||
type customErrorPage struct{}
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -91,7 +91,7 @@ func (m *forwardAuthMiddleware) before(w http.ResponseWriter, r *http.Request) (
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
body, release, err := utils.ReadAllBody(resp)
|
||||
body, release, err := httputils.ReadAllBody(resp)
|
||||
defer release()
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/gperr"
|
||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/reverseproxy"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
"github.com/yusing/goutils/http/reverseproxy"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/gperr"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
"github.com/yusing/godoxy/internal/utils/strutils"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
// snakes and cases will be stripped on `Get`
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/rs/zerolog/log"
|
||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
"github.com/yusing/godoxy/internal/utils/synk"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
ioutils "github.com/yusing/goutils/io"
|
||||
"github.com/yusing/goutils/synk"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ func (m *modifyHTML) modifyResponse(resp *http.Response) error {
|
||||
}
|
||||
|
||||
// NOTE: do not put it in the defer, it will be used as resp.Body
|
||||
content, release, err := utils.ReadAllBody(resp)
|
||||
content, release, err := httputils.ReadAllBody(resp)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
return err
|
||||
@@ -71,19 +72,19 @@ func (m *modifyHTML) modifyResponse(resp *http.Response) error {
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(content[:0])
|
||||
err = buildHTML(m, doc, buf)
|
||||
err = buildHTML(doc, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.ContentLength = int64(buf.Len())
|
||||
resp.Header.Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||
resp.Body = utils.NewHookCloser(io.NopCloser(bytes.NewReader(buf.Bytes())), release)
|
||||
resp.Body = ioutils.NewHookReadCloser(io.NopCloser(bytes.NewReader(buf.Bytes())), release)
|
||||
return nil
|
||||
}
|
||||
|
||||
// copied and modified from (*goquery.Selection).Html()
|
||||
func buildHTML(m *modifyHTML, s *goquery.Document, buf *bytes.Buffer) error {
|
||||
func buildHTML(s *goquery.Document, buf *bytes.Buffer) error {
|
||||
// Merge all head nodes into one
|
||||
headNodes := s.Find("head")
|
||||
if headNodes.Length() > 1 {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/httpheaders"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
)
|
||||
|
||||
// https://nginx.org/en/docs/http/ngx_http_realip_module.html
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/httpheaders"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
. "github.com/yusing/godoxy/internal/utils/testing"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
)
|
||||
|
||||
func TestSetRealIPOpts(t *testing.T) {
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/gperr"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/reverseproxy"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
. "github.com/yusing/godoxy/internal/utils/testing"
|
||||
"github.com/yusing/goutils/http/reverseproxy"
|
||||
)
|
||||
|
||||
//go:embed test_data/sample_headers.json
|
||||
@@ -152,7 +152,7 @@ func newMiddlewaresTest(middlewares []*Middleware, args *testArgs) (*TestResult,
|
||||
rr.parent = http.DefaultTransport
|
||||
}
|
||||
|
||||
rp := reverseproxy.NewReverseProxy("test", args.upstreamURL, rr)
|
||||
rp := reverseproxy.NewReverseProxy("test", &args.upstreamURL.URL, rr)
|
||||
patchReverseProxy(rp, middlewares)
|
||||
rp.ServeHTTP(w, req)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
@@ -1,560 +0,0 @@
|
||||
// Copyright 2011 The Go Authors.
|
||||
// Modified from the Go project under the a BSD-style License (https://cs.opensource.google/go/go/+/refs/tags/go1.23.1:src/net/http/httputil/reverseproxy.go)
|
||||
// https://cs.opensource.google/go/go/+/master:LICENSE
|
||||
|
||||
package reverseproxy
|
||||
|
||||
// This is a small mod on net/http/httputil/reverseproxy.go
|
||||
// that boosts performance in some cases
|
||||
// and compatible to other modules of this project
|
||||
// Copyright (c) 2024 yusing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/httpheaders"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
U "github.com/yusing/godoxy/internal/utils"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
// A ProxyRequest contains a request to be rewritten by a [ReverseProxy].
|
||||
type ProxyRequest struct {
|
||||
// In is the request received by the proxy.
|
||||
// The Rewrite function must not modify In.
|
||||
In *http.Request
|
||||
|
||||
// Out is the request which will be sent by the proxy.
|
||||
// The Rewrite function may modify or replace this request.
|
||||
// Hop-by-hop headers are removed from this request
|
||||
// before Rewrite is called.
|
||||
Out *http.Request
|
||||
}
|
||||
|
||||
// SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and
|
||||
// X-Forwarded-Proto headers of the outbound request.
|
||||
//
|
||||
// - The X-Forwarded-For header is set to the client IP address.
|
||||
// - The X-Forwarded-Host header is set to the host name requested
|
||||
// by the client.
|
||||
// - The X-Forwarded-Proto header is set to "http" or "https", depending
|
||||
// on whether the inbound request was made on a TLS-enabled connection.
|
||||
//
|
||||
// If the outbound request contains an existing X-Forwarded-For header,
|
||||
// SetXForwarded appends the client IP address to it. To append to the
|
||||
// inbound request's X-Forwarded-For header (the default behavior of
|
||||
// [ReverseProxy] when using a Director function), copy the header
|
||||
// from the inbound request before calling SetXForwarded:
|
||||
//
|
||||
// rewriteFunc := func(r *httputil.ProxyRequest) {
|
||||
// r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
|
||||
// r.SetXForwarded()
|
||||
// }
|
||||
|
||||
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||
// sends it to another server, proxying the response back to the
|
||||
// client.
|
||||
//
|
||||
// 1xx responses are forwarded to the client if the underlying
|
||||
// transport supports ClientTrace.Got1xxResponse.
|
||||
type ReverseProxy struct {
|
||||
zerolog.Logger
|
||||
|
||||
// The transport used to perform proxy requests.
|
||||
Transport http.RoundTripper
|
||||
|
||||
// ModifyResponse is an optional function that modifies the
|
||||
// Response from the backend. It is called if the backend
|
||||
// returns a response at all, with any HTTP status code.
|
||||
// If the backend is unreachable, the optional ErrorHandler is
|
||||
// called before ModifyResponse.
|
||||
//
|
||||
// If ModifyResponse returns an error, ErrorHandler is called
|
||||
// with its error value. If ErrorHandler is nil, its default
|
||||
// implementation is used.
|
||||
ModifyResponse func(*http.Response) error
|
||||
AccessLogger *accesslog.AccessLogger
|
||||
|
||||
HandlerFunc http.HandlerFunc
|
||||
|
||||
TargetName string
|
||||
TargetURL *nettypes.URL
|
||||
}
|
||||
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
func joinURLPath(a, b *url.URL) (path, rawpath string) {
|
||||
if a.RawPath == "" && b.RawPath == "" {
|
||||
return singleJoiningSlash(a.Path, b.Path), ""
|
||||
}
|
||||
// Same as singleJoiningSlash, but uses EscapedPath to determine
|
||||
// whether a slash should be added
|
||||
apath := a.EscapedPath()
|
||||
bpath := b.EscapedPath()
|
||||
|
||||
aslash := strings.HasSuffix(apath, "/")
|
||||
bslash := strings.HasPrefix(bpath, "/")
|
||||
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a.Path + b.Path[1:], apath + bpath[1:]
|
||||
case !aslash && !bslash:
|
||||
return a.Path + "/" + b.Path, apath + "/" + bpath
|
||||
}
|
||||
return a.Path + b.Path, apath + bpath
|
||||
}
|
||||
|
||||
// NewReverseProxy returns a new [ReverseProxy] that routes
|
||||
// URLs to the scheme, host, and base path provided in target. If the
|
||||
// target's path is "/base" and the incoming request was for "/dir",
|
||||
// the target request will be for /base/dir.
|
||||
func NewReverseProxy(name string, target *nettypes.URL, transport http.RoundTripper) *ReverseProxy {
|
||||
if transport == nil {
|
||||
panic("nil transport")
|
||||
}
|
||||
rp := &ReverseProxy{
|
||||
Logger: log.With().Str("name", name).Logger(),
|
||||
Transport: transport,
|
||||
TargetName: name,
|
||||
TargetURL: target,
|
||||
}
|
||||
rp.HandlerFunc = rp.handler
|
||||
return rp
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) rewriteRequestURL(req *http.Request) {
|
||||
targetQuery := p.TargetURL.RawQuery
|
||||
req.URL.Scheme = p.TargetURL.Scheme
|
||||
req.URL.Host = p.TargetURL.Host
|
||||
req.URL.Path, req.URL.RawPath = joinURLPath(&p.TargetURL.URL, req.URL)
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//go:linkname errStreamClosed golang.org/x/net/http2.errStreamClosed
|
||||
var errStreamClosed error
|
||||
|
||||
func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, r *http.Request, err error, writeHeader bool) {
|
||||
reqURL := r.Host + r.URL.Path
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled), errors.Is(err, io.EOF):
|
||||
log.Trace().Err(err).Str("url", reqURL).Msg("http proxy error")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
log.Debug().Err(err).Str("url", reqURL).Msg("http proxy error")
|
||||
default:
|
||||
var recordErr tls.RecordHeaderError
|
||||
if errors.As(err, &recordErr) {
|
||||
log.Error().
|
||||
Str("url", reqURL).
|
||||
Msgf(`scheme was likely misconfigured as https,
|
||||
try setting "proxy.%s.scheme" back to "http"`, p.TargetName)
|
||||
log.Err(err).Msg("underlying error")
|
||||
goto logged
|
||||
}
|
||||
if errors.Is(err, errStreamClosed) {
|
||||
goto logged
|
||||
}
|
||||
var h2Err http2.StreamError
|
||||
if errors.As(err, &h2Err) {
|
||||
// ignore these errors
|
||||
switch h2Err.Code {
|
||||
case http2.ErrCodeStreamClosed:
|
||||
goto logged
|
||||
}
|
||||
}
|
||||
var h3Err *http3.Error
|
||||
if errors.As(err, &h3Err) {
|
||||
// ignore these errors
|
||||
switch h3Err.ErrorCode {
|
||||
case
|
||||
http3.ErrCodeNoError,
|
||||
http3.ErrCodeRequestCanceled:
|
||||
goto logged
|
||||
}
|
||||
}
|
||||
log.Err(err).Str("url", reqURL).Msg("http proxy error")
|
||||
}
|
||||
|
||||
logged:
|
||||
if writeHeader {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
if p.AccessLogger != nil {
|
||||
p.AccessLogger.LogError(r, err)
|
||||
}
|
||||
}
|
||||
|
||||
// modifyResponse conditionally runs the optional ModifyResponse hook
|
||||
// and reports whether the request should proceed.
|
||||
func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response, origReq, req *http.Request) bool {
|
||||
if p.ModifyResponse == nil {
|
||||
return true
|
||||
}
|
||||
res.Request = origReq
|
||||
err := p.ModifyResponse(res)
|
||||
res.Request = req
|
||||
if err != nil {
|
||||
res.Body.Close()
|
||||
p.errorHandler(rw, req, err, true)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
p.HandlerFunc(rw, req)
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
||||
transport := p.Transport
|
||||
|
||||
ctx := req.Context()
|
||||
if ctx.Done() != nil {
|
||||
// CloseNotifier predates context.Context, and has been
|
||||
// entirely superseded by it. If the request contains
|
||||
// a Context that carries a cancellation signal, don't
|
||||
// bother spinning up a goroutine to watch the CloseNotify
|
||||
// channel (if any).
|
||||
//
|
||||
// If the request Context has a nil Done channel (which
|
||||
// means it is either context.Background, or a custom
|
||||
// Context implementation with no cancellation signal),
|
||||
// then consult the CloseNotifier if available.
|
||||
} else if cn, ok := rw.(http.CloseNotifier); ok {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
notifyChan := cn.CloseNotify()
|
||||
go func() {
|
||||
select {
|
||||
case <-notifyChan:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
outreq := req.Clone(ctx)
|
||||
if req.ContentLength == 0 {
|
||||
outreq.Body = nil // Issue 16036: nil Body for http.Transport retries
|
||||
}
|
||||
if outreq.Body != nil {
|
||||
// Reading from the request body after returning from a handler is not
|
||||
// allowed, and the RoundTrip goroutine that reads the Body can outlive
|
||||
// this handler. This can lead to a crash if the handler panics (see
|
||||
// Issue 46866). Although calling Close doesn't guarantee there isn't
|
||||
// any Read in flight after the handle returns, in practice it's safe to
|
||||
// read after closing it.
|
||||
defer outreq.Body.Close()
|
||||
}
|
||||
if outreq.Header == nil {
|
||||
outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
|
||||
}
|
||||
|
||||
p.rewriteRequestURL(outreq)
|
||||
outreq.Close = false
|
||||
|
||||
reqUpType := httpheaders.UpgradeType(outreq.Header)
|
||||
if !IsPrint(reqUpType) {
|
||||
p.errorHandler(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType), true)
|
||||
return
|
||||
}
|
||||
|
||||
outreq.Header.Del("Forwarded")
|
||||
httpheaders.RemoveHopByHopHeaders(outreq.Header)
|
||||
|
||||
// Issue 21096: tell backend applications that care about trailer support
|
||||
// that we support trailers. (We do, but we don't go out of our way to
|
||||
// advertise that unless the incoming client request thought it was worth
|
||||
// mentioning.) Note that we look at req.Header, not outreq.Header, since
|
||||
// the latter has passed through removeHopByHopHeaders.
|
||||
if httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") {
|
||||
outreq.Header.Set("Te", "trailers")
|
||||
}
|
||||
|
||||
// After stripping all the hop-by-hop connection headers above, add back any
|
||||
// necessary for protocol upgrades, such as for websockets.
|
||||
if reqUpType != "" {
|
||||
outreq.Header.Set("Connection", "Upgrade")
|
||||
outreq.Header.Set("Upgrade", reqUpType)
|
||||
|
||||
if strings.EqualFold(reqUpType, "websocket") {
|
||||
cleanWebsocketHeaders(outreq)
|
||||
}
|
||||
}
|
||||
|
||||
// If we aren't the first proxy retain prior
|
||||
// X-Forwarded-For information as a comma+space
|
||||
// separated list and fold multiple headers into one.
|
||||
prior, ok := outreq.Header[httpheaders.HeaderXForwardedFor]
|
||||
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
|
||||
|
||||
if !omit {
|
||||
xff, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
xff = req.RemoteAddr
|
||||
}
|
||||
if len(prior) > 0 {
|
||||
xff = strings.Join(prior, ", ") + ", " + xff
|
||||
}
|
||||
outreq.Header.Set(httpheaders.HeaderXForwardedFor, xff)
|
||||
}
|
||||
|
||||
var reqScheme string
|
||||
if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
reqScheme = "https"
|
||||
} else {
|
||||
reqScheme = "http"
|
||||
}
|
||||
|
||||
outreq.Header.Set(httpheaders.HeaderXForwardedMethod, req.Method)
|
||||
outreq.Header.Set(httpheaders.HeaderXForwardedProto, reqScheme)
|
||||
outreq.Header.Set(httpheaders.HeaderXForwardedHost, req.Host)
|
||||
outreq.Header.Set(httpheaders.HeaderXForwardedURI, req.RequestURI)
|
||||
|
||||
if _, ok := outreq.Header["User-Agent"]; !ok {
|
||||
// If the outbound request doesn't have a User-Agent header set,
|
||||
// don't send the default Go HTTP client User-Agent.
|
||||
outreq.Header.Set("User-Agent", "")
|
||||
}
|
||||
|
||||
var (
|
||||
roundTripMutex sync.Mutex
|
||||
roundTripDone bool
|
||||
)
|
||||
trace := &httptrace.ClientTrace{
|
||||
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
|
||||
roundTripMutex.Lock()
|
||||
defer roundTripMutex.Unlock()
|
||||
if roundTripDone {
|
||||
// If RoundTrip has returned, don't try to further modify
|
||||
// the ResponseWriter's header map.
|
||||
return nil
|
||||
}
|
||||
h := rw.Header()
|
||||
copyHeader(h, http.Header(header))
|
||||
rw.WriteHeader(code)
|
||||
|
||||
// Clear headers, it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses
|
||||
clear(h)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
outreq = outreq.WithContext(httptrace.WithClientTrace(outreq.Context(), trace)) //nolint:contextcheck
|
||||
|
||||
res, err := transport.RoundTrip(outreq)
|
||||
|
||||
roundTripMutex.Lock()
|
||||
roundTripDone = true
|
||||
roundTripMutex.Unlock()
|
||||
if err != nil {
|
||||
p.errorHandler(rw, outreq, err, false)
|
||||
res = &http.Response{
|
||||
Status: http.StatusText(http.StatusBadGateway),
|
||||
StatusCode: http.StatusBadGateway,
|
||||
Proto: req.Proto,
|
||||
ProtoMajor: req.ProtoMajor,
|
||||
ProtoMinor: req.ProtoMinor,
|
||||
Header: http.Header{},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte("Origin server is not reachable."))),
|
||||
Request: req,
|
||||
TLS: req.TLS,
|
||||
}
|
||||
}
|
||||
|
||||
if p.AccessLogger != nil {
|
||||
defer func() {
|
||||
p.AccessLogger.Log(req, res)
|
||||
}()
|
||||
}
|
||||
|
||||
httpheaders.RemoveServiceHeaders(res.Header)
|
||||
|
||||
// Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
|
||||
if res.StatusCode == http.StatusSwitchingProtocols {
|
||||
if !p.modifyResponse(rw, res, req, outreq) {
|
||||
return
|
||||
}
|
||||
p.handleUpgradeResponse(rw, outreq, res)
|
||||
return
|
||||
}
|
||||
|
||||
httpheaders.RemoveHopByHopHeaders(res.Header)
|
||||
|
||||
if !p.modifyResponse(rw, res, req, outreq) {
|
||||
return
|
||||
}
|
||||
|
||||
copyHeader(rw.Header(), res.Header)
|
||||
|
||||
// The "Trailer" header isn't included in the Transport's response,
|
||||
// at least for *http.Transport. Build it up from Trailer.
|
||||
announcedTrailers := len(res.Trailer)
|
||||
if announcedTrailers > 0 {
|
||||
trailerKeys := make([]string, 0, len(res.Trailer))
|
||||
for k := range res.Trailer {
|
||||
trailerKeys = append(trailerKeys, k)
|
||||
}
|
||||
rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
|
||||
}
|
||||
|
||||
rw.WriteHeader(res.StatusCode)
|
||||
|
||||
err = U.CopyCloseWithContext(ctx, rw, res.Body, int(res.ContentLength)) // close now, instead of defer, to populate res.Trailer
|
||||
if err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
p.errorHandler(rw, req, err, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(res.Trailer) > 0 {
|
||||
// Force chunking if we saw a response trailer.
|
||||
// This prevents net/http from calculating the length for short
|
||||
// bodies and adding a Content-Length.
|
||||
http.NewResponseController(rw).Flush()
|
||||
}
|
||||
|
||||
if len(res.Trailer) == announcedTrailers {
|
||||
copyHeader(rw.Header(), res.Trailer)
|
||||
return
|
||||
}
|
||||
|
||||
for k, vv := range res.Trailer {
|
||||
k = http.TrailerPrefix + k
|
||||
for _, v := range vv {
|
||||
rw.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reference: https://github.com/traefik/traefik/blob/master/pkg/proxy/httputil/proxy.go
|
||||
// https://tools.ietf.org/html/rfc6455#page-20
|
||||
func cleanWebsocketHeaders(req *http.Request) {
|
||||
req.Header["Sec-WebSocket-Key"] = req.Header["Sec-Websocket-Key"]
|
||||
delete(req.Header, "Sec-Websocket-Key")
|
||||
|
||||
req.Header["Sec-WebSocket-Extensions"] = req.Header["Sec-Websocket-Extensions"]
|
||||
delete(req.Header, "Sec-Websocket-Extensions")
|
||||
|
||||
req.Header["Sec-WebSocket-Accept"] = req.Header["Sec-Websocket-Accept"]
|
||||
delete(req.Header, "Sec-Websocket-Accept")
|
||||
|
||||
req.Header["Sec-WebSocket-Protocol"] = req.Header["Sec-Websocket-Protocol"]
|
||||
delete(req.Header, "Sec-Websocket-Protocol")
|
||||
|
||||
req.Header["Sec-WebSocket-Version"] = req.Header["Sec-Websocket-Version"]
|
||||
delete(req.Header, "Sec-Websocket-Version")
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, res *http.Response) {
|
||||
reqUpType := httpheaders.UpgradeType(req.Header)
|
||||
resUpType := httpheaders.UpgradeType(res.Header)
|
||||
if !IsPrint(resUpType) { // We know reqUpType is ASCII, it's checked by the caller.
|
||||
p.errorHandler(rw, req, fmt.Errorf("backend tried to switch to invalid protocol %q", resUpType), true)
|
||||
return
|
||||
}
|
||||
if !strings.EqualFold(reqUpType, resUpType) {
|
||||
p.errorHandler(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType), true)
|
||||
return
|
||||
}
|
||||
|
||||
backConn, ok := res.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
p.errorHandler(rw, req, errors.New("internal error: 101 switching protocols response with non-writable body"), true)
|
||||
return
|
||||
}
|
||||
|
||||
rc := http.NewResponseController(rw)
|
||||
conn, brw, hijackErr := rc.Hijack()
|
||||
if errors.Is(hijackErr, http.ErrNotSupported) {
|
||||
p.errorHandler(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw), true)
|
||||
return
|
||||
}
|
||||
|
||||
backConnCloseCh := make(chan bool)
|
||||
go func() {
|
||||
// Ensure that the cancellation of a request closes the backend.
|
||||
// See issue https://golang.org/issue/35559.
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
case <-backConnCloseCh:
|
||||
}
|
||||
backConn.Close()
|
||||
}()
|
||||
defer close(backConnCloseCh)
|
||||
|
||||
if hijackErr != nil {
|
||||
p.errorHandler(rw, req, fmt.Errorf("hijack failed on protocol switch: %w", hijackErr), true)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
copyHeader(rw.Header(), res.Header)
|
||||
|
||||
res.Header = rw.Header()
|
||||
res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
|
||||
if err := res.Write(brw); err != nil {
|
||||
//nolint:errorlint
|
||||
p.errorHandler(rw, req, fmt.Errorf("response write: %s", err), true)
|
||||
return
|
||||
}
|
||||
if err := brw.Flush(); err != nil {
|
||||
//nolint:errorlint
|
||||
p.errorHandler(rw, req, fmt.Errorf("response flush: %s", err), true)
|
||||
return
|
||||
}
|
||||
|
||||
bdp := U.NewBidirectionalPipe(req.Context(), conn, backConn)
|
||||
//nolint:errcheck
|
||||
bdp.Start()
|
||||
}
|
||||
|
||||
func IsPrint(s string) bool {
|
||||
for _, r := range s {
|
||||
if r < ' ' || r > '~' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
)
|
||||
|
||||
type noopTransport struct{}
|
||||
|
||||
func (t noopTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("Hello, world!")),
|
||||
Request: req,
|
||||
ContentLength: int64(len("Hello, world!")),
|
||||
Header: http.Header{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type noopResponseWriter struct{}
|
||||
|
||||
func (w noopResponseWriter) Header() http.Header {
|
||||
return http.Header{}
|
||||
}
|
||||
|
||||
func (w noopResponseWriter) Write(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (w noopResponseWriter) WriteHeader(statusCode int) {
|
||||
}
|
||||
|
||||
func BenchmarkReverseProxy(b *testing.B) {
|
||||
var w noopResponseWriter
|
||||
var req = http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Scheme: "http", Host: "test"},
|
||||
Body: io.NopCloser(strings.NewReader("Hello, world!")),
|
||||
}
|
||||
proxy := NewReverseProxy("test", nettypes.MustParseURL("http://localhost:8080"), noopTransport{})
|
||||
for b.Loop() {
|
||||
proxy.ServeHTTP(w, &req)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user