mirror of
https://github.com/yusing/godoxy.git
synced 2025-07-08 07:24:04 +02:00
added real_ip and cloudflare_real_ip middlewares, fixed that some middlewares does not work properly
This commit is contained in:
parent
ac3af49aa7
commit
860e914b90
27 changed files with 463 additions and 179 deletions
5
Makefile
5
Makefile
|
@ -15,7 +15,7 @@ build:
|
||||||
go build -ldflags '${BUILD_FLAG}' -pgo=auto -o bin/go-proxy ./cmd
|
go build -ldflags '${BUILD_FLAG}' -pgo=auto -o bin/go-proxy ./cmd
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test ./internal/...
|
GOPROXY_TEST=1 go test ./internal/...
|
||||||
|
|
||||||
up:
|
up:
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
@ -32,6 +32,9 @@ get:
|
||||||
debug:
|
debug:
|
||||||
make BUILD_FLAG="" build && sudo GOPROXY_DEBUG=1 bin/go-proxy
|
make BUILD_FLAG="" build && sudo GOPROXY_DEBUG=1 bin/go-proxy
|
||||||
|
|
||||||
|
run-test:
|
||||||
|
make BUILD_FLAG="" build && sudo GOPROXY_TEST=1 bin/go-proxy
|
||||||
|
|
||||||
run:
|
run:
|
||||||
make build && sudo bin/go-proxy
|
make build && sudo bin/go-proxy
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,8 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
NoSchemaValidation = GetEnvBool("GOPROXY_NO_SCHEMA_VALIDATION", false)
|
NoSchemaValidation = GetEnvBool("GOPROXY_NO_SCHEMA_VALIDATION", false)
|
||||||
IsDebug = GetEnvBool("GOPROXY_DEBUG", false)
|
IsTest = GetEnvBool("GOPROXY_TEST", false)
|
||||||
|
IsDebug = GetEnvBool("GOPROXY_DEBUG", IsTest)
|
||||||
|
|
||||||
ProxyHTTPAddr,
|
ProxyHTTPAddr,
|
||||||
ProxyHTTPHost,
|
ProxyHTTPHost,
|
||||||
|
|
|
@ -50,7 +50,7 @@ func (b Builder) Build() NestedError {
|
||||||
if len(b.errors) == 0 {
|
if len(b.errors) == 0 {
|
||||||
return nil
|
return nil
|
||||||
} else if len(b.errors) == 1 {
|
} else if len(b.errors) == 1 {
|
||||||
return b.errors[0]
|
return b.errors[0].Subjectf("%s", b.message)
|
||||||
}
|
}
|
||||||
return Join(b.message, b.errors...)
|
return Join(b.message, b.errors...)
|
||||||
}
|
}
|
||||||
|
|
118
internal/net/http/middleware/cloudflare_real_ip.go
Normal file
118
internal/net/http/middleware/cloudflare_real_ip.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cfIPv4CIDRsEndpoint = "https://www.cloudflare.com/ips-v4"
|
||||||
|
cfIPv6CIDRsEndpoint = "https://www.cloudflare.com/ips-v6"
|
||||||
|
cfCIDRsUpdateInterval = time.Hour
|
||||||
|
cfCIDRsUpdateRetryInterval = 3 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfCIDRsLastUpdate time.Time
|
||||||
|
cfCIDRsMu sync.Mutex
|
||||||
|
cfCIDRsLogger = logrus.WithField("middleware", "CloudflareRealIP")
|
||||||
|
)
|
||||||
|
|
||||||
|
var CloudflareRealIP = &realIP{
|
||||||
|
m: &Middleware{
|
||||||
|
withOptions: NewCloudflareRealIP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCloudflareRealIP(_ OptionsRaw, _ *ReverseProxy) (*Middleware, E.NestedError) {
|
||||||
|
cri := new(realIP)
|
||||||
|
cri.m = &Middleware{
|
||||||
|
impl: cri,
|
||||||
|
rewrite: func(r *Request) {
|
||||||
|
cidrs := tryFetchCFCIDR()
|
||||||
|
if cidrs != nil {
|
||||||
|
cri.From = cidrs
|
||||||
|
}
|
||||||
|
cri.setRealIP(r)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cri.realIPOpts = &realIPOpts{
|
||||||
|
Header: "CF-Connecting-IP",
|
||||||
|
Recursive: true,
|
||||||
|
}
|
||||||
|
return cri.m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryFetchCFCIDR() (cfCIDRs []*net.IPNet) {
|
||||||
|
if time.Since(cfCIDRsLastUpdate) < cfCIDRsUpdateInterval {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfCIDRsMu.Lock()
|
||||||
|
defer cfCIDRsMu.Unlock()
|
||||||
|
|
||||||
|
if time.Since(cfCIDRsLastUpdate) < cfCIDRsUpdateInterval {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if common.IsTest {
|
||||||
|
cfCIDRs = []*net.IPNet{
|
||||||
|
{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)},
|
||||||
|
{IP: net.IPv4(10, 0, 0, 0), Mask: net.IPv4Mask(255, 0, 0, 0)},
|
||||||
|
{IP: net.IPv4(172, 16, 0, 0), Mask: net.IPv4Mask(255, 255, 0, 0)},
|
||||||
|
{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cfCIDRs = make([]*net.IPNet, 0, 30)
|
||||||
|
err := errors.Join(
|
||||||
|
fetchUpdateCFIPRange(cfIPv4CIDRsEndpoint, cfCIDRs),
|
||||||
|
fetchUpdateCFIPRange(cfIPv6CIDRsEndpoint, cfCIDRs),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
cfCIDRsLastUpdate = time.Now().Add(-cfCIDRsUpdateRetryInterval - cfCIDRsUpdateInterval)
|
||||||
|
cfCIDRsLogger.Errorf("failed to update cloudflare range: %s, retry in %s", err, cfCIDRsUpdateRetryInterval)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfCIDRsLastUpdate = time.Now()
|
||||||
|
cfCIDRsLogger.Debugf("cloudflare CIDR range updated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchUpdateCFIPRange(endpoint string, cfCIDRs []*net.IPNet) error {
|
||||||
|
resp, err := http.Get(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(body), "\n") {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, cidr, err := net.ParseCIDR(line)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cloudflare responeded an invalid CIDR: %s", line)
|
||||||
|
} else {
|
||||||
|
cfCIDRs = append(cfCIDRs, cidr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/error_page"
|
"github.com/yusing/go-proxy/internal/api/v1/error_page"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
gpHTTP "github.com/yusing/go-proxy/internal/http"
|
gpHTTP "github.com/yusing/go-proxy/internal/net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
var CustomErrorPage = &Middleware{
|
var CustomErrorPage = &Middleware{
|
|
@ -17,8 +17,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
D "github.com/yusing/go-proxy/internal/docker"
|
D "github.com/yusing/go-proxy/internal/docker"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
gpHTTP "github.com/yusing/go-proxy/internal/http"
|
gpHTTP "github.com/yusing/go-proxy/internal/net/http"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -44,50 +43,50 @@ const (
|
||||||
xForwardedPort = "X-Forwarded-Port"
|
xForwardedPort = "X-Forwarded-Port"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ForwardAuth = newForwardAuth()
|
var ForwardAuth = func() *forwardAuth {
|
||||||
var faLogger = logrus.WithField("middleware", "ForwardAuth")
|
fa := new(forwardAuth)
|
||||||
|
|
||||||
func newForwardAuth() (fa *forwardAuth) {
|
|
||||||
fa = new(forwardAuth)
|
|
||||||
fa.m = new(Middleware)
|
fa.m = new(Middleware)
|
||||||
fa.m.labelParserMap = D.ValueParserMap{
|
fa.m.labelParserMap = D.ValueParserMap{
|
||||||
"trust_forward_header": D.BoolParser,
|
"trust_forward_header": D.BoolParser,
|
||||||
"auth_response_headers": D.YamlStringListParser,
|
"auth_response_headers": D.YamlStringListParser,
|
||||||
"add_auth_cookies_to_response": D.YamlStringListParser,
|
"add_auth_cookies_to_response": D.YamlStringListParser,
|
||||||
}
|
}
|
||||||
fa.m.withOptions = func(optsRaw OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError) {
|
fa.m.withOptions = NewForwardAuthfunc
|
||||||
tr, ok := rp.Transport.(*http.Transport)
|
return fa
|
||||||
if ok {
|
}()
|
||||||
tr = tr.Clone()
|
var faLogger = logrus.WithField("middleware", "ForwardAuth")
|
||||||
} else {
|
|
||||||
tr = common.DefaultTransport.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
faWithOpts := new(forwardAuth)
|
func NewForwardAuthfunc(optsRaw OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError) {
|
||||||
faWithOpts.forwardAuthOpts = new(forwardAuthOpts)
|
tr, ok := rp.Transport.(*http.Transport)
|
||||||
faWithOpts.client = http.Client{
|
if ok {
|
||||||
CheckRedirect: func(r *Request, via []*Request) error {
|
tr = tr.Clone()
|
||||||
return http.ErrUseLastResponse
|
} else {
|
||||||
},
|
tr = common.DefaultTransport.Clone()
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
Transport: tr,
|
|
||||||
}
|
|
||||||
faWithOpts.m = &Middleware{
|
|
||||||
impl: faWithOpts,
|
|
||||||
before: faWithOpts.forward,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := U.Deserialize(optsRaw, faWithOpts.forwardAuthOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.FailWith("set options", err)
|
|
||||||
}
|
|
||||||
_, err = E.Check(url.Parse(faWithOpts.Address))
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Invalid("address", faWithOpts.Address)
|
|
||||||
}
|
|
||||||
return faWithOpts.m, nil
|
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
faWithOpts := new(forwardAuth)
|
||||||
|
faWithOpts.forwardAuthOpts = new(forwardAuthOpts)
|
||||||
|
faWithOpts.client = http.Client{
|
||||||
|
CheckRedirect: func(r *Request, via []*Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Transport: tr,
|
||||||
|
}
|
||||||
|
faWithOpts.m = &Middleware{
|
||||||
|
impl: faWithOpts,
|
||||||
|
before: faWithOpts.forward,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Deserialize(optsRaw, faWithOpts.forwardAuthOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.FailWith("set options", err)
|
||||||
|
}
|
||||||
|
_, err = E.Check(url.Parse(faWithOpts.Address))
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Invalid("address", faWithOpts.Address)
|
||||||
|
}
|
||||||
|
return faWithOpts.m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request) {
|
func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request) {
|
|
@ -5,7 +5,8 @@ import (
|
||||||
|
|
||||||
D "github.com/yusing/go-proxy/internal/docker"
|
D "github.com/yusing/go-proxy/internal/docker"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
gpHTTP "github.com/yusing/go-proxy/internal/http"
|
gpHTTP "github.com/yusing/go-proxy/internal/net/http"
|
||||||
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -20,7 +21,7 @@ type (
|
||||||
Cookie = http.Cookie
|
Cookie = http.Cookie
|
||||||
|
|
||||||
BeforeFunc func(next http.Handler, w ResponseWriter, r *Request)
|
BeforeFunc func(next http.Handler, w ResponseWriter, r *Request)
|
||||||
RewriteFunc func(req *ProxyRequest)
|
RewriteFunc func(req *Request)
|
||||||
ModifyResponseFunc func(resp *Response) error
|
ModifyResponseFunc func(resp *Response) error
|
||||||
CloneWithOptFunc func(opts OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError)
|
CloneWithOptFunc func(opts OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError)
|
||||||
|
|
||||||
|
@ -42,6 +43,8 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Deserialize = U.Deserialize
|
||||||
|
|
||||||
func (m *Middleware) Name() string {
|
func (m *Middleware) Name() string {
|
||||||
return m.name
|
return m.name
|
||||||
}
|
}
|
||||||
|
@ -80,7 +83,7 @@ func PatchReverseProxy(rp *ReverseProxy, middlewares map[string]OptionsRaw) (res
|
||||||
for name, opts := range middlewares {
|
for name, opts := range middlewares {
|
||||||
m, ok := Get(name)
|
m, ok := Get(name)
|
||||||
if !ok {
|
if !ok {
|
||||||
invalidM.Addf("%s", name)
|
invalidM.Add(E.NotExist("middleware", name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,13 +121,12 @@ func PatchReverseProxy(rp *ReverseProxy, middlewares map[string]OptionsRaw) (res
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rewrites) > 0 {
|
if len(rewrites) > 0 {
|
||||||
if rp.Rewrite != nil {
|
origServeHTTP = rp.ServeHTTP
|
||||||
rewrites = append([]RewriteFunc{rp.Rewrite}, rewrites...)
|
rp.ServeHTTP = func(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
|
||||||
rp.Rewrite = func(req *ProxyRequest) {
|
|
||||||
for _, rewrite := range rewrites {
|
for _, rewrite := range rewrites {
|
||||||
rewrite(req)
|
rewrite(r)
|
||||||
}
|
}
|
||||||
|
origServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,16 @@ func Get(name string) (middleware *Middleware, ok bool) {
|
||||||
// initialize middleware names and label parsers
|
// initialize middleware names and label parsers
|
||||||
func init() {
|
func init() {
|
||||||
middlewares = map[string]*Middleware{
|
middlewares = map[string]*Middleware{
|
||||||
"set_x_forwarded": SetXForwarded,
|
"set_x_forwarded": SetXForwarded,
|
||||||
"add_x_forwarded": AddXForwarded,
|
"add_x_forwarded": AddXForwarded,
|
||||||
"redirect_http": RedirectHTTP,
|
"redirect_http": RedirectHTTP,
|
||||||
"forward_auth": ForwardAuth.m,
|
"forward_auth": ForwardAuth.m,
|
||||||
"modify_response": ModifyResponse.m,
|
"modify_response": ModifyResponse.m,
|
||||||
"modify_request": ModifyRequest.m,
|
"modify_request": ModifyRequest.m,
|
||||||
"error_page": CustomErrorPage,
|
"error_page": CustomErrorPage,
|
||||||
"custom_error_page": CustomErrorPage,
|
"custom_error_page": CustomErrorPage,
|
||||||
|
"real_ip": RealIP.m,
|
||||||
|
"cloudflare_real_ip": CloudflareRealIP.m,
|
||||||
}
|
}
|
||||||
names := make(map[*Middleware][]string)
|
names := make(map[*Middleware][]string)
|
||||||
for name, m := range middlewares {
|
for name, m := range middlewares {
|
57
internal/net/http/middleware/modify_request.go
Normal file
57
internal/net/http/middleware/modify_request.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
D "github.com/yusing/go-proxy/internal/docker"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
modifyRequest struct {
|
||||||
|
*modifyRequestOpts
|
||||||
|
m *Middleware
|
||||||
|
}
|
||||||
|
// order: set_headers -> add_headers -> hide_headers
|
||||||
|
modifyRequestOpts struct {
|
||||||
|
SetHeaders map[string]string
|
||||||
|
AddHeaders map[string]string
|
||||||
|
HideHeaders []string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var ModifyRequest = func() *modifyRequest {
|
||||||
|
mr := new(modifyRequest)
|
||||||
|
mr.m = new(Middleware)
|
||||||
|
mr.m.labelParserMap = D.ValueParserMap{
|
||||||
|
"set_headers": D.YamlLikeMappingParser(true),
|
||||||
|
"add_headers": D.YamlLikeMappingParser(true),
|
||||||
|
"hide_headers": D.YamlStringListParser,
|
||||||
|
}
|
||||||
|
mr.m.withOptions = NewModifyRequest
|
||||||
|
return mr
|
||||||
|
}()
|
||||||
|
|
||||||
|
func NewModifyRequest(optsRaw OptionsRaw, _ *ReverseProxy) (*Middleware, E.NestedError) {
|
||||||
|
mr := new(modifyRequest)
|
||||||
|
mr.m = &Middleware{
|
||||||
|
impl: mr,
|
||||||
|
rewrite: mr.modifyRequest,
|
||||||
|
}
|
||||||
|
mr.modifyRequestOpts = new(modifyRequestOpts)
|
||||||
|
err := Deserialize(optsRaw, mr.modifyRequestOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.FailWith("set options", err)
|
||||||
|
}
|
||||||
|
return mr.m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *modifyRequest) modifyRequest(req *Request) {
|
||||||
|
for k, v := range mr.SetHeaders {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
for k, v := range mr.AddHeaders {
|
||||||
|
req.Header.Add(k, v)
|
||||||
|
}
|
||||||
|
for _, k := range mr.HideHeaders {
|
||||||
|
req.Header.Del(k)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ import (
|
||||||
|
|
||||||
D "github.com/yusing/go-proxy/internal/docker"
|
D "github.com/yusing/go-proxy/internal/docker"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -21,9 +20,7 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var ModifyResponse = newModifyResponse()
|
var ModifyResponse = func() (mr *modifyResponse) {
|
||||||
|
|
||||||
func newModifyResponse() (mr *modifyResponse) {
|
|
||||||
mr = new(modifyResponse)
|
mr = new(modifyResponse)
|
||||||
mr.m = new(Middleware)
|
mr.m = new(Middleware)
|
||||||
mr.m.labelParserMap = D.ValueParserMap{
|
mr.m.labelParserMap = D.ValueParserMap{
|
||||||
|
@ -31,20 +28,22 @@ func newModifyResponse() (mr *modifyResponse) {
|
||||||
"add_headers": D.YamlLikeMappingParser(true),
|
"add_headers": D.YamlLikeMappingParser(true),
|
||||||
"hide_headers": D.YamlStringListParser,
|
"hide_headers": D.YamlStringListParser,
|
||||||
}
|
}
|
||||||
mr.m.withOptions = func(optsRaw OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError) {
|
mr.m.withOptions = NewModifyResponse
|
||||||
mrWithOpts := new(modifyResponse)
|
|
||||||
mrWithOpts.m = &Middleware{
|
|
||||||
impl: mrWithOpts,
|
|
||||||
modifyResponse: mrWithOpts.modifyResponse,
|
|
||||||
}
|
|
||||||
mrWithOpts.modifyResponseOpts = new(modifyResponseOpts)
|
|
||||||
err := U.Deserialize(optsRaw, mrWithOpts.modifyResponseOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.FailWith("set options", err)
|
|
||||||
}
|
|
||||||
return mrWithOpts.m, nil
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
|
}()
|
||||||
|
|
||||||
|
func NewModifyResponse(optsRaw OptionsRaw, _ *ReverseProxy) (*Middleware, E.NestedError) {
|
||||||
|
mr := new(modifyResponse)
|
||||||
|
mr.m = &Middleware{
|
||||||
|
impl: mr,
|
||||||
|
modifyResponse: mr.modifyResponse,
|
||||||
|
}
|
||||||
|
mr.modifyResponseOpts = new(modifyResponseOpts)
|
||||||
|
err := Deserialize(optsRaw, mr.modifyResponseOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.FailWith("set options", err)
|
||||||
|
}
|
||||||
|
return mr.m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mr *modifyResponse) modifyResponse(resp *http.Response) error {
|
func (mr *modifyResponse) modifyResponse(resp *http.Response) error {
|
157
internal/net/http/middleware/real_ip.go
Normal file
157
internal/net/http/middleware/real_ip.go
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
D "github.com/yusing/go-proxy/internal/docker"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://nginx.org/en/docs/http/ngx_http_realip_module.html
|
||||||
|
|
||||||
|
type realIP struct {
|
||||||
|
*realIPOpts
|
||||||
|
m *Middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
type realIPOpts struct {
|
||||||
|
// Header is the name of the header to use for the real client IP
|
||||||
|
Header string
|
||||||
|
// From is a list of Address / CIDRs to trust
|
||||||
|
From []*net.IPNet
|
||||||
|
/*
|
||||||
|
If recursive search is disabled,
|
||||||
|
the original client address that matches one of the trusted addresses is replaced by
|
||||||
|
the last address sent in the request header field defined by the Header field.
|
||||||
|
If recursive search is enabled,
|
||||||
|
the original client address that matches one of the trusted addresses is replaced by
|
||||||
|
the last non-trusted address sent in the request header field.
|
||||||
|
*/
|
||||||
|
Recursive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var RealIP = &realIP{
|
||||||
|
m: &Middleware{
|
||||||
|
labelParserMap: D.ValueParserMap{
|
||||||
|
"from": CIDRListParser,
|
||||||
|
"recursive": D.BoolParser,
|
||||||
|
},
|
||||||
|
withOptions: NewRealIP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var realIPOptsDefault = func() *realIPOpts {
|
||||||
|
return &realIPOpts{
|
||||||
|
Header: "X-Real-IP",
|
||||||
|
From: []*net.IPNet{
|
||||||
|
{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(8, 32)},
|
||||||
|
{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
|
||||||
|
{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
|
||||||
|
{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
|
||||||
|
{IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128)},
|
||||||
|
{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var realIPLogger = logrus.WithField("middleware", "RealIP")
|
||||||
|
|
||||||
|
func NewRealIP(opts OptionsRaw, _ *ReverseProxy) (*Middleware, E.NestedError) {
|
||||||
|
riWithOpts := new(realIP)
|
||||||
|
riWithOpts.m = &Middleware{
|
||||||
|
impl: riWithOpts,
|
||||||
|
rewrite: riWithOpts.setRealIP,
|
||||||
|
}
|
||||||
|
riWithOpts.realIPOpts = realIPOptsDefault()
|
||||||
|
err := Deserialize(opts, riWithOpts.realIPOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.FailWith("set options", err)
|
||||||
|
}
|
||||||
|
return riWithOpts.m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CIDRListParser(s string) (any, E.NestedError) {
|
||||||
|
sl, err := D.YamlStringListParser(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b := E.NewBuilder("invalid CIDR(s)")
|
||||||
|
|
||||||
|
CIDRs := sl.([]string)
|
||||||
|
res := make([]*net.IPNet, 0, len(CIDRs))
|
||||||
|
|
||||||
|
for _, cidr := range CIDRs {
|
||||||
|
if !strings.Contains(cidr, "/") {
|
||||||
|
cidr += "/32" // single IP
|
||||||
|
}
|
||||||
|
_, ipnet, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
b.Add(E.Invalid("CIDR", cidr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res = append(res, ipnet)
|
||||||
|
}
|
||||||
|
return res, b.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ri *realIP) isInCIDRList(ip net.IP) bool {
|
||||||
|
for _, CIDR := range ri.From {
|
||||||
|
if CIDR.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// not in any CIDR
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ri *realIP) setRealIP(req *Request) {
|
||||||
|
clientIPStr, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
realIPLogger.Debugf("failed to split host port from %s: %s", req.RemoteAddr, err)
|
||||||
|
}
|
||||||
|
clientIP := net.ParseIP(clientIPStr)
|
||||||
|
|
||||||
|
var isTrusted = false
|
||||||
|
for _, CIDR := range ri.From {
|
||||||
|
if CIDR.Contains(clientIP) {
|
||||||
|
isTrusted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isTrusted {
|
||||||
|
realIPLogger.Debugf("client ip %s is not trusted", clientIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var realIPs = req.Header.Values(ri.Header)
|
||||||
|
var lastNonTrustedIP string
|
||||||
|
|
||||||
|
if len(realIPs) == 0 {
|
||||||
|
realIPLogger.Debugf("no real ip found in header %q", ri.Header)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ri.Recursive {
|
||||||
|
lastNonTrustedIP = realIPs[len(realIPs)-1]
|
||||||
|
} else {
|
||||||
|
for _, r := range realIPs {
|
||||||
|
if !ri.isInCIDRList(net.ParseIP(r)) {
|
||||||
|
lastNonTrustedIP = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastNonTrustedIP == "" {
|
||||||
|
realIPLogger.Debugf("no non-trusted ip found in header %q", ri.Header)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.RemoteAddr = lastNonTrustedIP
|
||||||
|
req.Header.Set(ri.Header, lastNonTrustedIP)
|
||||||
|
req.Header.Set("X-Real-IP", lastNonTrustedIP)
|
||||||
|
req.Header.Set("X-Forwarded-For", lastNonTrustedIP)
|
||||||
|
|
||||||
|
realIPLogger.Debugf("real ip %s", lastNonTrustedIP)
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
gpHTTP "github.com/yusing/go-proxy/internal/http"
|
gpHTTP "github.com/yusing/go-proxy/internal/net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed test_data/sample_headers.json
|
//go:embed test_data/sample_headers.json
|
44
internal/net/http/middleware/x_forwarded.go
Normal file
44
internal/net/http/middleware/x_forwarded.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AddXForwarded = &Middleware{
|
||||||
|
rewrite: func(req *Request) {
|
||||||
|
clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err == nil {
|
||||||
|
req.Header.Set("X-Forwarded-For", clientIP)
|
||||||
|
} else {
|
||||||
|
req.Header.Del("X-Forwarded-For")
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Forwarded-Host", req.Host)
|
||||||
|
if req.TLS == nil {
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "http")
|
||||||
|
} else {
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var SetXForwarded = &Middleware{
|
||||||
|
rewrite: func(req *Request) {
|
||||||
|
clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err == nil {
|
||||||
|
prior := req.Header["X-Forwarded-For"]
|
||||||
|
if len(prior) > 0 {
|
||||||
|
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Forwarded-For", clientIP)
|
||||||
|
} else {
|
||||||
|
req.Header.Del("X-Forwarded-For")
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Forwarded-Host", req.Host)
|
||||||
|
if req.TLS == nil {
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "http")
|
||||||
|
} else {
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -15,7 +15,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptrace"
|
"net/http/httptrace"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
@ -58,39 +57,6 @@ type ProxyRequest struct {
|
||||||
// r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
|
// r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
|
||||||
// r.SetXForwarded()
|
// r.SetXForwarded()
|
||||||
// }
|
// }
|
||||||
func (r *ProxyRequest) SetXForwarded() {
|
|
||||||
clientIP, _, err := net.SplitHostPort(r.In.RemoteAddr)
|
|
||||||
if err == nil {
|
|
||||||
r.Out.Header.Set("X-Forwarded-For", clientIP)
|
|
||||||
} else {
|
|
||||||
r.Out.Header.Del("X-Forwarded-For")
|
|
||||||
}
|
|
||||||
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
|
|
||||||
if r.In.TLS == nil {
|
|
||||||
r.Out.Header.Set("X-Forwarded-Proto", "http")
|
|
||||||
} else {
|
|
||||||
r.Out.Header.Set("X-Forwarded-Proto", "https")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProxyRequest) AddXForwarded() {
|
|
||||||
clientIP, _, err := net.SplitHostPort(r.In.RemoteAddr)
|
|
||||||
if err == nil {
|
|
||||||
prior := r.Out.Header["X-Forwarded-For"]
|
|
||||||
if len(prior) > 0 {
|
|
||||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
|
||||||
}
|
|
||||||
r.Out.Header.Set("X-Forwarded-For", clientIP)
|
|
||||||
} else {
|
|
||||||
r.Out.Header.Del("X-Forwarded-For")
|
|
||||||
}
|
|
||||||
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
|
|
||||||
if r.In.TLS == nil {
|
|
||||||
r.Out.Header.Set("X-Forwarded-Proto", "http")
|
|
||||||
} else {
|
|
||||||
r.Out.Header.Set("X-Forwarded-Proto", "https")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||||
// sends it to another server, proxying the response back to the
|
// sends it to another server, proxying the response back to the
|
|
@ -13,10 +13,10 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/docker/idlewatcher"
|
"github.com/yusing/go-proxy/internal/docker/idlewatcher"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
. "github.com/yusing/go-proxy/internal/http"
|
. "github.com/yusing/go-proxy/internal/net/http"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
P "github.com/yusing/go-proxy/internal/proxy"
|
P "github.com/yusing/go-proxy/internal/proxy"
|
||||||
PT "github.com/yusing/go-proxy/internal/proxy/fields"
|
PT "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||||
"github.com/yusing/go-proxy/internal/route/middleware"
|
|
||||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
D "github.com/yusing/go-proxy/internal/docker"
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
modifyRequest struct {
|
|
||||||
*modifyRequestOpts
|
|
||||||
m *Middleware
|
|
||||||
}
|
|
||||||
// order: set_headers -> add_headers -> hide_headers
|
|
||||||
modifyRequestOpts struct {
|
|
||||||
SetHeaders map[string]string
|
|
||||||
AddHeaders map[string]string
|
|
||||||
HideHeaders []string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var ModifyRequest = newModifyRequest()
|
|
||||||
|
|
||||||
func newModifyRequest() (mr *modifyRequest) {
|
|
||||||
mr = new(modifyRequest)
|
|
||||||
mr.m = new(Middleware)
|
|
||||||
mr.m.labelParserMap = D.ValueParserMap{
|
|
||||||
"set_headers": D.YamlLikeMappingParser(true),
|
|
||||||
"add_headers": D.YamlLikeMappingParser(true),
|
|
||||||
"hide_headers": D.YamlStringListParser,
|
|
||||||
}
|
|
||||||
mr.m.withOptions = func(optsRaw OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError) {
|
|
||||||
mrWithOpts := new(modifyRequest)
|
|
||||||
mrWithOpts.m = &Middleware{
|
|
||||||
impl: mrWithOpts,
|
|
||||||
rewrite: mrWithOpts.modifyRequest,
|
|
||||||
}
|
|
||||||
mrWithOpts.modifyRequestOpts = new(modifyRequestOpts)
|
|
||||||
err := U.Deserialize(optsRaw, mrWithOpts.modifyRequestOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.FailWith("set options", err)
|
|
||||||
}
|
|
||||||
return mrWithOpts.m, nil
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr *modifyRequest) modifyRequest(req *ProxyRequest) {
|
|
||||||
for k, v := range mr.SetHeaders {
|
|
||||||
req.Out.Header.Set(k, v)
|
|
||||||
}
|
|
||||||
for k, v := range mr.AddHeaders {
|
|
||||||
req.Out.Header.Add(k, v)
|
|
||||||
}
|
|
||||||
for _, k := range mr.HideHeaders {
|
|
||||||
req.Out.Header.Del(k)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
var AddXForwarded = &Middleware{
|
|
||||||
rewrite: (*ProxyRequest).AddXForwarded,
|
|
||||||
}
|
|
||||||
|
|
||||||
var SetXForwarded = &Middleware{
|
|
||||||
rewrite: (*ProxyRequest).SetXForwarded,
|
|
||||||
}
|
|
|
@ -107,6 +107,9 @@ func Serialize(data any) (SerializedObject, E.NestedError) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Deserialize(src SerializedObject, target any) E.NestedError {
|
func Deserialize(src SerializedObject, target any) E.NestedError {
|
||||||
|
if src == nil || target == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
// convert data fields to lower no-snake
|
// convert data fields to lower no-snake
|
||||||
// convert target fields to lower no-snake
|
// convert target fields to lower no-snake
|
||||||
// then check if the field of data is in the target
|
// then check if the field of data is in the target
|
||||||
|
|
Loading…
Add table
Reference in a new issue