mirror of
https://github.com/yusing/godoxy.git
synced 2025-07-26 21:53:16 +02:00
feature: accesslogger
This commit is contained in:
parent
34858a1ba0
commit
00f60a6e78
23 changed files with 1116 additions and 71 deletions
|
@ -174,6 +174,7 @@ func (cfg *Config) load() E.Error {
|
||||||
// errors are non fatal below
|
// errors are non fatal below
|
||||||
errs := E.NewBuilder(errMsg)
|
errs := E.NewBuilder(errMsg)
|
||||||
errs.Add(entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
|
errs.Add(entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
|
||||||
|
errs.Add(entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
||||||
errs.Add(cfg.initNotification(model.Providers.Notification))
|
errs.Add(cfg.initNotification(model.Providers.Notification))
|
||||||
errs.Add(cfg.initAutoCert(&model.AutoCert))
|
errs.Add(cfg.initAutoCert(&model.AutoCert))
|
||||||
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
|
import "github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Config struct {
|
Config struct {
|
||||||
AutoCert AutoCertConfig `json:"autocert" yaml:",flow"`
|
AutoCert AutoCertConfig `json:"autocert" yaml:",flow"`
|
||||||
|
@ -16,6 +18,7 @@ type (
|
||||||
}
|
}
|
||||||
Entrypoint struct {
|
Entrypoint struct {
|
||||||
Middlewares []map[string]any `json:"middlewares" yaml:"middlewares"`
|
Middlewares []map[string]any `json:"middlewares" yaml:"middlewares"`
|
||||||
|
AccessLog *accesslog.Config `json:"access_log" yaml:"access_log"`
|
||||||
}
|
}
|
||||||
NotificationConfig map[string]any
|
NotificationConfig map[string]any
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,7 +39,7 @@ const (
|
||||||
// TODO: support stream
|
// TODO: support stream
|
||||||
|
|
||||||
func newWaker(providerSubTask *task.Task, entry route.Entry, rp *gphttp.ReverseProxy, stream net.Stream) (Waker, E.Error) {
|
func newWaker(providerSubTask *task.Task, entry route.Entry, rp *gphttp.ReverseProxy, stream net.Stream) (Waker, E.Error) {
|
||||||
hcCfg := entry.HealthCheckConfig()
|
hcCfg := entry.RawEntry().HealthCheck
|
||||||
hcCfg.Timeout = idleWakerCheckTimeout
|
hcCfg.Timeout = idleWakerCheckTimeout
|
||||||
|
|
||||||
waker := &waker{
|
waker := &waker{
|
||||||
|
|
|
@ -7,10 +7,13 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/middleware/errorpage"
|
"github.com/yusing/go-proxy/internal/net/http/middleware/errorpage"
|
||||||
"github.com/yusing/go-proxy/internal/route/routes"
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
var findRouteFunc = findRouteAnyDomain
|
var findRouteFunc = findRouteAnyDomain
|
||||||
|
@ -18,6 +21,9 @@ var findRouteFunc = findRouteAnyDomain
|
||||||
var (
|
var (
|
||||||
epMiddleware *middleware.Middleware
|
epMiddleware *middleware.Middleware
|
||||||
epMiddlewareMu sync.Mutex
|
epMiddlewareMu sync.Mutex
|
||||||
|
|
||||||
|
epAccessLogger *accesslog.AccessLogger
|
||||||
|
epAccessLoggerMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetFindRouteDomains(domains []string) {
|
func SetFindRouteDomains(domains []string) {
|
||||||
|
@ -47,6 +53,23 @@ func SetMiddlewares(mws []map[string]any) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetAccessLogger(parent *task.Task, cfg *accesslog.Config) (err error) {
|
||||||
|
epAccessLoggerMu.Lock()
|
||||||
|
defer epAccessLoggerMu.Unlock()
|
||||||
|
|
||||||
|
if cfg == nil {
|
||||||
|
epAccessLogger = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
epAccessLogger, err = accesslog.NewFileAccessLogger(parent, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debug().Msg("entrypoint access logger created")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func Handler(w http.ResponseWriter, r *http.Request) {
|
func Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
mux, err := findRouteFunc(r.Host)
|
mux, err := findRouteFunc(r.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -58,6 +81,16 @@ func Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if epAccessLogger != nil {
|
||||||
|
epMiddlewareMu.Lock()
|
||||||
|
if epAccessLogger != nil {
|
||||||
|
w = gphttp.NewModifyResponseWriter(w, r, func(resp *http.Response) error {
|
||||||
|
epAccessLogger.Log(r, resp)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
epMiddlewareMu.Unlock()
|
||||||
|
}
|
||||||
if epMiddleware != nil {
|
if epMiddleware != nil {
|
||||||
epMiddlewareMu.Lock()
|
epMiddlewareMu.Lock()
|
||||||
if epMiddleware != nil {
|
if epMiddleware != nil {
|
||||||
|
|
133
internal/net/http/accesslog/access_logger.go
Normal file
133
internal/net/http/accesslog/access_logger.go
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
AccessLogger struct {
|
||||||
|
parent *task.Task
|
||||||
|
buf chan []byte
|
||||||
|
cfg *Config
|
||||||
|
w io.WriteCloser
|
||||||
|
Formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
Formatter interface {
|
||||||
|
// Format writes a log line to line without a trailing newline
|
||||||
|
Format(line *bytes.Buffer, req *http.Request, res *http.Response)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = logging.With().Str("module", "accesslog").Logger()
|
||||||
|
|
||||||
|
var TestTimeNow = time.Now().Format(logTimeFormat)
|
||||||
|
|
||||||
|
const logTimeFormat = "02/Jan/2006:15:04:05 -0700"
|
||||||
|
|
||||||
|
func NewFileAccessLogger(parent *task.Task, cfg *Config) (*AccessLogger, error) {
|
||||||
|
f, err := os.OpenFile(cfg.Path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewAccessLogger(parent, f, cfg), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccessLogger(parent *task.Task, w io.WriteCloser, cfg *Config) *AccessLogger {
|
||||||
|
l := &AccessLogger{
|
||||||
|
parent: parent,
|
||||||
|
cfg: cfg,
|
||||||
|
w: w,
|
||||||
|
}
|
||||||
|
fmt := CommonFormatter{cfg: &l.cfg.Fields}
|
||||||
|
switch l.cfg.Format {
|
||||||
|
case FormatCommon:
|
||||||
|
l.Formatter = fmt
|
||||||
|
case FormatCombined:
|
||||||
|
l.Formatter = CombinedFormatter{CommonFormatter: fmt}
|
||||||
|
case FormatJSON:
|
||||||
|
l.Formatter = JSONFormatter{CommonFormatter: fmt}
|
||||||
|
}
|
||||||
|
if cfg.BufferSize == 0 {
|
||||||
|
cfg.BufferSize = DefaultBufferSize
|
||||||
|
}
|
||||||
|
l.buf = make(chan []byte, cfg.BufferSize)
|
||||||
|
go l.start()
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeNow() string {
|
||||||
|
if !common.IsTest {
|
||||||
|
return time.Now().Format(logTimeFormat)
|
||||||
|
}
|
||||||
|
return TestTimeNow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AccessLogger) checkKeep(req *http.Request, res *http.Response) bool {
|
||||||
|
if !l.cfg.Filters.StatusCodes.CheckKeep(req, res) ||
|
||||||
|
!l.cfg.Filters.Method.CheckKeep(req, res) ||
|
||||||
|
!l.cfg.Filters.Headers.CheckKeep(req, res) ||
|
||||||
|
!l.cfg.Filters.CIDR.CheckKeep(req, res) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AccessLogger) Log(req *http.Request, res *http.Response) {
|
||||||
|
if !l.checkKeep(req, res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var line bytes.Buffer
|
||||||
|
l.Format(&line, req, res)
|
||||||
|
line.WriteRune('\n')
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-l.parent.Context().Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
l.buf <- line.Bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AccessLogger) LogError(req *http.Request, err error) {
|
||||||
|
l.Log(req, &http.Response{StatusCode: http.StatusInternalServerError, Status: err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AccessLogger) close() {
|
||||||
|
close(l.buf)
|
||||||
|
l.w.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AccessLogger) handleErr(err error) {
|
||||||
|
E.LogError("failed to write access log", err, &logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AccessLogger) start() {
|
||||||
|
task := l.parent.Subtask("access log flusher")
|
||||||
|
defer task.Finish("done")
|
||||||
|
defer l.close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-task.Context().Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
for line := range l.buf {
|
||||||
|
_, err := l.w.Write(line)
|
||||||
|
if err != nil {
|
||||||
|
l.handleErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
internal/net/http/accesslog/access_logger_test.go
Normal file
132
internal/net/http/accesslog/access_logger_test.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package accesslog_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
. "github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
|
taskPkg "github.com/yusing/go-proxy/internal/task"
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testWritter struct {
|
||||||
|
line string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *testWritter) Write(p []byte) (n int, err error) {
|
||||||
|
w.line = string(p)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *testWritter) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var tw testWritter
|
||||||
|
|
||||||
|
const (
|
||||||
|
remote = "192.168.1.1"
|
||||||
|
u = "http://example.com/?bar=baz&foo=bar"
|
||||||
|
uRedacted = "http://example.com/?bar=" + RedactedValue + "&foo=" + RedactedValue
|
||||||
|
referer = "https://www.google.com/"
|
||||||
|
proto = "HTTP/1.1"
|
||||||
|
ua = "Go-http-client/1.1"
|
||||||
|
status = http.StatusOK
|
||||||
|
contentLength = 100
|
||||||
|
method = http.MethodGet
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testURL = E.Must(url.Parse(u))
|
||||||
|
req = &http.Request{
|
||||||
|
RemoteAddr: remote,
|
||||||
|
Method: method,
|
||||||
|
Proto: proto,
|
||||||
|
Host: testURL.Host,
|
||||||
|
URL: testURL,
|
||||||
|
Header: http.Header{
|
||||||
|
"User-Agent": []string{ua},
|
||||||
|
"Referer": []string{referer},
|
||||||
|
"Cookie": []string{
|
||||||
|
"foo=bar",
|
||||||
|
"bar=baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp = &http.Response{
|
||||||
|
StatusCode: status,
|
||||||
|
ContentLength: contentLength,
|
||||||
|
Header: http.Header{"Content-Type": []string{"text/plain"}},
|
||||||
|
}
|
||||||
|
task = taskPkg.GlobalTask("test logger")
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccessLoggerCommon(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
config.Format = FormatCommon
|
||||||
|
logger := NewAccessLogger(task, &tw, &config)
|
||||||
|
logger.Log(req, resp)
|
||||||
|
ExpectEqual(t, tw.line,
|
||||||
|
fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %d\n",
|
||||||
|
remote, TestTimeNow, method, u, proto, status, contentLength,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLoggerCombined(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
config.Format = FormatCombined
|
||||||
|
logger := NewAccessLogger(task, &tw, &config)
|
||||||
|
logger.Log(req, resp)
|
||||||
|
ExpectEqual(t, tw.line,
|
||||||
|
fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\"\n",
|
||||||
|
remote, TestTimeNow, method, u, proto, status, contentLength, referer, ua,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLoggerRedactQuery(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
config.Format = FormatCommon
|
||||||
|
config.Fields.Query.DefaultMode = FieldModeRedact
|
||||||
|
logger := NewAccessLogger(task, &tw, &config)
|
||||||
|
logger.Log(req, resp)
|
||||||
|
ExpectEqual(t, tw.line,
|
||||||
|
fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %d\n",
|
||||||
|
remote, TestTimeNow, method, uRedacted, proto, status, contentLength,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJSONEntry(t *testing.T, config *Config) JSONLogEntry {
|
||||||
|
t.Helper()
|
||||||
|
config.Format = FormatJSON
|
||||||
|
logger := NewAccessLogger(task, &tw, config)
|
||||||
|
logger.Log(req, resp)
|
||||||
|
var entry JSONLogEntry
|
||||||
|
err := json.Unmarshal([]byte(tw.line), &entry)
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLoggerJSON(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
entry := getJSONEntry(t, &config)
|
||||||
|
ExpectEqual(t, entry.IP, remote)
|
||||||
|
ExpectEqual(t, entry.Method, method)
|
||||||
|
ExpectEqual(t, entry.Scheme, "http")
|
||||||
|
ExpectEqual(t, entry.Host, testURL.Host)
|
||||||
|
ExpectEqual(t, entry.URI, testURL.RequestURI())
|
||||||
|
ExpectEqual(t, entry.Protocol, proto)
|
||||||
|
ExpectEqual(t, entry.Status, status)
|
||||||
|
ExpectEqual(t, entry.ContentType, "text/plain")
|
||||||
|
ExpectEqual(t, entry.Size, contentLength)
|
||||||
|
ExpectEqual(t, entry.Referer, referer)
|
||||||
|
ExpectEqual(t, entry.UserAgent, ua)
|
||||||
|
ExpectEqual(t, len(entry.Headers), 0)
|
||||||
|
ExpectEqual(t, len(entry.Cookies), 0)
|
||||||
|
}
|
47
internal/net/http/accesslog/config.go
Normal file
47
internal/net/http/accesslog/config.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
type (
|
||||||
|
Format string
|
||||||
|
Filters struct {
|
||||||
|
StatusCodes LogFilter[*StatusCodeRange]
|
||||||
|
Method LogFilter[HTTPMethod]
|
||||||
|
Headers LogFilter[*HTTPHeader] // header exists or header == value
|
||||||
|
CIDR LogFilter[*CIDR]
|
||||||
|
}
|
||||||
|
Fields struct {
|
||||||
|
Headers FieldConfig
|
||||||
|
Query FieldConfig
|
||||||
|
Cookies FieldConfig
|
||||||
|
}
|
||||||
|
Config struct {
|
||||||
|
BufferSize uint
|
||||||
|
Format Format `validate:"oneof=common combined json"`
|
||||||
|
Path string `validate:"required"`
|
||||||
|
Filters Filters
|
||||||
|
Fields Fields
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
FormatCommon Format = "common"
|
||||||
|
FormatCombined Format = "combined"
|
||||||
|
FormatJSON Format = "json"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultBufferSize = 100
|
||||||
|
|
||||||
|
var DefaultConfig = Config{
|
||||||
|
BufferSize: DefaultBufferSize,
|
||||||
|
Format: FormatCombined,
|
||||||
|
Fields: Fields{
|
||||||
|
Headers: FieldConfig{
|
||||||
|
DefaultMode: FieldModeDrop,
|
||||||
|
},
|
||||||
|
Query: FieldConfig{
|
||||||
|
DefaultMode: FieldModeKeep,
|
||||||
|
},
|
||||||
|
Cookies: FieldConfig{
|
||||||
|
DefaultMode: FieldModeDrop,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
53
internal/net/http/accesslog/config_test.go
Normal file
53
internal/net/http/accesslog/config_test.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package accesslog_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/docker"
|
||||||
|
. "github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewConfig(t *testing.T) {
|
||||||
|
labels := map[string]string{
|
||||||
|
"proxy.buffer_size": "10",
|
||||||
|
"proxy.format": "combined",
|
||||||
|
"proxy.file_path": "/tmp/access.log",
|
||||||
|
"proxy.filters.status_codes.values": "200-299",
|
||||||
|
"proxy.filters.method.values": "GET, POST",
|
||||||
|
"proxy.filters.headers.values": "foo=bar, baz",
|
||||||
|
"proxy.filters.headers.negative": "true",
|
||||||
|
"proxy.filters.cidr.values": "192.168.10.0/24",
|
||||||
|
"proxy.fields.headers.default_mode": "keep",
|
||||||
|
"proxy.fields.headers.config.foo": "redact",
|
||||||
|
"proxy.fields.query.default_mode": "drop",
|
||||||
|
"proxy.fields.query.config.foo": "keep",
|
||||||
|
"proxy.fields.cookies.default_mode": "redact",
|
||||||
|
"proxy.fields.cookies.config.foo": "keep",
|
||||||
|
}
|
||||||
|
parsed, err := docker.ParseLabels(labels)
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
err = utils.Deserialize(parsed, &config)
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
|
||||||
|
ExpectEqual(t, config.BufferSize, 10)
|
||||||
|
ExpectEqual(t, config.Format, FormatCombined)
|
||||||
|
ExpectEqual(t, config.Path, "/tmp/access.log")
|
||||||
|
ExpectDeepEqual(t, config.Filters.StatusCodes.Values, []*StatusCodeRange{{Start: 200, End: 299}})
|
||||||
|
ExpectEqual(t, len(config.Filters.Method.Values), 2)
|
||||||
|
ExpectDeepEqual(t, config.Filters.Method.Values, []HTTPMethod{"GET", "POST"})
|
||||||
|
ExpectEqual(t, len(config.Filters.Headers.Values), 2)
|
||||||
|
ExpectDeepEqual(t, config.Filters.Headers.Values, []*HTTPHeader{{Key: "foo", Value: "bar"}, {Key: "baz", Value: ""}})
|
||||||
|
ExpectTrue(t, config.Filters.Headers.Negative)
|
||||||
|
ExpectEqual(t, len(config.Filters.CIDR.Values), 1)
|
||||||
|
ExpectEqual(t, config.Filters.CIDR.Values[0].String(), "192.168.10.0/24")
|
||||||
|
ExpectEqual(t, config.Fields.Headers.DefaultMode, FieldModeKeep)
|
||||||
|
ExpectEqual(t, config.Fields.Headers.Config["foo"], FieldModeRedact)
|
||||||
|
ExpectEqual(t, config.Fields.Query.DefaultMode, FieldModeDrop)
|
||||||
|
ExpectEqual(t, config.Fields.Query.Config["foo"], FieldModeKeep)
|
||||||
|
ExpectEqual(t, config.Fields.Cookies.DefaultMode, FieldModeRedact)
|
||||||
|
ExpectEqual(t, config.Fields.Cookies.Config["foo"], FieldModeKeep)
|
||||||
|
}
|
103
internal/net/http/accesslog/fields.go
Normal file
103
internal/net/http/accesslog/fields.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
FieldConfig struct {
|
||||||
|
DefaultMode FieldMode `validate:"oneof=keep drop redact"`
|
||||||
|
Config map[string]FieldMode `validate:"dive,oneof=keep drop redact"`
|
||||||
|
}
|
||||||
|
FieldMode string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FieldModeKeep FieldMode = "keep"
|
||||||
|
FieldModeDrop FieldMode = "drop"
|
||||||
|
FieldModeRedact FieldMode = "redact"
|
||||||
|
|
||||||
|
RedactedValue = "REDACTED"
|
||||||
|
)
|
||||||
|
|
||||||
|
func processMap[V any](cfg *FieldConfig, m map[string]V, redactedV V) map[string]V {
|
||||||
|
if len(cfg.Config) == 0 {
|
||||||
|
switch cfg.DefaultMode {
|
||||||
|
case FieldModeKeep:
|
||||||
|
return m
|
||||||
|
case FieldModeDrop:
|
||||||
|
return nil
|
||||||
|
case FieldModeRedact:
|
||||||
|
redacted := make(map[string]V)
|
||||||
|
for k := range m {
|
||||||
|
redacted[k] = redactedV
|
||||||
|
}
|
||||||
|
return redacted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m) == 0 {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
newMap := make(map[string]V)
|
||||||
|
for k := range m {
|
||||||
|
var mode FieldMode
|
||||||
|
var ok bool
|
||||||
|
if mode, ok = cfg.Config[k]; !ok {
|
||||||
|
mode = cfg.DefaultMode
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case FieldModeKeep:
|
||||||
|
newMap[k] = m[k]
|
||||||
|
case FieldModeRedact:
|
||||||
|
newMap[k] = redactedV
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func processSlice[V any, VReturn any](cfg *FieldConfig, s []V, getKey func(V) string, convert func(V) VReturn, redact func(V) VReturn) map[string]VReturn {
|
||||||
|
if len(s) == 0 ||
|
||||||
|
len(cfg.Config) == 0 && cfg.DefaultMode == FieldModeDrop {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
newMap := make(map[string]VReturn, len(s))
|
||||||
|
for _, v := range s {
|
||||||
|
var mode FieldMode
|
||||||
|
var ok bool
|
||||||
|
k := getKey(v)
|
||||||
|
if mode, ok = cfg.Config[k]; !ok {
|
||||||
|
mode = cfg.DefaultMode
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case FieldModeKeep:
|
||||||
|
newMap[k] = convert(v)
|
||||||
|
case FieldModeRedact:
|
||||||
|
newMap[k] = redact(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *FieldConfig) ProcessHeaders(headers http.Header) http.Header {
|
||||||
|
return processMap(cfg, headers, []string{RedactedValue})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *FieldConfig) ProcessQuery(q url.Values) url.Values {
|
||||||
|
return processMap(cfg, q, []string{RedactedValue})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *FieldConfig) ProcessCookies(cookies []*http.Cookie) map[string]string {
|
||||||
|
return processSlice(cfg, cookies,
|
||||||
|
func(c *http.Cookie) string {
|
||||||
|
return c.Name
|
||||||
|
},
|
||||||
|
func(c *http.Cookie) string {
|
||||||
|
return c.Value
|
||||||
|
},
|
||||||
|
func(c *http.Cookie) string {
|
||||||
|
return RedactedValue
|
||||||
|
})
|
||||||
|
}
|
72
internal/net/http/accesslog/fields_test.go
Normal file
72
internal/net/http/accesslog/fields_test.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package accesslog_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cookie header should be removed,
|
||||||
|
// stored in JSONLogEntry.Cookies instead.
|
||||||
|
func TestAccessLoggerJSONKeepHeaders(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
config.Fields.Headers.DefaultMode = FieldModeKeep
|
||||||
|
entry := getJSONEntry(t, &config)
|
||||||
|
ExpectDeepEqual(t, len(entry.Headers["Cookie"]), 0)
|
||||||
|
for k, v := range req.Header {
|
||||||
|
if k != "Cookie" {
|
||||||
|
ExpectDeepEqual(t, entry.Headers[k], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLoggerJSONRedactHeaders(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
config.Fields.Headers.DefaultMode = FieldModeRedact
|
||||||
|
entry := getJSONEntry(t, &config)
|
||||||
|
ExpectDeepEqual(t, len(entry.Headers["Cookie"]), 0)
|
||||||
|
for k := range req.Header {
|
||||||
|
if k != "Cookie" {
|
||||||
|
ExpectDeepEqual(t, entry.Headers[k], []string{RedactedValue})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLoggerJSONKeepCookies(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
config.Fields.Headers.DefaultMode = FieldModeKeep
|
||||||
|
config.Fields.Cookies.DefaultMode = FieldModeKeep
|
||||||
|
entry := getJSONEntry(t, &config)
|
||||||
|
ExpectDeepEqual(t, len(entry.Headers["Cookie"]), 0)
|
||||||
|
for _, cookie := range req.Cookies() {
|
||||||
|
ExpectEqual(t, entry.Cookies[cookie.Name], cookie.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLoggerJSONRedactCookies(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
config.Fields.Headers.DefaultMode = FieldModeKeep
|
||||||
|
config.Fields.Cookies.DefaultMode = FieldModeRedact
|
||||||
|
entry := getJSONEntry(t, &config)
|
||||||
|
ExpectDeepEqual(t, len(entry.Headers["Cookie"]), 0)
|
||||||
|
for _, cookie := range req.Cookies() {
|
||||||
|
ExpectEqual(t, entry.Cookies[cookie.Name], RedactedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLoggerJSONDropQuery(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
config.Fields.Query.DefaultMode = FieldModeDrop
|
||||||
|
entry := getJSONEntry(t, &config)
|
||||||
|
ExpectDeepEqual(t, entry.Query["foo"], nil)
|
||||||
|
ExpectDeepEqual(t, entry.Query["bar"], nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccessLoggerJSONRedactQuery(t *testing.T) {
|
||||||
|
config := DefaultConfig
|
||||||
|
config.Fields.Query.DefaultMode = FieldModeRedact
|
||||||
|
entry := getJSONEntry(t, &config)
|
||||||
|
ExpectDeepEqual(t, entry.Query["foo"], []string{RedactedValue})
|
||||||
|
ExpectDeepEqual(t, entry.Query["bar"], []string{RedactedValue})
|
||||||
|
}
|
102
internal/net/http/accesslog/filter.go
Normal file
102
internal/net/http/accesslog/filter.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
LogFilter[T Filterable] struct {
|
||||||
|
Negative bool
|
||||||
|
Values []T
|
||||||
|
}
|
||||||
|
Filterable interface {
|
||||||
|
comparable
|
||||||
|
Fulfill(req *http.Request, res *http.Response) bool
|
||||||
|
}
|
||||||
|
HTTPMethod string
|
||||||
|
HTTPHeader struct {
|
||||||
|
Key, Value string
|
||||||
|
}
|
||||||
|
CIDR struct {
|
||||||
|
*net.IPNet
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidHTTPHeaderFilter = E.New("invalid http header filter")
|
||||||
|
|
||||||
|
func (f *LogFilter[T]) CheckKeep(req *http.Request, res *http.Response) bool {
|
||||||
|
if len(f.Values) == 0 {
|
||||||
|
return !f.Negative
|
||||||
|
}
|
||||||
|
for _, check := range f.Values {
|
||||||
|
if check.Fulfill(req, res) {
|
||||||
|
return !f.Negative
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f.Negative
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusCodeRange) Fulfill(req *http.Request, res *http.Response) bool {
|
||||||
|
return r.Includes(res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (method HTTPMethod) Fulfill(req *http.Request, res *http.Response) bool {
|
||||||
|
return req.Method == string(method)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *HTTPHeader) Parse(v string) error {
|
||||||
|
split := strings.Split(v, "=")
|
||||||
|
switch len(split) {
|
||||||
|
case 1:
|
||||||
|
split = append(split, "")
|
||||||
|
case 2:
|
||||||
|
default:
|
||||||
|
return ErrInvalidHTTPHeaderFilter.Subject(v)
|
||||||
|
}
|
||||||
|
k.Key = split[0]
|
||||||
|
k.Value = split[1]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *HTTPHeader) Fulfill(req *http.Request, res *http.Response) bool {
|
||||||
|
wanted := k.Value
|
||||||
|
// non canonical key matching
|
||||||
|
got, ok := req.Header[k.Key]
|
||||||
|
if wanted == "" {
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, v := range got {
|
||||||
|
if strings.EqualFold(v, wanted) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cidr *CIDR) Parse(v string) error {
|
||||||
|
_, ipnet, err := net.ParseCIDR(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cidr.IPNet = ipnet
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cidr *CIDR) Fulfill(req *http.Request, res *http.Response) bool {
|
||||||
|
ip, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
ip = req.RemoteAddr
|
||||||
|
}
|
||||||
|
netIP := net.ParseIP(ip)
|
||||||
|
if netIP == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return cidr.Contains(netIP)
|
||||||
|
}
|
188
internal/net/http/accesslog/filter_test.go
Normal file
188
internal/net/http/accesslog/filter_test.go
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
package accesslog_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatusCodeFilter(t *testing.T) {
|
||||||
|
values := []*StatusCodeRange{
|
||||||
|
strutils.MustParse[*StatusCodeRange]("200-308"),
|
||||||
|
}
|
||||||
|
t.Run("positive", func(t *testing.T) {
|
||||||
|
filter := &LogFilter[*StatusCodeRange]{}
|
||||||
|
ExpectTrue(t, filter.CheckKeep(nil, nil))
|
||||||
|
|
||||||
|
// keep any 2xx 3xx (inclusive)
|
||||||
|
filter.Values = values
|
||||||
|
ExpectFalse(t, filter.CheckKeep(nil, &http.Response{
|
||||||
|
StatusCode: http.StatusForbidden,
|
||||||
|
}))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(nil, &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
}))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(nil, &http.Response{
|
||||||
|
StatusCode: http.StatusMultipleChoices,
|
||||||
|
}))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(nil, &http.Response{
|
||||||
|
StatusCode: http.StatusPermanentRedirect,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative", func(t *testing.T) {
|
||||||
|
filter := &LogFilter[*StatusCodeRange]{
|
||||||
|
Negative: true,
|
||||||
|
}
|
||||||
|
ExpectFalse(t, filter.CheckKeep(nil, nil))
|
||||||
|
|
||||||
|
// drop any 2xx 3xx (inclusive)
|
||||||
|
filter.Values = values
|
||||||
|
ExpectTrue(t, filter.CheckKeep(nil, &http.Response{
|
||||||
|
StatusCode: http.StatusForbidden,
|
||||||
|
}))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(nil, &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
}))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(nil, &http.Response{
|
||||||
|
StatusCode: http.StatusMultipleChoices,
|
||||||
|
}))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(nil, &http.Response{
|
||||||
|
StatusCode: http.StatusPermanentRedirect,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMethodFilter(t *testing.T) {
|
||||||
|
t.Run("positive", func(t *testing.T) {
|
||||||
|
filter := &LogFilter[HTTPMethod]{}
|
||||||
|
ExpectTrue(t, filter.CheckKeep(&http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
}, nil))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(&http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
}, nil))
|
||||||
|
|
||||||
|
// keep get only
|
||||||
|
filter.Values = []HTTPMethod{http.MethodGet}
|
||||||
|
ExpectTrue(t, filter.CheckKeep(&http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
}, nil))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(&http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
}, nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative", func(t *testing.T) {
|
||||||
|
filter := &LogFilter[HTTPMethod]{
|
||||||
|
Negative: true,
|
||||||
|
}
|
||||||
|
ExpectFalse(t, filter.CheckKeep(&http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
}, nil))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(&http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
}, nil))
|
||||||
|
|
||||||
|
// drop post only
|
||||||
|
filter.Values = []HTTPMethod{http.MethodPost}
|
||||||
|
ExpectFalse(t, filter.CheckKeep(&http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
}, nil))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(&http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
}, nil))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderFilter(t *testing.T) {
|
||||||
|
fooBar := &http.Request{
|
||||||
|
Header: http.Header{
|
||||||
|
"Foo": []string{"bar"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fooBaz := &http.Request{
|
||||||
|
Header: http.Header{
|
||||||
|
"Foo": []string{"baz"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
headerFoo := []*HTTPHeader{
|
||||||
|
strutils.MustParse[*HTTPHeader]("Foo"),
|
||||||
|
}
|
||||||
|
ExpectEqual(t, headerFoo[0].Key, "Foo")
|
||||||
|
ExpectEqual(t, headerFoo[0].Value, "")
|
||||||
|
headerFooBar := []*HTTPHeader{
|
||||||
|
strutils.MustParse[*HTTPHeader]("Foo=bar"),
|
||||||
|
}
|
||||||
|
ExpectEqual(t, headerFooBar[0].Key, "Foo")
|
||||||
|
ExpectEqual(t, headerFooBar[0].Value, "bar")
|
||||||
|
|
||||||
|
t.Run("positive", func(t *testing.T) {
|
||||||
|
filter := &LogFilter[*HTTPHeader]{}
|
||||||
|
ExpectTrue(t, filter.CheckKeep(fooBar, nil))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(fooBaz, nil))
|
||||||
|
|
||||||
|
// keep any foo
|
||||||
|
filter.Values = headerFoo
|
||||||
|
ExpectTrue(t, filter.CheckKeep(fooBar, nil))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(fooBaz, nil))
|
||||||
|
|
||||||
|
// keep foo == bar
|
||||||
|
filter.Values = headerFooBar
|
||||||
|
ExpectTrue(t, filter.CheckKeep(fooBar, nil))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(fooBaz, nil))
|
||||||
|
})
|
||||||
|
t.Run("negative", func(t *testing.T) {
|
||||||
|
filter := &LogFilter[*HTTPHeader]{
|
||||||
|
Negative: true,
|
||||||
|
}
|
||||||
|
ExpectFalse(t, filter.CheckKeep(fooBar, nil))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(fooBaz, nil))
|
||||||
|
|
||||||
|
// drop any foo
|
||||||
|
filter.Values = headerFoo
|
||||||
|
ExpectFalse(t, filter.CheckKeep(fooBar, nil))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(fooBaz, nil))
|
||||||
|
|
||||||
|
// drop foo == bar
|
||||||
|
filter.Values = headerFooBar
|
||||||
|
ExpectFalse(t, filter.CheckKeep(fooBar, nil))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(fooBaz, nil))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCIDRFilter(t *testing.T) {
|
||||||
|
cidr := []*CIDR{
|
||||||
|
strutils.MustParse[*CIDR]("192.168.10.0/24"),
|
||||||
|
}
|
||||||
|
ExpectEqual(t, cidr[0].String(), "192.168.10.0/24")
|
||||||
|
inCIDR := &http.Request{
|
||||||
|
RemoteAddr: "192.168.10.1",
|
||||||
|
}
|
||||||
|
notInCIDR := &http.Request{
|
||||||
|
RemoteAddr: "192.168.11.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("positive", func(t *testing.T) {
|
||||||
|
filter := &LogFilter[*CIDR]{}
|
||||||
|
ExpectTrue(t, filter.CheckKeep(inCIDR, nil))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(notInCIDR, nil))
|
||||||
|
|
||||||
|
filter.Values = cidr
|
||||||
|
ExpectTrue(t, filter.CheckKeep(inCIDR, nil))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(notInCIDR, nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative", func(t *testing.T) {
|
||||||
|
filter := &LogFilter[*CIDR]{Negative: true}
|
||||||
|
ExpectFalse(t, filter.CheckKeep(inCIDR, nil))
|
||||||
|
ExpectFalse(t, filter.CheckKeep(notInCIDR, nil))
|
||||||
|
|
||||||
|
filter.Values = cidr
|
||||||
|
ExpectFalse(t, filter.CheckKeep(inCIDR, nil))
|
||||||
|
ExpectTrue(t, filter.CheckKeep(notInCIDR, nil))
|
||||||
|
})
|
||||||
|
}
|
129
internal/net/http/accesslog/formatter.go
Normal file
129
internal/net/http/accesslog/formatter.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
CommonFormatter struct {
|
||||||
|
cfg *Fields
|
||||||
|
}
|
||||||
|
CombinedFormatter struct {
|
||||||
|
CommonFormatter
|
||||||
|
}
|
||||||
|
JSONFormatter struct {
|
||||||
|
CommonFormatter
|
||||||
|
}
|
||||||
|
JSONLogEntry struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
URI string `json:"uri"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
ContentType string `json:"type"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Referer string `json:"referer"`
|
||||||
|
UserAgent string `json:"useragent"`
|
||||||
|
Query map[string][]string `json:"query,omitempty"`
|
||||||
|
Headers map[string][]string `json:"headers,omitempty"`
|
||||||
|
Cookies map[string]string `json:"cookies,omitempty"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func scheme(req *http.Request) string {
|
||||||
|
if req.TLS != nil {
|
||||||
|
return "https"
|
||||||
|
}
|
||||||
|
return "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestURI(u *url.URL, query url.Values) string {
|
||||||
|
uri := u.EscapedPath()
|
||||||
|
if len(query) > 0 {
|
||||||
|
uri += "?" + query.Encode()
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientIP(req *http.Request) string {
|
||||||
|
clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err == nil {
|
||||||
|
return clientIP
|
||||||
|
}
|
||||||
|
return req.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f CommonFormatter) Format(line *bytes.Buffer, req *http.Request, res *http.Response) {
|
||||||
|
query := f.cfg.Query.ProcessQuery(req.URL.Query())
|
||||||
|
|
||||||
|
line.WriteString(req.Host)
|
||||||
|
line.WriteRune(' ')
|
||||||
|
|
||||||
|
line.WriteString(clientIP(req))
|
||||||
|
line.WriteString(" - - [")
|
||||||
|
|
||||||
|
line.WriteString(timeNow())
|
||||||
|
line.WriteString("] \"")
|
||||||
|
|
||||||
|
line.WriteString(req.Method)
|
||||||
|
line.WriteRune(' ')
|
||||||
|
line.WriteString(requestURI(req.URL, query))
|
||||||
|
line.WriteRune(' ')
|
||||||
|
line.WriteString(req.Proto)
|
||||||
|
line.WriteString("\" ")
|
||||||
|
|
||||||
|
line.WriteString(strconv.Itoa(res.StatusCode))
|
||||||
|
line.WriteRune(' ')
|
||||||
|
line.WriteString(strconv.FormatInt(res.ContentLength, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f CombinedFormatter) Format(line *bytes.Buffer, req *http.Request, res *http.Response) {
|
||||||
|
f.CommonFormatter.Format(line, req, res)
|
||||||
|
line.WriteString(" \"")
|
||||||
|
line.WriteString(req.Referer())
|
||||||
|
line.WriteString("\" \"")
|
||||||
|
line.WriteString(req.UserAgent())
|
||||||
|
line.WriteRune('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f JSONFormatter) Format(line *bytes.Buffer, req *http.Request, res *http.Response) {
|
||||||
|
query := f.cfg.Query.ProcessQuery(req.URL.Query())
|
||||||
|
headers := f.cfg.Headers.ProcessHeaders(req.Header)
|
||||||
|
headers.Del("Cookie")
|
||||||
|
cookies := f.cfg.Cookies.ProcessCookies(req.Cookies())
|
||||||
|
|
||||||
|
entry := JSONLogEntry{
|
||||||
|
IP: clientIP(req),
|
||||||
|
Method: req.Method,
|
||||||
|
Scheme: scheme(req),
|
||||||
|
Host: req.Host,
|
||||||
|
URI: requestURI(req.URL, query),
|
||||||
|
Protocol: req.Proto,
|
||||||
|
Status: res.StatusCode,
|
||||||
|
ContentType: res.Header.Get("Content-Type"),
|
||||||
|
Size: res.ContentLength,
|
||||||
|
Referer: req.Referer(),
|
||||||
|
UserAgent: req.UserAgent(),
|
||||||
|
Query: query,
|
||||||
|
Headers: headers,
|
||||||
|
Cookies: cookies,
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
entry.Error = res.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
marshaller := json.NewEncoder(line)
|
||||||
|
err := marshaller.Encode(entry)
|
||||||
|
if err != nil {
|
||||||
|
logger.Err(err).Msg("failed to marshal json log")
|
||||||
|
}
|
||||||
|
}
|
51
internal/net/http/accesslog/status_code_range.go
Normal file
51
internal/net/http/accesslog/status_code_range.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusCodeRange struct {
|
||||||
|
Start int
|
||||||
|
End int
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrInvalidStatusCodeRange = E.New("invalid status code range")
|
||||||
|
|
||||||
|
func (r *StatusCodeRange) Includes(code int) bool {
|
||||||
|
return r.Start <= code && code <= r.End
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusCodeRange) Parse(v string) error {
|
||||||
|
split := strings.Split(v, "-")
|
||||||
|
switch len(split) {
|
||||||
|
case 1:
|
||||||
|
start, err := strconv.Atoi(split[0])
|
||||||
|
if err != nil {
|
||||||
|
return E.From(err)
|
||||||
|
}
|
||||||
|
r.Start = start
|
||||||
|
r.End = start
|
||||||
|
return nil
|
||||||
|
case 2:
|
||||||
|
start, errStart := strconv.Atoi(split[0])
|
||||||
|
end, errEnd := strconv.Atoi(split[1])
|
||||||
|
if err := E.Join(errStart, errEnd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.Start = start
|
||||||
|
r.End = end
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return ErrInvalidStatusCodeRange.Subject(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StatusCodeRange) String() string {
|
||||||
|
if r.Start == r.End {
|
||||||
|
return strconv.Itoa(r.Start)
|
||||||
|
}
|
||||||
|
return strconv.Itoa(r.Start) + "-" + strconv.Itoa(r.End)
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ type (
|
||||||
|
|
||||||
headerSent bool
|
headerSent bool
|
||||||
code int
|
code int
|
||||||
|
size int
|
||||||
|
|
||||||
modifier ModifyResponseFunc
|
modifier ModifyResponseFunc
|
||||||
modified bool
|
modified bool
|
||||||
|
@ -38,6 +39,14 @@ func (w *ModifyResponseWriter) Unwrap() http.ResponseWriter {
|
||||||
return w.w
|
return w.w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *ModifyResponseWriter) StatusCode() int {
|
||||||
|
return w.code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ModifyResponseWriter) Size() int {
|
||||||
|
return w.size
|
||||||
|
}
|
||||||
|
|
||||||
func (w *ModifyResponseWriter) WriteHeader(code int) {
|
func (w *ModifyResponseWriter) WriteHeader(code int) {
|
||||||
if w.headerSent {
|
if w.headerSent {
|
||||||
return
|
return
|
||||||
|
@ -58,12 +67,15 @@ func (w *ModifyResponseWriter) WriteHeader(code int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := http.Response{
|
resp := http.Response{
|
||||||
|
StatusCode: code,
|
||||||
Header: w.w.Header(),
|
Header: w.w.Header(),
|
||||||
Request: w.r,
|
Request: w.r,
|
||||||
|
ContentLength: int64(w.size),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.modifier(&resp); err != nil {
|
if err := w.modifier(&resp); err != nil {
|
||||||
w.modifierErr = fmt.Errorf("response modifier error: %w", err)
|
w.modifierErr = fmt.Errorf("response modifier error: %w", err)
|
||||||
|
resp.Status = w.modifierErr.Error()
|
||||||
w.w.WriteHeader(http.StatusInternalServerError)
|
w.w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -81,7 +93,10 @@ func (w *ModifyResponseWriter) Write(b []byte) (int, error) {
|
||||||
if w.modifierErr != nil {
|
if w.modifierErr != nil {
|
||||||
return 0, w.modifierErr
|
return 0, w.modifierErr
|
||||||
}
|
}
|
||||||
return w.w.Write(b)
|
|
||||||
|
n, err := w.w.Write(b)
|
||||||
|
w.size += n
|
||||||
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack hijacks the connection.
|
// Hijack hijacks the connection.
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/metrics"
|
"github.com/yusing/go-proxy/internal/metrics"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
"github.com/yusing/go-proxy/internal/net/types"
|
"github.com/yusing/go-proxy/internal/net/types"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
"golang.org/x/net/http/httpguts"
|
"golang.org/x/net/http/httpguts"
|
||||||
|
@ -88,6 +89,7 @@ type ReverseProxy struct {
|
||||||
// with its error value. If ErrorHandler is nil, its default
|
// with its error value. If ErrorHandler is nil, its default
|
||||||
// implementation is used.
|
// implementation is used.
|
||||||
ModifyResponse func(*http.Response) error
|
ModifyResponse func(*http.Response) error
|
||||||
|
AccessLogger *accesslog.AccessLogger
|
||||||
|
|
||||||
HandlerFunc http.HandlerFunc
|
HandlerFunc http.HandlerFunc
|
||||||
|
|
||||||
|
@ -245,7 +247,10 @@ func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, r *http.Request, err
|
||||||
logger.Err(err).Str("url", r.URL.String()).Msg("http proxy error")
|
logger.Err(err).Str("url", r.URL.String()).Msg("http proxy error")
|
||||||
}
|
}
|
||||||
if writeHeader {
|
if writeHeader {
|
||||||
rw.WriteHeader(http.StatusBadGateway)
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
if p.AccessLogger != nil {
|
||||||
|
p.AccessLogger.LogError(r, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,37 +276,19 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
visitorIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
visitorIP = req.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
if common.PrometheusEnabled {
|
if common.PrometheusEnabled {
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
var visitor string
|
// req.RemoteAddr had been modified by middleware (if any)
|
||||||
if realIPs := req.Header.Values(HeaderXRealIP); len(realIPs) > 0 {
|
|
||||||
if len(realIPs) == 1 {
|
|
||||||
visitor = realIPs[0]
|
|
||||||
} else {
|
|
||||||
p.Warn().Strs("real_ips", realIPs).
|
|
||||||
Str("remote_addr", req.RemoteAddr).
|
|
||||||
Str("request_url", req.URL.String()).
|
|
||||||
Msg("client sent multiple 'X-Real-IP' values, ignoring.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if visitor == "" {
|
|
||||||
if fwdIPs := req.Header.Values(HeaderXForwardedFor); len(fwdIPs) > 0 {
|
|
||||||
// right-most IP is the visitor
|
|
||||||
visitor = fwdIPs[len(fwdIPs)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if visitor == "" {
|
|
||||||
var err error
|
|
||||||
visitor, _, err = net.SplitHostPort(req.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
visitor = req.RemoteAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lbls := &metrics.HTTPRouteMetricLabels{
|
lbls := &metrics.HTTPRouteMetricLabels{
|
||||||
Service: p.TargetName,
|
Service: p.TargetName,
|
||||||
Method: req.Method,
|
Method: req.Method,
|
||||||
Host: req.Host,
|
Host: req.Host,
|
||||||
Visitor: visitor,
|
Visitor: visitorIP,
|
||||||
Path: req.URL.Path,
|
Path: req.URL.Path,
|
||||||
}
|
}
|
||||||
rw = &httpMetricLogger{
|
rw = &httpMetricLogger{
|
||||||
|
@ -389,18 +376,17 @@ func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
|
||||||
// If we aren't the first proxy retain prior
|
// If we aren't the first proxy retain prior
|
||||||
// X-Forwarded-For information as a comma+space
|
// X-Forwarded-For information as a comma+space
|
||||||
// separated list and fold multiple headers into one.
|
// separated list and fold multiple headers into one.
|
||||||
prior, ok := outreq.Header[HeaderXForwardedFor]
|
prior, ok := outreq.Header[HeaderXForwardedFor]
|
||||||
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
|
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
|
||||||
|
xff := visitorIP
|
||||||
if len(prior) > 0 {
|
if len(prior) > 0 {
|
||||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
xff = strings.Join(prior, ", ") + ", " + xff
|
||||||
}
|
}
|
||||||
if !omit {
|
if !omit {
|
||||||
outreq.Header.Set(HeaderXForwardedFor, clientIP)
|
outreq.Header.Set(HeaderXForwardedFor, xff)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var reqScheme string
|
var reqScheme string
|
||||||
|
@ -465,6 +451,12 @@ func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.AccessLogger != nil {
|
||||||
|
defer func() {
|
||||||
|
p.AccessLogger.Log(req, res)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
|
// Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
|
||||||
if res.StatusCode == http.StatusSwitchingProtocols {
|
if res.StatusCode == http.StatusSwitchingProtocols {
|
||||||
if !p.modifyResponse(rw, res, req, outreq) {
|
if !p.modifyResponse(rw, res, req, outreq) {
|
||||||
|
|
|
@ -43,7 +43,7 @@ func ShouldNotServe(entry Entry) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func UseLoadBalance(entry Entry) bool {
|
func UseLoadBalance(entry Entry) bool {
|
||||||
lb := entry.LoadBalanceConfig()
|
lb := entry.RawEntry().LoadBalance
|
||||||
return lb != nil && lb.Link != ""
|
return lb != nil && lb.Link != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,10 @@ func UseIdleWatcher(entry Entry) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func UseHealthCheck(entry Entry) bool {
|
func UseHealthCheck(entry Entry) bool {
|
||||||
hc := entry.HealthCheckConfig()
|
hc := entry.RawEntry().HealthCheck
|
||||||
return hc != nil && !hc.Disable
|
return hc != nil && !hc.Disable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UseAccessLog(entry Entry) bool {
|
||||||
|
return entry.RawEntry().AccessLog != nil
|
||||||
|
}
|
||||||
|
|
|
@ -7,10 +7,8 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
"github.com/yusing/go-proxy/internal/docker"
|
||||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
|
||||||
net "github.com/yusing/go-proxy/internal/net/types"
|
net "github.com/yusing/go-proxy/internal/net/types"
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReverseProxyEntry struct { // real model after validation
|
type ReverseProxyEntry struct { // real model after validation
|
||||||
|
@ -33,14 +31,6 @@ func (rp *ReverseProxyEntry) RawEntry() *route.RawEntry {
|
||||||
return rp.Raw
|
return rp.Raw
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rp *ReverseProxyEntry) LoadBalanceConfig() *loadbalance.Config {
|
|
||||||
return rp.Raw.LoadBalance
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rp *ReverseProxyEntry) HealthCheckConfig() *health.HealthCheckConfig {
|
|
||||||
return rp.Raw.HealthCheck
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rp *ReverseProxyEntry) IdlewatcherConfig() *idlewatcher.Config {
|
func (rp *ReverseProxyEntry) IdlewatcherConfig() *idlewatcher.Config {
|
||||||
return rp.Idlewatcher
|
return rp.Idlewatcher
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,8 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
"github.com/yusing/go-proxy/internal/docker"
|
||||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
|
||||||
net "github.com/yusing/go-proxy/internal/net/types"
|
net "github.com/yusing/go-proxy/internal/net/types"
|
||||||
route "github.com/yusing/go-proxy/internal/route/types"
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type StreamEntry struct {
|
type StreamEntry struct {
|
||||||
|
@ -36,15 +34,6 @@ func (s *StreamEntry) RawEntry() *route.RawEntry {
|
||||||
return s.Raw
|
return s.Raw
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StreamEntry) LoadBalanceConfig() *loadbalance.Config {
|
|
||||||
// TODO: support stream load balance
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StreamEntry) HealthCheckConfig() *health.HealthCheckConfig {
|
|
||||||
return s.Raw.HealthCheck
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StreamEntry) IdlewatcherConfig() *idlewatcher.Config {
|
func (s *StreamEntry) IdlewatcherConfig() *idlewatcher.Config {
|
||||||
return s.Idlewatcher
|
return s.Idlewatcher
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"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"
|
||||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
||||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
|
@ -105,6 +106,15 @@ func (r *HTTPRoute) Start(providerSubtask *task.Task) E.Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if entry.UseAccessLog(r) {
|
||||||
|
var err error
|
||||||
|
r.rp.AccessLogger, err = accesslog.NewFileAccessLogger(r.task, r.Raw.AccessLog)
|
||||||
|
if err != nil {
|
||||||
|
r.task.Finish(err)
|
||||||
|
return E.From(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if r.handler == nil {
|
if r.handler == nil {
|
||||||
pathPatterns := r.Raw.PathPatterns
|
pathPatterns := r.Raw.PathPatterns
|
||||||
switch {
|
switch {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: support stream load balance
|
||||||
type StreamRoute struct {
|
type StreamRoute struct {
|
||||||
*entry.StreamEntry
|
*entry.StreamEntry
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,12 @@ package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
|
||||||
net "github.com/yusing/go-proxy/internal/net/types"
|
net "github.com/yusing/go-proxy/internal/net/types"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Entry interface {
|
type Entry interface {
|
||||||
TargetName() string
|
TargetName() string
|
||||||
TargetURL() net.URL
|
TargetURL() net.URL
|
||||||
RawEntry() *RawEntry
|
RawEntry() *RawEntry
|
||||||
LoadBalanceConfig() *loadbalance.Config
|
|
||||||
HealthCheckConfig() *health.HealthCheckConfig
|
|
||||||
IdlewatcherConfig() *idlewatcher.Config
|
IdlewatcherConfig() *idlewatcher.Config
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
"github.com/yusing/go-proxy/internal/docker"
|
||||||
"github.com/yusing/go-proxy/internal/homepage"
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
|
@ -33,7 +34,7 @@ type (
|
||||||
LoadBalance *loadbalance.Config `json:"load_balance,omitempty" yaml:"load_balance"`
|
LoadBalance *loadbalance.Config `json:"load_balance,omitempty" yaml:"load_balance"`
|
||||||
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty" yaml:"middlewares"`
|
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty" yaml:"middlewares"`
|
||||||
Homepage *homepage.Item `json:"homepage,omitempty" yaml:"homepage"`
|
Homepage *homepage.Item `json:"homepage,omitempty" yaml:"homepage"`
|
||||||
// AccessLog *accesslog.Config `json:"access_log,omitempty" yaml:"access_log"`
|
AccessLog *accesslog.Config `json:"access_log,omitempty" yaml:"access_log"`
|
||||||
|
|
||||||
/* Docker only */
|
/* Docker only */
|
||||||
Container *docker.Container `json:"container,omitempty" yaml:"-"`
|
Container *docker.Container `json:"container,omitempty" yaml:"-"`
|
||||||
|
|
Loading…
Add table
Reference in a new issue