Compare commits

..

No commits in common. "main" and "v0.1.26" have entirely different histories.

2 changed files with 99 additions and 83 deletions

View File

@ -6,16 +6,15 @@ import (
"fmt" "fmt"
"gitea.illuad.fr/adrien/middleman" "gitea.illuad.fr/adrien/middleman"
"gitea.illuad.fr/adrien/middleman/flag" "gitea.illuad.fr/adrien/middleman/flag"
"gitea.illuad.fr/adrien/middleman/pkg/fastrp"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"net" "net"
"net/http" "net/http"
"net/http/httputil"
"net/netip" "net/netip"
"net/url" "net/url"
"regexp" "regexp"
"slices"
"strings" "strings"
"time" "time"
) )
@ -35,7 +34,25 @@ type methodRegex = map[string]*regexp.Regexp
// ProxyHandler takes an incoming request and sends it to another server, proxying the response back to the client. // ProxyHandler takes an incoming request and sends it to another server, proxying the response back to the client.
type ProxyHandler struct { type ProxyHandler struct {
frp *fastrp.FastRP rp *httputil.ReverseProxy
}
// ErrHTTPMethodNotAllowed is returned if the request's HTTP method is not allowed for this container.
type ErrHTTPMethodNotAllowed struct {
httpMethod string
}
// ErrNoMatch is returned if the request's URL path is not allowed for this container.
type ErrNoMatch struct {
path, httpMethod string
}
func (e ErrHTTPMethodNotAllowed) Error() string {
return fmt.Sprintf("%s is not in the list of HTTP methods allowed this container", e.httpMethod)
}
func (e ErrNoMatch) Error() string {
return fmt.Sprintf("%s does not match any registered regular expression for this HTTP method (%s)", e.path, e.httpMethod)
} }
// ServeCmd is the command name. // ServeCmd is the command name.
@ -75,8 +92,8 @@ const (
defaultHealthEndpoint = "/healthz" defaultHealthEndpoint = "/healthz"
// defaultHealthListenAddr is the proxy health endpoint default listen address and port. // defaultHealthListenAddr is the proxy health endpoint default listen address and port.
defaultHealthListenAddr = "127.0.0.1:5732" defaultHealthListenAddr = "127.0.0.1:5732"
// defaultAllowedRequests is the default allowed requests. Note that they should be sufficient to make Traefik work properly. // defaultAllowedRequest is the proxy default allowed request.
defaultAllowedRequests = "/v1.\\d{1,2}/(version|containers/(?:json|[a-zA-Z0-9]{64}/json)|events)$" defaultAllowedRequest = "^/(version|containers/.*|events.*)$"
) )
var s serve var s serve
@ -84,7 +101,7 @@ var s serve
var ( var (
containerMethodRegex = map[string]methodRegex{ containerMethodRegex = map[string]methodRegex{
"*": { "*": {
http.MethodGet: regexp.MustCompile(defaultAllowedRequests), http.MethodGet: regexp.MustCompile(defaultAllowedRequest),
}, },
} }
applicatorURLRegex = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9_.-]+)\(((?:(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS)(?:,(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS))*)?)\):(.*)$`) applicatorURLRegex = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9_.-]+)\(((?:(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS)(?:,(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS))*)?)\):(.*)$`)
@ -132,40 +149,64 @@ func Serve(group *errgroup.Group) *cli.Command {
func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug().Str("remote_addr", r.RemoteAddr).Str("method", r.Method).Str("path", r.URL.Path).Msg("incoming request") log.Debug().Str("remote_addr", r.RemoteAddr).Str("method", r.Method).Str("path", r.URL.Path).Msg("incoming request")
if mr, ok := containerMethodRegex["*"]; ok { mr, ok := containerMethodRegex["*"]
if code := ph.checkMethodAndRegex(mr, r, ""); code != http.StatusOK { if ok {
http.Error(w, http.StatusText(code), code) ph.checkMethodAndRegex(w, r, mr)
/*
var req *regexp.Regexp
req, ok = mr[r.Method]
if !ok {
logDeniedRequest(r, http.StatusMethodNotAllowed, "this HTTP method is not in the list of those authorized for this container")
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return return
} }
ph.frp.ServeHTTP(w, r) if !req.MatchString(r.URL.Path) {
logDeniedRequest(r, http.StatusForbidden, "this path does not match any regular expression for this HTTP method")
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return return
} }
host, _, _ := net.SplitHostPort(r.RemoteAddr) logAuthorizedRequest(r, "", "incoming request matches a registered regular expression")
for containerName, mr := range containerMethodRegex { ph.rp.ServeHTTP(w, r)
if ph.isContainerAuthorized(containerName, host) { return
if code := ph.checkMethodAndRegex(mr, r, containerName); code != http.StatusOK { */
http.Error(w, http.StatusText(code), code) }
var (
containerName string
host, _, _ = net.SplitHostPort(r.RemoteAddr)
)
for containerName, mr = range containerMethodRegex {
resolvedIPs, err := net.LookupIP(containerName)
if err != nil {
continue
}
for _, resolvedIP := range resolvedIPs {
if resolvedIP.Equal(net.ParseIP(host)) {
ph.checkMethodAndRegex(w, r, mr)
/*
var req *regexp.Regexp
req, ok = mr[r.Method]
if !ok {
logDeniedRequest(r, http.StatusMethodNotAllowed, "this HTTP method is not in the list of those authorized for this container")
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return return
} }
ph.frp.ServeHTTP(w, r) if !req.MatchString(r.URL.Path) {
logDeniedRequest(r, http.StatusForbidden, "this path does not match any regular expression for this HTTP method")
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return return
} }
logAuthorizedRequest(r, containerName, "incoming request matches a registered regular expression")
ph.rp.ServeHTTP(w, r)
return
*/
}
}
} }
logDeniedRequest(r, http.StatusUnauthorized, "this container is not on the list of authorized ones") logDeniedRequest(r, http.StatusUnauthorized, "this container is not on the list of authorized ones")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
} return
func (ph *ProxyHandler) isContainerAuthorized(containerName, host string) bool {
resolvedIPs, err := net.LookupIP(containerName)
if err != nil {
return false
}
for resolvedIP := range slices.Values(resolvedIPs) {
if resolvedIP.Equal(net.ParseIP(host)) {
return true
}
}
return false
} }
func logDeniedRequest(r *http.Request, statusCode int, message string) { func logDeniedRequest(r *http.Request, statusCode int, message string) {
@ -188,18 +229,30 @@ func logAuthorizedRequest(r *http.Request, containerName, message string) {
l.Msg(message) l.Msg(message)
} }
func (ph *ProxyHandler) checkMethodAndRegex(mr methodRegex, r *http.Request, containerName string) int { func (ph *ProxyHandler) checkMethodAndRegex(w http.ResponseWriter, r *http.Request, mr methodRegex) {
req, ok := mr[r.Method] req, ok := mr[r.Method]
if !ok { if !ok {
logDeniedRequest(r, http.StatusMethodNotAllowed, "this HTTP method is not in the list of those authorized for this container") logDeniedRequest(r, http.StatusMethodNotAllowed, "this HTTP method is not in the list of those authorized for this container")
return http.StatusMethodNotAllowed http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
} }
if !req.MatchString(r.URL.Path) { if !req.MatchString(r.URL.Path) {
logDeniedRequest(r, http.StatusForbidden, "this path does not match any regular expression for this HTTP method") logDeniedRequest(r, http.StatusForbidden, "this path does not match any regular expression for this HTTP method")
return http.StatusForbidden http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
} }
logAuthorizedRequest(r, containerName, "incoming request matches a registered regular expression") logAuthorizedRequest(r, "", "incoming request matches a registered regular expression")
return http.StatusOK /*
log.Info().
Str("remote_addr", r.RemoteAddr).
Str("method", r.Method).
Str("path", r.URL.Path).
Int("status_code", http.StatusOK).
Str("status_text", http.StatusText(http.StatusOK)).
Msg("incoming request matches a registered regular expression")
*/
ph.rp.ServeHTTP(w, r)
return
} }
// action is executed when the ServeCmd command is called. // action is executed when the ServeCmd command is called.
@ -217,11 +270,15 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
return err return err
} }
dummyURL, _ := url.Parse("http://dummy") dummyURL, _ := url.Parse("http://dummy")
srv := &http.Server{ // #nosec: G112 rp := httputil.NewSingleHostReverseProxy(dummyURL)
Handler: &ProxyHandler{ rp.Transport = &http.Transport{
frp: fastrp.NewRP("unix", command.String(dockerSocketPathFlagName), dummyURL), DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) {
return net.Dial("unix", command.String(dockerSocketPathFlagName))
}, },
} }
srv := &http.Server{ // #nosec: G112
Handler: &ProxyHandler{rp: rp},
}
s.group.Go(func() error { s.group.Go(func() error {
if err = srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) { if err = srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err return err
@ -419,7 +476,7 @@ func addRequests() cli.Flag {
Usage: "add requests", Usage: "add requests",
Sources: middleman.PluckEnvVar(EnvVarPrefix, addRequestsFlagName), Sources: middleman.PluckEnvVar(EnvVarPrefix, addRequestsFlagName),
// Local: true, // Required to trigger the Action when this flag is set via the environment variable, see https://github.com/urfave/cli/issues/2041. // Local: true, // Required to trigger the Action when this flag is set via the environment variable, see https://github.com/urfave/cli/issues/2041.
Value: []string{"*:" + defaultAllowedRequests}, Value: []string{"*:" + defaultAllowedRequest},
Aliases: middleman.PluckAlias(ServeCmd, addRequestsFlagName), Aliases: middleman.PluckAlias(ServeCmd, addRequestsFlagName),
Action: func(ctx context.Context, command *cli.Command, requests []string) error { Action: func(ctx context.Context, command *cli.Command, requests []string) error {
clear(containerMethodRegex) clear(containerMethodRegex)

View File

@ -1,41 +0,0 @@
package fastrp
import (
"context"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
)
type FastRP struct {
rp *httputil.ReverseProxy
connPool *sync.Pool
}
func NewRP(network, address string, url *url.URL) *FastRP {
roundTripper := &http.Transport{
DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) {
return net.Dial(network, address)
},
MaxIdleConns: 10,
}
frp := &FastRP{
rp: httputil.NewSingleHostReverseProxy(url),
connPool: &sync.Pool{
New: func() any {
return roundTripper.Clone()
},
},
}
frp.rp.Transport = frp.connPool.Get().(http.RoundTripper)
return frp
}
func (frp *FastRP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
roundTripper := frp.connPool.Get().(http.RoundTripper)
defer frp.connPool.Put(roundTripper)
frp.rp.Transport = roundTripper
frp.rp.ServeHTTP(w, r)
}