mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-21 16:01:22 +02:00
implement godoxy-agent
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||
mux.HandleFunc("GET", "/v1/stats", useCfg(cfg, v1.Stats))
|
||||
mux.HandleFunc("GET", "/v1/stats/ws", useCfg(cfg, v1.StatsWS))
|
||||
mux.HandleFunc("GET", "/v1/health/ws", auth.RequireAuth(useCfg(cfg, v1.HealthWS)))
|
||||
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(useCfg(cfg, v1.LogsWS())))
|
||||
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(memlogger.LogsWS(cfg)))
|
||||
mux.HandleFunc("GET", "/v1/favicon", auth.RequireAuth(favicon.GetFavIcon))
|
||||
mux.HandleFunc("POST", "/v1/homepage/set", auth.RequireAuth(v1.SetHomePageOverrides))
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
func reqLogger(r *http.Request, level zerolog.Level) *zerolog.Event {
|
||||
return logging.WithLevel(level).
|
||||
Str("module", "api").
|
||||
Str("remote", r.RemoteAddr).
|
||||
Str("host", r.Host).
|
||||
Str("uri", r.Method+" "+r.RequestURI)
|
||||
|
||||
@@ -22,7 +22,7 @@ func InitiateWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Reques
|
||||
|
||||
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
|
||||
|
||||
if len(cfg.Value().MatchDomains) == 0 {
|
||||
if cfg == nil || len(cfg.Value().MatchDomains) == 0 {
|
||||
warnNoMatchDomainOnce.Do(warnNoMatchDomains)
|
||||
originPats = []string{"*"}
|
||||
} else {
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Args struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
const (
|
||||
CommandStart = ""
|
||||
CommandSetup = "setup"
|
||||
CommandNewAgent = "new-agent"
|
||||
CommandValidate = "validate"
|
||||
CommandListConfigs = "ls-config"
|
||||
CommandListRoutes = "ls-routes"
|
||||
@@ -23,34 +14,22 @@ const (
|
||||
CommandDebugListMTrace = "debug-ls-mtrace"
|
||||
)
|
||||
|
||||
var ValidCommands = []string{
|
||||
CommandStart,
|
||||
CommandSetup,
|
||||
CommandValidate,
|
||||
CommandListConfigs,
|
||||
CommandListRoutes,
|
||||
CommandListIcons,
|
||||
CommandReload,
|
||||
CommandDebugListEntries,
|
||||
CommandDebugListProviders,
|
||||
CommandDebugListMTrace,
|
||||
}
|
||||
type MainServerCommandValidator struct{}
|
||||
|
||||
func GetArgs() Args {
|
||||
var args Args
|
||||
flag.Parse()
|
||||
args.Command = flag.Arg(0)
|
||||
if err := validateArg(args.Command); err != nil {
|
||||
log.Fatalf("invalid command: %s", err)
|
||||
func (v MainServerCommandValidator) IsCommandValid(cmd string) bool {
|
||||
switch cmd {
|
||||
case CommandStart,
|
||||
CommandSetup,
|
||||
CommandNewAgent,
|
||||
CommandValidate,
|
||||
CommandListConfigs,
|
||||
CommandListRoutes,
|
||||
CommandListIcons,
|
||||
CommandReload,
|
||||
CommandDebugListEntries,
|
||||
CommandDebugListProviders,
|
||||
CommandDebugListMTrace:
|
||||
return true
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func validateArg(arg string) error {
|
||||
for _, v := range ValidCommands {
|
||||
if arg == v {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid command %q", arg)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ const (
|
||||
ComposeExampleFileName = "compose.example.yml"
|
||||
|
||||
ErrorPagesBasePath = "error_pages"
|
||||
|
||||
AgentCertsBasePath = "certs"
|
||||
)
|
||||
|
||||
var RequiredDirectories = []string{
|
||||
|
||||
@@ -86,6 +86,10 @@ func GetEnvBool(key string, defaultValue bool) bool {
|
||||
return GetEnv(key, defaultValue, strconv.ParseBool)
|
||||
}
|
||||
|
||||
func GetEnvInt(key string, defaultValue int) int {
|
||||
return GetEnv(key, defaultValue, strconv.Atoi)
|
||||
}
|
||||
|
||||
func GetAddrEnv(key, defaultValue, scheme string) (addr, host, port, fullURL string) {
|
||||
addr = GetEnvString(key, defaultValue)
|
||||
if addr == "" {
|
||||
|
||||
@@ -9,7 +9,7 @@ var (
|
||||
"3000": true,
|
||||
}
|
||||
|
||||
ServiceNamePortMapTCP = map[string]int{
|
||||
ImageNamePortMapTCP = map[string]int{
|
||||
"mssql": 1433,
|
||||
"mysql": 3306,
|
||||
"mariadb": 3306,
|
||||
@@ -19,27 +19,9 @@ var (
|
||||
"memcached": 11211,
|
||||
"mongo": 27017,
|
||||
"minecraft-server": 25565,
|
||||
|
||||
"ssh": 22,
|
||||
"ftp": 21,
|
||||
"smtp": 25,
|
||||
"dns": 53,
|
||||
"pop3": 110,
|
||||
"imap": 143,
|
||||
}
|
||||
|
||||
ImageNamePortMap = func() (m map[string]int) {
|
||||
m = make(map[string]int, len(ServiceNamePortMapTCP)+len(imageNamePortMap))
|
||||
for k, v := range ServiceNamePortMapTCP {
|
||||
m[k] = v
|
||||
}
|
||||
for k, v := range imageNamePortMap {
|
||||
m[k] = v
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
imageNamePortMap = map[string]int{
|
||||
ImageNamePortMapHTTP = map[string]int{
|
||||
"adguardhome": 3000,
|
||||
"bazarr": 6767,
|
||||
"calibre-web": 8083,
|
||||
|
||||
@@ -287,6 +287,9 @@ func (cfg *Config) loadRouteProviders(providers *types.Providers) E.Error {
|
||||
lenLongestName = len(p.String())
|
||||
}
|
||||
}
|
||||
for _, agent := range providers.Agents {
|
||||
cfg.providers.Store(agent.Name(), proxy.NewAgentProvider(&agent))
|
||||
}
|
||||
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
|
||||
if err := p.LoadRoutes(); err != nil {
|
||||
errs.Add(err.Subject(p.String()))
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/autocert"
|
||||
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||
"github.com/yusing/go-proxy/internal/notif"
|
||||
@@ -23,9 +24,10 @@ type (
|
||||
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
|
||||
}
|
||||
Providers struct {
|
||||
Files []string `json:"include" validate:"dive,filepath"`
|
||||
Docker map[string]string `json:"docker" validate:"dive,unix_addr|url"`
|
||||
Notification []notif.NotificationConfig `json:"notification"`
|
||||
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
|
||||
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"dive,unix_addr|url"`
|
||||
Agents []agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
||||
Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
||||
}
|
||||
Entrypoint struct {
|
||||
Middlewares []map[string]any `json:"middlewares"`
|
||||
|
||||
@@ -2,12 +2,14 @@ package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
@@ -81,32 +83,44 @@ func ConnectClient(host string) (*SharedClient, error) {
|
||||
// create client
|
||||
var opt []client.Opt
|
||||
|
||||
switch host {
|
||||
case "":
|
||||
return nil, errors.New("empty docker host")
|
||||
case common.DockerHostFromEnv:
|
||||
opt = clientOptEnvHost
|
||||
default:
|
||||
helper, err := connhelper.GetConnectionHelper(host)
|
||||
if err != nil {
|
||||
logging.Panic().Err(err).Msg("failed to get connection helper")
|
||||
if agent.IsDockerHostAgent(host) {
|
||||
cfg, ok := agent.GetAgentFromDockerHost(host)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("agent not found for host: %s", host)
|
||||
}
|
||||
if helper != nil {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
},
|
||||
opt = []client.Opt{
|
||||
client.WithHost(agent.DockerHost),
|
||||
client.WithHTTPClient(cfg.NewHTTPClient()),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
}
|
||||
} else {
|
||||
switch host {
|
||||
case "":
|
||||
return nil, errors.New("empty docker host")
|
||||
case common.DockerHostFromEnv:
|
||||
opt = clientOptEnvHost
|
||||
default:
|
||||
helper, err := connhelper.GetConnectionHelper(host)
|
||||
if err != nil {
|
||||
logging.Panic().Err(err).Msg("failed to get connection helper")
|
||||
}
|
||||
opt = []client.Opt{
|
||||
client.WithHTTPClient(httpClient),
|
||||
client.WithHost(helper.Host),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
client.WithDialContext(helper.Dialer),
|
||||
}
|
||||
} else {
|
||||
opt = []client.Opt{
|
||||
client.WithHost(host),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
if helper != nil {
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
},
|
||||
}
|
||||
opt = []client.Opt{
|
||||
client.WithHTTPClient(httpClient),
|
||||
client.WithHost(helper.Host),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
client.WithDialContext(helper.Dialer),
|
||||
}
|
||||
} else {
|
||||
opt = []client.Opt{
|
||||
client.WithHost(host),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
@@ -21,13 +22,14 @@ type (
|
||||
ContainerID string `json:"container_id"`
|
||||
ImageName string `json:"image_name"`
|
||||
|
||||
Agent *agent.AgentConfig `json:"agent"`
|
||||
|
||||
Labels map[string]string `json:"-"`
|
||||
|
||||
PublicPortMapping PortMapping `json:"public_ports"` // non-zero publicPort:types.Port
|
||||
PrivatePortMapping PortMapping `json:"private_ports"` // privatePort:types.Port
|
||||
PublicIP string `json:"public_ip"`
|
||||
PrivateIP string `json:"private_ip"`
|
||||
NetworkMode string `json:"network_mode"`
|
||||
PublicHostname string `json:"public_hostname"`
|
||||
PrivateHostname string `json:"private_hostname"`
|
||||
|
||||
Aliases []string `json:"aliases"`
|
||||
IsExcluded bool `json:"is_excluded"`
|
||||
@@ -51,7 +53,8 @@ func FromDocker(c *types.Container, dockerHost string) (res *Container) {
|
||||
for lbl := range c.Labels {
|
||||
if strings.HasPrefix(lbl, NSProxy+".") {
|
||||
isExplicit = true
|
||||
break
|
||||
} else {
|
||||
delete(c.Labels, lbl)
|
||||
}
|
||||
}
|
||||
res = &Container{
|
||||
@@ -64,7 +67,6 @@ func FromDocker(c *types.Container, dockerHost string) (res *Container) {
|
||||
|
||||
PublicPortMapping: helper.getPublicPortMapping(),
|
||||
PrivatePortMapping: helper.getPrivatePortMapping(),
|
||||
NetworkMode: c.HostConfig.NetworkMode,
|
||||
|
||||
Aliases: helper.getAliases(),
|
||||
IsExcluded: strutils.ParseBool(helper.getDeleteLabel(LabelExclude)),
|
||||
@@ -78,8 +80,13 @@ func FromDocker(c *types.Container, dockerHost string) (res *Container) {
|
||||
StartEndpoint: helper.getDeleteLabel(LabelStartEndpoint),
|
||||
Running: c.Status == "running" || c.State == "running",
|
||||
}
|
||||
res.setPrivateIP(helper)
|
||||
res.setPublicIP()
|
||||
|
||||
if agent.IsDockerHostAgent(dockerHost) {
|
||||
res.Agent, _ = agent.GetAgentFromDockerHost(dockerHost)
|
||||
}
|
||||
|
||||
res.setPrivateHostname(helper)
|
||||
res.setPublicHostname()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,29 +122,28 @@ func FromJSON(json types.ContainerJSON, dockerHost string) *Container {
|
||||
Networks: json.NetworkSettings.Networks,
|
||||
},
|
||||
}, dockerHost)
|
||||
cont.NetworkMode = string(json.HostConfig.NetworkMode)
|
||||
return cont
|
||||
}
|
||||
|
||||
func (c *Container) setPublicIP() {
|
||||
func (c *Container) setPublicHostname() {
|
||||
if !c.Running {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(c.DockerHost, "unix://") {
|
||||
c.PublicIP = "127.0.0.1"
|
||||
c.PublicHostname = "127.0.0.1"
|
||||
return
|
||||
}
|
||||
url, err := url.Parse(c.DockerHost)
|
||||
if err != nil {
|
||||
logging.Err(err).Msgf("invalid docker host %q, falling back to 127.0.0.1", c.DockerHost)
|
||||
c.PublicIP = "127.0.0.1"
|
||||
c.PublicHostname = "127.0.0.1"
|
||||
return
|
||||
}
|
||||
c.PublicIP = url.Hostname()
|
||||
c.PublicHostname = url.Hostname()
|
||||
}
|
||||
|
||||
func (c *Container) setPrivateIP(helper containerHelper) {
|
||||
if !strings.HasPrefix(c.DockerHost, "unix://") {
|
||||
func (c *Container) setPrivateHostname(helper containerHelper) {
|
||||
if !strings.HasPrefix(c.DockerHost, "unix://") && c.Agent == nil {
|
||||
return
|
||||
}
|
||||
if helper.NetworkSettings == nil {
|
||||
@@ -147,7 +153,7 @@ func (c *Container) setPrivateIP(helper containerHelper) {
|
||||
if v.IPAddress == "" {
|
||||
continue
|
||||
}
|
||||
c.PrivateIP = v.IPAddress
|
||||
c.PrivateHostname = v.IPAddress
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package err
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
)
|
||||
|
||||
@@ -14,6 +15,9 @@ func getLogger(logger ...*zerolog.Logger) *zerolog.Logger {
|
||||
|
||||
//go:inline
|
||||
func LogFatal(msg string, err error, logger ...*zerolog.Logger) {
|
||||
if common.IsDebug {
|
||||
LogPanic(msg, err, logger...)
|
||||
}
|
||||
getLogger(logger...).Fatal().Msg(err.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -66,9 +66,3 @@ func Collect[T any, Err error, Arg any, Func func(Arg) (T, Err)](eb *Builder, fn
|
||||
eb.Add(err)
|
||||
return result
|
||||
}
|
||||
|
||||
func Collect2[T any, Err error, Arg1 any, Arg2 any, Func func(Arg1, Arg2) (T, Err)](eb *Builder, fn Func, arg1 Arg1, arg2 Arg2) T {
|
||||
result, err := fn(arg1, arg2)
|
||||
eb.Add(err)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
var levelHTMLFormats = [][]byte{
|
||||
[]byte(` <span class="log-trace">TRC</span> `),
|
||||
[]byte(` <span class="log-debug">DBG</span> `),
|
||||
[]byte(` <span class="log-info">INF</span> `),
|
||||
[]byte(` <span class="log-warn">WRN</span> `),
|
||||
[]byte(` <span class="log-error">ERR</span> `),
|
||||
[]byte(` <span class="log-fatal">FTL</span> `),
|
||||
[]byte(` <span class="log-panic">PAN</span> `),
|
||||
}
|
||||
|
||||
var colorToClass = map[string]string{
|
||||
"1": "log-bold",
|
||||
"3": "log-italic",
|
||||
"4": "log-underline",
|
||||
"30": "log-black",
|
||||
"31": "log-red",
|
||||
"32": "log-green",
|
||||
"33": "log-yellow",
|
||||
"34": "log-blue",
|
||||
"35": "log-magenta",
|
||||
"36": "log-cyan",
|
||||
"37": "log-white",
|
||||
"90": "log-bright-black",
|
||||
"91": "log-red",
|
||||
"92": "log-bright-green",
|
||||
"93": "log-bright-yellow",
|
||||
"94": "log-bright-blue",
|
||||
"95": "log-bright-magenta",
|
||||
"96": "log-bright-cyan",
|
||||
"97": "log-bright-white",
|
||||
}
|
||||
|
||||
// FormatMessageToHTMLBytes converts text with ANSI color codes to HTML with class names.
|
||||
// ANSI codes are mapped to classes via a static map, and reset codes ([0m) close all spans.
|
||||
// Time complexity is O(n) with minimal allocations.
|
||||
func FormatMessageToHTMLBytes(msg string, buf []byte) ([]byte, error) {
|
||||
buf = append(buf, "<span class=\"log-message\">"...)
|
||||
var stack []string
|
||||
lastPos := 0
|
||||
|
||||
for i := 0; i < len(msg); {
|
||||
if msg[i] == '\x1b' && i+1 < len(msg) && msg[i+1] == '[' {
|
||||
if lastPos < i {
|
||||
escapeAndAppend(msg[lastPos:i], &buf)
|
||||
}
|
||||
i += 2 // Skip \x1b[
|
||||
|
||||
start := i
|
||||
for ; i < len(msg) && msg[i] != 'm'; i++ {
|
||||
if !isANSICodeChar(msg[i]) {
|
||||
return nil, fmt.Errorf("invalid ANSI char: %c", msg[i])
|
||||
}
|
||||
}
|
||||
|
||||
if i >= len(msg) {
|
||||
return nil, errors.New("unterminated ANSI sequence")
|
||||
}
|
||||
|
||||
codeStr := msg[start:i]
|
||||
i++ // Skip 'm'
|
||||
lastPos = i
|
||||
|
||||
startPart := 0
|
||||
for j := 0; j <= len(codeStr); j++ {
|
||||
if j == len(codeStr) || codeStr[j] == ';' {
|
||||
part := codeStr[startPart:j]
|
||||
if part == "" {
|
||||
return nil, errors.New("empty code part")
|
||||
}
|
||||
|
||||
if part == "0" {
|
||||
for range stack {
|
||||
buf = append(buf, "</span>"...)
|
||||
}
|
||||
stack = stack[:0]
|
||||
} else {
|
||||
className, ok := colorToClass[part]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid ANSI code: %s", part)
|
||||
}
|
||||
stack = append(stack, className)
|
||||
buf = append(buf, `<span class="`...)
|
||||
buf = append(buf, className...)
|
||||
buf = append(buf, `">`...)
|
||||
}
|
||||
startPart = j + 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
if lastPos < len(msg) {
|
||||
escapeAndAppend(msg[lastPos:], &buf)
|
||||
}
|
||||
|
||||
for range stack {
|
||||
buf = append(buf, "</span>"...)
|
||||
}
|
||||
|
||||
buf = append(buf, "</span>"...)
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func isANSICodeChar(c byte) bool {
|
||||
return (c >= '0' && c <= '9') || c == ';'
|
||||
}
|
||||
|
||||
func escapeAndAppend(s string, buf *[]byte) {
|
||||
for i, r := range s {
|
||||
switch r {
|
||||
case '•':
|
||||
*buf = append(*buf, "·"...)
|
||||
case '&':
|
||||
*buf = append(*buf, "&"...)
|
||||
case '<':
|
||||
*buf = append(*buf, "<"...)
|
||||
case '>':
|
||||
*buf = append(*buf, ">"...)
|
||||
case '\t':
|
||||
*buf = append(*buf, "	"...)
|
||||
case '\n':
|
||||
*buf = append(*buf, "<br>"...)
|
||||
*buf = append(*buf, prefixHTML...)
|
||||
default:
|
||||
*buf = append(*buf, s[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func timeNowHTML() []byte {
|
||||
if !common.IsTest {
|
||||
return []byte(time.Now().Format(timeFmt))
|
||||
}
|
||||
return []byte(time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC).Format(timeFmt))
|
||||
}
|
||||
|
||||
func FormatLogEntryHTML(level zerolog.Level, message string, buf []byte) []byte {
|
||||
buf = append(buf, []byte(`<pre class="log-entry">`)...)
|
||||
buf = append(buf, timeNowHTML()...)
|
||||
if level < zerolog.NoLevel {
|
||||
buf = append(buf, levelHTMLFormats[level+1]...)
|
||||
}
|
||||
buf, _ = FormatMessageToHTMLBytes(message, buf)
|
||||
buf = append(buf, []byte("</pre>")...)
|
||||
return buf
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestFormatHTML(t *testing.T) {
|
||||
buf := make([]byte, 0, 100)
|
||||
buf = FormatLogEntryHTML(zerolog.InfoLevel, "This is a test.\nThis is a new line.", buf)
|
||||
ExpectEqual(t, string(buf), `<pre class="log-entry">01-01 01:01 <span class="log-info">INF</span> <span class="log-message">This is a test.<br>`+prefix+`This is a new line.</span></pre>`)
|
||||
}
|
||||
|
||||
func TestFormatHTMLANSI(t *testing.T) {
|
||||
buf := make([]byte, 0, 100)
|
||||
buf = FormatLogEntryHTML(zerolog.InfoLevel, "This is \x1b[91m\x1b[1ma test.\x1b[0mOK!.", buf)
|
||||
ExpectEqual(t, string(buf), `<pre class="log-entry">01-01 01:01 <span class="log-info">INF</span> <span class="log-message">This is <span class="log-red"><span class="log-bold">a test.</span></span>OK!.</span></pre>`)
|
||||
buf = buf[:0]
|
||||
buf = FormatLogEntryHTML(zerolog.InfoLevel, "This is \x1b[91ma \x1b[1mtest.\x1b[0mOK!.", buf)
|
||||
ExpectEqual(t, string(buf), `<pre class="log-entry">01-01 01:01 <span class="log-info">INF</span> <span class="log-message">This is <span class="log-red">a <span class="log-bold">test.</span></span>OK!.</span></pre>`)
|
||||
}
|
||||
|
||||
func BenchmarkFormatLogEntryHTML(b *testing.B) {
|
||||
buf := make([]byte, 0, 250)
|
||||
for range b.N {
|
||||
FormatLogEntryHTML(zerolog.InfoLevel, "This is \x1b[91ma \x1b[1mtest.\x1b[0mOK!.", buf)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package v1
|
||||
package memlogger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
@@ -31,11 +30,7 @@ type memLogger struct {
|
||||
bufPool sync.Pool // used in hook mode
|
||||
}
|
||||
|
||||
type MemLogger interface {
|
||||
io.Writer
|
||||
// TODO: hook does not pass in fields, looking for a workaround to do server side log rendering
|
||||
zerolog.Hook
|
||||
}
|
||||
type MemLogger io.Writer
|
||||
|
||||
type buffer struct {
|
||||
data []byte
|
||||
@@ -85,8 +80,10 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func LogsWS() func(config config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
return memLoggerInstance.ServeHTTP
|
||||
func LogsWS(config config.ConfigInstance) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
memLoggerInstance.ServeHTTP(config, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func GetMemLogger() MemLogger {
|
||||
@@ -138,29 +135,6 @@ func (m *memLogger) writeBuf(b []byte) (pos int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Run implements zerolog.Hook.
|
||||
func (m *memLogger) Run(e *zerolog.Event, level zerolog.Level, message string) {
|
||||
bufStruct := m.bufPool.Get().(*buffer)
|
||||
buf := bufStruct.data
|
||||
defer func() {
|
||||
bufStruct.data = bufStruct.data[:0]
|
||||
m.bufPool.Put(bufStruct)
|
||||
}()
|
||||
|
||||
buf = logging.FormatLogEntryHTML(level, message, buf)
|
||||
n := len(buf)
|
||||
|
||||
m.truncateIfNeeded(n)
|
||||
|
||||
pos, err := m.writeBuf(buf)
|
||||
if err != nil {
|
||||
// not logging the error here, it will cause Run to be called again = infinite loop
|
||||
return
|
||||
}
|
||||
|
||||
m.notifyWS(pos, n)
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (m *memLogger) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
@@ -17,6 +17,8 @@ type customErrorPage struct{}
|
||||
|
||||
var CustomErrorPage = NewMiddleware[customErrorPage]()
|
||||
|
||||
const StaticFilePathPrefix = "/$gperrorpage/"
|
||||
|
||||
// before implements RequestModifier.
|
||||
func (customErrorPage) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
return !ServeStaticErrorPageFile(w, r)
|
||||
@@ -49,8 +51,8 @@ func ServeStaticErrorPageFile(w http.ResponseWriter, r *http.Request) (served bo
|
||||
if path != "" && path[0] != '/' {
|
||||
path = "/" + path
|
||||
}
|
||||
if strings.HasPrefix(path, gphttp.StaticFilePathPrefix) {
|
||||
filename := path[len(gphttp.StaticFilePathPrefix):]
|
||||
if strings.HasPrefix(path, StaticFilePathPrefix) {
|
||||
filename := path[len(StaticFilePathPrefix):]
|
||||
file, ok := errorpage.GetStaticFile(filename)
|
||||
if !ok {
|
||||
logging.Error().Msg("unable to load resource " + filename)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -45,7 +46,7 @@ func StartServer(parent task.Parent, opt Options) (s *Server) {
|
||||
func NewServer(opt Options) (s *Server) {
|
||||
var httpSer, httpsSer *http.Server
|
||||
|
||||
logger := logging.With().Str("module", "server").Str("name", opt.Name).Logger()
|
||||
logger := logging.With().Str("server", opt.Name).Logger()
|
||||
|
||||
certAvailable := false
|
||||
if opt.CertProvider != nil {
|
||||
@@ -55,7 +56,7 @@ func NewServer(opt Options) (s *Server) {
|
||||
|
||||
out := io.Discard
|
||||
if common.IsDebug {
|
||||
out = logging.GetLogger()
|
||||
out = logger
|
||||
}
|
||||
|
||||
if opt.HTTPAddr != "" {
|
||||
@@ -107,7 +108,13 @@ func (s *Server) Start(parent task.Parent) {
|
||||
|
||||
if s.https != nil {
|
||||
go func() {
|
||||
s.handleErr("https", s.https.ListenAndServeTLS(s.CertProvider.GetCertPath(), s.CertProvider.GetKeyPath()))
|
||||
l, err := net.Listen("tcp", s.https.Addr)
|
||||
if err != nil {
|
||||
s.handleErr("https", err)
|
||||
return
|
||||
}
|
||||
defer l.Close()
|
||||
s.handleErr("https", s.https.Serve(tls.NewListener(l, s.https.TLSConfig)))
|
||||
}()
|
||||
s.httpsStarted = true
|
||||
s.l.Info().Str("addr", s.https.Addr).Msgf("server started")
|
||||
|
||||
@@ -7,28 +7,28 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultDialer = net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
DefaultTransport = &http.Transport{
|
||||
var DefaultDialer = net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
func NewTransport() *http.Transport {
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: defaultDialer.DialContext,
|
||||
DialContext: DefaultDialer.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
DisableCompression: true, // Prevent double compression
|
||||
// DisableCompression: true, // Prevent double compression
|
||||
ResponseHeaderTimeout: 60 * time.Second,
|
||||
WriteBufferSize: 16 * 1024, // 16KB
|
||||
ReadBufferSize: 16 * 1024, // 16KB
|
||||
}
|
||||
DefaultTransportNoTLS = func() *http.Transport {
|
||||
clone := DefaultTransport.Clone()
|
||||
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
return clone
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
const StaticFilePathPrefix = "/$gperrorpage/"
|
||||
func NewTransportWithTLSConfig(tlsConfig *tls.Config) *http.Transport {
|
||||
tr := NewTransport()
|
||||
tr.TLSClientConfig = tlsConfig
|
||||
return tr
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func (s *FileServer) Start(parent task.Parent) E.Error {
|
||||
}
|
||||
|
||||
if s.UseHealthCheck() {
|
||||
s.Health = monitor.NewFileServerHealthMonitor(s.TargetName(), s.HealthCheck, s.Root)
|
||||
s.Health = monitor.NewFileServerHealthMonitor(s.HealthCheck, s.Root)
|
||||
if err := s.Health.Start(s.task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
34
internal/route/provider/agent.go
Normal file
34
internal/route/provider/agent.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
|
||||
type AgentProvider struct {
|
||||
*agent.AgentConfig
|
||||
docker ProviderImpl
|
||||
}
|
||||
|
||||
func (p *AgentProvider) ShortName() string {
|
||||
return p.Name()
|
||||
}
|
||||
|
||||
func (p *AgentProvider) NewWatcher() watcher.Watcher {
|
||||
return p.docker.NewWatcher()
|
||||
}
|
||||
|
||||
func (p *AgentProvider) IsExplicitOnly() bool {
|
||||
return p.docker.IsExplicitOnly()
|
||||
}
|
||||
|
||||
func (p *AgentProvider) loadRoutesImpl() (route.Routes, E.Error) {
|
||||
return p.docker.loadRoutesImpl()
|
||||
}
|
||||
|
||||
func (p *AgentProvider) Logger() *zerolog.Logger {
|
||||
return p.docker.Logger()
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const (
|
||||
|
||||
var ErrAliasRefIndexOutOfRange = E.New("index out of range")
|
||||
|
||||
func DockerProviderImpl(name, dockerHost string) (ProviderImpl, error) {
|
||||
func DockerProviderImpl(name, dockerHost string) ProviderImpl {
|
||||
if dockerHost == common.DockerHostFromEnv {
|
||||
dockerHost = common.GetEnvString("DOCKER_HOST", client.DefaultDockerHost)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func DockerProviderImpl(name, dockerHost string) (ProviderImpl, error) {
|
||||
name,
|
||||
dockerHost,
|
||||
logging.With().Str("type", "docker").Str("name", name).Logger(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DockerProvider) String() string {
|
||||
|
||||
@@ -258,16 +258,16 @@ func TestPublicIPLocalhost(t *testing.T) {
|
||||
c := &types.Container{Names: dummyNames, State: "running"}
|
||||
r, ok := makeRoutes(c)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, r.Container.PublicIP, "127.0.0.1")
|
||||
ExpectEqual(t, r.Host, r.Container.PublicIP)
|
||||
ExpectEqual(t, r.Container.PublicHostname, "127.0.0.1")
|
||||
ExpectEqual(t, r.Host, r.Container.PublicHostname)
|
||||
}
|
||||
|
||||
func TestPublicIPRemote(t *testing.T) {
|
||||
c := &types.Container{Names: dummyNames, State: "running"}
|
||||
raw, ok := makeRoutes(c, testIP)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PublicIP, testIP)
|
||||
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
|
||||
ExpectEqual(t, raw.Container.PublicHostname, testIP)
|
||||
ExpectEqual(t, raw.Host, raw.Container.PublicHostname)
|
||||
}
|
||||
|
||||
func TestPrivateIPLocalhost(t *testing.T) {
|
||||
@@ -283,8 +283,8 @@ func TestPrivateIPLocalhost(t *testing.T) {
|
||||
}
|
||||
r, ok := makeRoutes(c)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, r.Container.PrivateIP, testDockerIP)
|
||||
ExpectEqual(t, r.Host, r.Container.PrivateIP)
|
||||
ExpectEqual(t, r.Container.PrivateHostname, testDockerIP)
|
||||
ExpectEqual(t, r.Host, r.Container.PrivateHostname)
|
||||
}
|
||||
|
||||
func TestPrivateIPRemote(t *testing.T) {
|
||||
@@ -301,9 +301,9 @@ func TestPrivateIPRemote(t *testing.T) {
|
||||
}
|
||||
r, ok := makeRoutes(c, testIP)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, r.Container.PrivateIP, "")
|
||||
ExpectEqual(t, r.Container.PublicIP, testIP)
|
||||
ExpectEqual(t, r.Host, r.Container.PublicIP)
|
||||
ExpectEqual(t, r.Container.PrivateHostname, "")
|
||||
ExpectEqual(t, r.Container.PublicHostname, testIP)
|
||||
ExpectEqual(t, r.Host, r.Container.PublicHostname)
|
||||
}
|
||||
|
||||
func TestStreamDefaultValues(t *testing.T) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
@@ -38,25 +37,25 @@ func (handler *EventHandler) Handle(parent task.Parent, events []watcher.Event)
|
||||
}
|
||||
}
|
||||
|
||||
if common.IsDebug {
|
||||
eventsLog := E.NewBuilder("events")
|
||||
for _, event := range events {
|
||||
eventsLog.Addf("event %s, actor: name=%s, id=%s", event.Action, event.ActorName, event.ActorID)
|
||||
}
|
||||
E.LogDebug(eventsLog.About(), eventsLog.Error(), handler.provider.Logger())
|
||||
// if common.IsDebug {
|
||||
// eventsLog := E.NewBuilder("events")
|
||||
// for _, event := range events {
|
||||
// eventsLog.Addf("event %s, actor: name=%s, id=%s", event.Action, event.ActorName, event.ActorID)
|
||||
// }
|
||||
// E.LogDebug(eventsLog.About(), eventsLog.Error(), handler.provider.Logger())
|
||||
|
||||
oldRoutesLog := E.NewBuilder("old routes")
|
||||
for k := range oldRoutes {
|
||||
oldRoutesLog.Adds(k)
|
||||
}
|
||||
E.LogDebug(oldRoutesLog.About(), oldRoutesLog.Error(), handler.provider.Logger())
|
||||
// oldRoutesLog := E.NewBuilder("old routes")
|
||||
// for k := range oldRoutes {
|
||||
// oldRoutesLog.Adds(k)
|
||||
// }
|
||||
// E.LogDebug(oldRoutesLog.About(), oldRoutesLog.Error(), handler.provider.Logger())
|
||||
|
||||
newRoutesLog := E.NewBuilder("new routes")
|
||||
for k := range newRoutes {
|
||||
newRoutesLog.Adds(k)
|
||||
}
|
||||
E.LogDebug(newRoutesLog.About(), newRoutesLog.Error(), handler.provider.Logger())
|
||||
}
|
||||
// newRoutesLog := E.NewBuilder("new routes")
|
||||
// for k := range newRoutes {
|
||||
// newRoutesLog.Adds(k)
|
||||
// }
|
||||
// E.LogDebug(newRoutesLog.About(), newRoutesLog.Error(), handler.provider.Logger())
|
||||
// }
|
||||
|
||||
for k, oldr := range oldRoutes {
|
||||
newr, ok := newRoutes[k]
|
||||
@@ -85,7 +84,7 @@ func (handler *EventHandler) matchAny(events []watcher.Event, route *route.Route
|
||||
|
||||
func (handler *EventHandler) match(event watcher.Event, route *route.Route) bool {
|
||||
switch handler.provider.GetType() {
|
||||
case types.ProviderTypeDocker:
|
||||
case types.ProviderTypeDocker, types.ProviderTypeAgent:
|
||||
return route.Container.ContainerID == event.ActorID ||
|
||||
route.Container.ContainerName == event.ActorName
|
||||
case types.ProviderTypeFile:
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
@@ -64,14 +65,22 @@ func NewDockerProvider(name string, dockerHost string) (p *Provider, err error)
|
||||
}
|
||||
|
||||
p = newProvider(types.ProviderTypeDocker)
|
||||
p.ProviderImpl, err = DockerProviderImpl(name, dockerHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.ProviderImpl = DockerProviderImpl(name, dockerHost)
|
||||
p.watcher = p.NewWatcher()
|
||||
return
|
||||
}
|
||||
|
||||
func NewAgentProvider(cfg *agent.AgentConfig) *Provider {
|
||||
p := newProvider(types.ProviderTypeAgent)
|
||||
agent := &AgentProvider{
|
||||
AgentConfig: cfg,
|
||||
docker: DockerProviderImpl(cfg.Name(), cfg.FakeDockerHost()),
|
||||
}
|
||||
p.ProviderImpl = agent
|
||||
p.watcher = p.NewWatcher()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Provider) GetType() types.ProviderType {
|
||||
return p.t
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ type ProviderType string
|
||||
const (
|
||||
ProviderTypeDocker ProviderType = "docker"
|
||||
ProviderTypeFile ProviderType = "file"
|
||||
ProviderTypeAgent ProviderType = "agent"
|
||||
)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agentproxy"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
@@ -38,20 +41,27 @@ type (
|
||||
|
||||
// var globalMux = http.NewServeMux() // TODO: support regex subdomain matching.
|
||||
|
||||
// TODO: fix this for agent
|
||||
func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, E.Error) {
|
||||
trans := gphttp.DefaultTransport
|
||||
httpConfig := base.HTTPConfig
|
||||
proxyURL := base.ProxyURL
|
||||
|
||||
if httpConfig.NoTLSVerify {
|
||||
trans = gphttp.DefaultTransportNoTLS
|
||||
}
|
||||
if httpConfig.ResponseHeaderTimeout > 0 {
|
||||
trans = trans.Clone()
|
||||
trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout
|
||||
trans := gphttp.NewTransport()
|
||||
a := base.Agent()
|
||||
if a != nil {
|
||||
trans = a.Transport()
|
||||
proxyURL = agent.HTTPProxyURL
|
||||
} else {
|
||||
if httpConfig.NoTLSVerify {
|
||||
trans.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
if httpConfig.ResponseHeaderTimeout > 0 {
|
||||
trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout
|
||||
}
|
||||
}
|
||||
|
||||
service := base.TargetName()
|
||||
rp := reverseproxy.NewReverseProxy(service, base.ProxyURL, trans)
|
||||
rp := reverseproxy.NewReverseProxy(service, proxyURL, trans)
|
||||
|
||||
if len(base.Middlewares) > 0 {
|
||||
err := middleware.PatchReverseProxy(rp, base.Middlewares)
|
||||
@@ -60,6 +70,20 @@ func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, E.Error) {
|
||||
}
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
headers := &agentproxy.AgentProxyHeaders{
|
||||
Host: base.ProxyURL.Host,
|
||||
IsHTTPS: base.ProxyURL.Scheme == "https",
|
||||
SkipTLSVerify: httpConfig.NoTLSVerify,
|
||||
ResponseHeaderTimeout: int(httpConfig.ResponseHeaderTimeout.Seconds()),
|
||||
}
|
||||
ori := rp.HandlerFunc
|
||||
rp.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||
agentproxy.SetAgentProxyHeaders(r, headers)
|
||||
ori(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
r := &ReveseProxyRoute{
|
||||
Route: base,
|
||||
rp: rp,
|
||||
@@ -88,13 +112,13 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) E.Error {
|
||||
if r.IsDocker() {
|
||||
client, err := docker.ConnectClient(r.Idlewatcher.DockerHost)
|
||||
if err == nil {
|
||||
fallback := monitor.NewHTTPHealthChecker(r.rp.TargetURL, r.HealthCheck)
|
||||
fallback := r.newHealthMonitor()
|
||||
r.HealthMon = monitor.NewDockerHealthMonitor(client, r.Idlewatcher.ContainerID, r.TargetName(), r.HealthCheck, fallback)
|
||||
r.task.OnCancel("close_docker_client", client.Close)
|
||||
}
|
||||
}
|
||||
if r.HealthMon == nil {
|
||||
r.HealthMon = monitor.NewHTTPHealthMonitor(r.rp.TargetURL, r.HealthCheck)
|
||||
r.HealthMon = r.newHealthMonitor()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +202,17 @@ func (r *ReveseProxyRoute) HealthMonitor() health.HealthMonitor {
|
||||
return r.HealthMon
|
||||
}
|
||||
|
||||
func (r *ReveseProxyRoute) newHealthMonitor() interface {
|
||||
health.HealthMonitor
|
||||
health.HealthChecker
|
||||
} {
|
||||
if a := r.Agent(); a != nil {
|
||||
target := monitor.AgentCheckHealthTargetFromURL(r.ProxyURL)
|
||||
return monitor.NewAgentRouteMonitor(a, r.HealthCheck, target)
|
||||
}
|
||||
return monitor.NewHTTPHealthMonitor(r.ProxyURL, r.HealthCheck)
|
||||
}
|
||||
|
||||
func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
||||
var lb *loadbalancer.LoadBalancer
|
||||
cfg := r.LoadBalance
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
@@ -159,6 +160,17 @@ func (r *Route) Type() types.RouteType {
|
||||
panic(fmt.Errorf("unexpected scheme %s for alias %s", r.Scheme, r.Alias))
|
||||
}
|
||||
|
||||
func (r *Route) Agent() *agent.AgentConfig {
|
||||
if r.Container == nil {
|
||||
return nil
|
||||
}
|
||||
return r.Container.Agent
|
||||
}
|
||||
|
||||
func (r *Route) IsAgent() bool {
|
||||
return r.Container != nil && r.Container.Agent != nil
|
||||
}
|
||||
|
||||
func (r *Route) HealthMonitor() health.HealthMonitor {
|
||||
return r.impl.HealthMonitor()
|
||||
}
|
||||
@@ -240,24 +252,24 @@ func (r *Route) Finalize() {
|
||||
switch {
|
||||
case !isDocker:
|
||||
r.Host = "localhost"
|
||||
case cont.PrivateIP != "":
|
||||
r.Host = cont.PrivateIP
|
||||
case cont.PublicIP != "":
|
||||
r.Host = cont.PublicIP
|
||||
case cont.PrivateHostname != "":
|
||||
r.Host = cont.PrivateHostname
|
||||
case cont.PublicHostname != "":
|
||||
r.Host = cont.PublicHostname
|
||||
}
|
||||
}
|
||||
|
||||
lp, pp := r.Port.Listening, r.Port.Proxy
|
||||
|
||||
if isDocker {
|
||||
if port, ok := common.ServiceNamePortMapTCP[cont.ImageName]; ok {
|
||||
if port, ok := common.ImageNamePortMapTCP[cont.ImageName]; ok {
|
||||
if pp == 0 {
|
||||
pp = port
|
||||
}
|
||||
if r.Scheme == "" {
|
||||
r.Scheme = "tcp"
|
||||
}
|
||||
} else if port, ok := common.ImageNamePortMap[cont.ImageName]; ok {
|
||||
} else if port, ok := common.ImageNamePortMapHTTP[cont.ImageName]; ok {
|
||||
if pp == 0 {
|
||||
pp = port
|
||||
}
|
||||
@@ -268,39 +280,34 @@ func (r *Route) Finalize() {
|
||||
}
|
||||
|
||||
if pp == 0 {
|
||||
switch {
|
||||
case r.Scheme == "https":
|
||||
pp = 443
|
||||
case !isDocker:
|
||||
pp = 80
|
||||
default:
|
||||
if isDocker {
|
||||
pp = lowestPort(cont.PrivatePortMapping)
|
||||
if pp == 0 {
|
||||
pp = lowestPort(cont.PublicPortMapping)
|
||||
}
|
||||
} else if r.Scheme == "https" {
|
||||
pp = 443
|
||||
} else {
|
||||
pp = 80
|
||||
}
|
||||
}
|
||||
|
||||
if isDocker {
|
||||
// replace private port with public port if using public IP.
|
||||
if r.Host == cont.PublicIP {
|
||||
if r.Host == cont.PublicHostname {
|
||||
if p, ok := cont.PrivatePortMapping[pp]; ok {
|
||||
pp = int(p.PublicPort)
|
||||
if r.Scheme == "" && p.Type == "udp" {
|
||||
r.Scheme = "udp"
|
||||
}
|
||||
}
|
||||
}
|
||||
// replace public port with private port if using private IP.
|
||||
if r.Host == cont.PrivateIP {
|
||||
} else {
|
||||
// replace public port with private port if using private IP.
|
||||
if p, ok := cont.PublicPortMapping[pp]; ok {
|
||||
pp = int(p.PrivatePort)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Scheme == "" {
|
||||
switch {
|
||||
case r.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp":
|
||||
r.Scheme = "udp"
|
||||
case r.Host == cont.PrivateIP && cont.PrivatePortMapping[pp].Type == "udp":
|
||||
r.Scheme = "udp"
|
||||
if r.Scheme == "" && p.Type == "udp" {
|
||||
r.Scheme = "udp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,13 +329,10 @@ func (r *Route) Finalize() {
|
||||
r.HealthCheck = health.DefaultHealthConfig
|
||||
}
|
||||
|
||||
// set or keep at least default
|
||||
if !r.HealthCheck.Disable {
|
||||
if r.HealthCheck.Interval == 0 {
|
||||
r.HealthCheck.Interval = common.HealthCheckIntervalDefault
|
||||
}
|
||||
if r.HealthCheck.Timeout == 0 {
|
||||
r.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
|
||||
}
|
||||
r.HealthCheck.Interval |= common.HealthCheckIntervalDefault
|
||||
r.HealthCheck.Timeout |= common.HealthCheckTimeoutDefault
|
||||
}
|
||||
|
||||
if isDocker && cont.IdleTimeout != "" {
|
||||
|
||||
@@ -125,7 +125,11 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||
if item.Category == "" {
|
||||
item.Category = "Docker"
|
||||
}
|
||||
item.SourceType = string(provider.ProviderTypeDocker)
|
||||
if r.IsAgent() {
|
||||
item.SourceType = string(provider.ProviderTypeAgent)
|
||||
} else {
|
||||
item.SourceType = string(provider.ProviderTypeDocker)
|
||||
}
|
||||
case r.UseLoadBalance():
|
||||
if item.Category == "" {
|
||||
item.Category = "Load-balanced"
|
||||
|
||||
@@ -164,7 +164,7 @@ var commands = map[string]struct {
|
||||
if target.Scheme == "" {
|
||||
target.Scheme = "http"
|
||||
}
|
||||
rp := reverseproxy.NewReverseProxy("", target, gphttp.DefaultTransport)
|
||||
rp := reverseproxy.NewReverseProxy("", target, gphttp.NewTransport())
|
||||
return ReturningCommand(rp.ServeHTTP)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -234,7 +234,8 @@ func TestOnCorrectness(t *testing.T) {
|
||||
|
||||
tests = append(tests, genCorrectnessTestCases("header", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
Header: http.Header{k: []string{v}}}
|
||||
Header: http.Header{k: []string{v}},
|
||||
}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("query", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
|
||||
@@ -3,6 +3,7 @@ package types
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
@@ -31,7 +32,10 @@ type (
|
||||
HomepageConfig() *homepage.Item
|
||||
ContainerInfo() *docker.Container
|
||||
|
||||
Agent() *agent.AgentConfig
|
||||
|
||||
IsDocker() bool
|
||||
IsAgent() bool
|
||||
UseLoadBalance() bool
|
||||
UseIdleWatcher() bool
|
||||
UseHealthCheck() bool
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
var (
|
||||
branch = common.GetEnvString("BRANCH", "v0.9")
|
||||
baseURL = "https://github.com/yusing/go-proxy/raw/" + branch
|
||||
requiredConfigs = []Config{
|
||||
{common.ConfigBasePath, true, false, ""},
|
||||
{common.DotEnvPath, false, true, common.DotEnvExamplePath},
|
||||
{common.ComposeFileName, false, true, common.ComposeExampleFileName},
|
||||
{path.Join(common.ConfigBasePath, common.ConfigFileName), false, true, common.ConfigExampleFileName},
|
||||
}
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Pathname string
|
||||
IsDir bool
|
||||
NeedDownload bool
|
||||
DownloadFileName string
|
||||
}
|
||||
|
||||
func Setup() {
|
||||
log.Println("setting up go-proxy")
|
||||
log.Println("branch:", branch)
|
||||
|
||||
if err := os.Chdir("/setup"); err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
|
||||
for _, config := range requiredConfigs {
|
||||
config.setup()
|
||||
}
|
||||
|
||||
log.Println("setup finished")
|
||||
}
|
||||
|
||||
func (c *Config) setup() {
|
||||
if c.IsDir {
|
||||
mkdir(c.Pathname)
|
||||
return
|
||||
}
|
||||
if !c.NeedDownload {
|
||||
touch(c.Pathname)
|
||||
return
|
||||
}
|
||||
|
||||
fetch(c.DownloadFileName, c.Pathname)
|
||||
}
|
||||
|
||||
func hasFileOrDir(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func mkdir(pathname string) {
|
||||
_, err := os.Stat(pathname)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
log.Printf("creating directory %q\n", pathname)
|
||||
err := os.MkdirAll(pathname, 0o755)
|
||||
if err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func touch(pathname string) {
|
||||
if hasFileOrDir(pathname) {
|
||||
return
|
||||
}
|
||||
log.Printf("creating file %q\n", pathname)
|
||||
_, err := os.Create(pathname)
|
||||
if err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func fetch(remoteFilename string, outFileName string) {
|
||||
if hasFileOrDir(outFileName) {
|
||||
if remoteFilename == outFileName {
|
||||
log.Printf("%q already exists, not overwriting\n", outFileName)
|
||||
return
|
||||
}
|
||||
log.Printf("%q already exists, downloading to %q\n", outFileName, remoteFilename)
|
||||
outFileName = remoteFilename
|
||||
}
|
||||
log.Printf("downloading %q to %q\n", remoteFilename, outFileName)
|
||||
|
||||
url, err := url.JoinPath(baseURL, remoteFilename)
|
||||
if err != nil {
|
||||
log.Fatalf("unexpected error: %s\n", err)
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatalf("http request failed: %s\n", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
log.Fatalf("error reading response body: %s\n", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(outFileName, body, 0o644)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
log.Fatalf("failed to write to file: %s\n", err)
|
||||
}
|
||||
|
||||
log.Print("done")
|
||||
|
||||
resp.Body.Close()
|
||||
}
|
||||
@@ -34,3 +34,15 @@ func ListFiles(dir string, maxDepth int, hideHidden ...bool) ([]string, error) {
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// FileExists checks if a file exists.
|
||||
//
|
||||
// If the file does not exist, it returns false and nil,
|
||||
// otherwise it returns true and any error that is not os.ErrNotExist.
|
||||
func FileExists(file string) (bool, error) {
|
||||
_, err := os.Stat(file)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
|
||||
25
internal/utils/wait_exit.go
Normal file
25
internal/utils/wait_exit.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
)
|
||||
|
||||
func WaitExit(shutdownTimeout int) {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT)
|
||||
signal.Notify(sig, syscall.SIGTERM)
|
||||
signal.Notify(sig, syscall.SIGHUP)
|
||||
|
||||
// wait for signal
|
||||
<-sig
|
||||
|
||||
// gracefully shutdown
|
||||
logging.Info().Msg("shutting down")
|
||||
_ = task.GracefulShutdown(time.Second * time.Duration(shutdownTimeout))
|
||||
}
|
||||
@@ -6,17 +6,13 @@ import (
|
||||
|
||||
docker_events "github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/rs/zerolog"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
type (
|
||||
DockerWatcher struct {
|
||||
zerolog.Logger
|
||||
|
||||
host string
|
||||
client *D.SharedClient
|
||||
clientOwned bool
|
||||
@@ -56,20 +52,12 @@ func NewDockerWatcher(host string) DockerWatcher {
|
||||
return DockerWatcher{
|
||||
host: host,
|
||||
clientOwned: true,
|
||||
Logger: logging.With().
|
||||
Str("type", "docker").
|
||||
Str("host", host).
|
||||
Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewDockerWatcherWithClient(client *D.SharedClient) DockerWatcher {
|
||||
return DockerWatcher{
|
||||
client: client,
|
||||
Logger: logging.With().
|
||||
Str("type", "docker").
|
||||
Str("host", client.DaemonHost()).
|
||||
Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +112,6 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
|
||||
case msg := <-cEventCh:
|
||||
action, ok := events.DockerEventMap[msg.Action]
|
||||
if !ok {
|
||||
w.Debug().Msgf("ignored unknown docker event: %s for container %s", msg.Action, msg.Actor.Attributes["name"])
|
||||
continue
|
||||
}
|
||||
event := Event{
|
||||
|
||||
75
internal/watcher/health/monitor/agent_route.go
Normal file
75
internal/watcher/health/monitor/agent_route.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type (
|
||||
AgentRouteMonior struct {
|
||||
agent *agentPkg.AgentConfig
|
||||
endpointURL string
|
||||
*monitor
|
||||
}
|
||||
AgentCheckHealthTarget struct {
|
||||
Scheme string
|
||||
Host string
|
||||
Path string
|
||||
}
|
||||
)
|
||||
|
||||
func AgentCheckHealthTargetFromURL(url *types.URL) *AgentCheckHealthTarget {
|
||||
return &AgentCheckHealthTarget{
|
||||
Scheme: url.Scheme,
|
||||
Host: url.Host,
|
||||
Path: url.Path,
|
||||
}
|
||||
}
|
||||
|
||||
func (target *AgentCheckHealthTarget) buildQuery() string {
|
||||
query := make(url.Values, 3)
|
||||
query.Set("scheme", target.Scheme)
|
||||
query.Set("host", target.Host)
|
||||
query.Set("path", target.Path)
|
||||
return query.Encode()
|
||||
}
|
||||
|
||||
func (target *AgentCheckHealthTarget) displayURL() *types.URL {
|
||||
return types.NewURL(&url.URL{
|
||||
Scheme: target.Scheme,
|
||||
Host: target.Host,
|
||||
Path: target.Path,
|
||||
})
|
||||
}
|
||||
|
||||
func NewAgentRouteMonitor(agent *agentPkg.AgentConfig, config *health.HealthCheckConfig, target *AgentCheckHealthTarget) *AgentRouteMonior {
|
||||
mon := &AgentRouteMonior{
|
||||
agent: agent,
|
||||
endpointURL: agentPkg.EndpointHealth + "?" + target.buildQuery(),
|
||||
}
|
||||
mon.monitor = newMonitor(target.displayURL(), config, mon.CheckHealth)
|
||||
return mon
|
||||
}
|
||||
|
||||
func (mon *AgentRouteMonior) CheckHealth() (result *health.HealthCheckResult, err error) {
|
||||
result = new(health.HealthCheckResult)
|
||||
ctx, cancel := mon.ContextWithTimeout("timeout querying agent")
|
||||
defer cancel()
|
||||
data, status, err := mon.agent.Fetch(ctx, mon.endpointURL)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
err = json.Unmarshal(data, result)
|
||||
default:
|
||||
err = errors.New(string(data))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -12,10 +12,9 @@ type FileServerHealthMonitor struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func NewFileServerHealthMonitor(alias string, config *health.HealthCheckConfig, path string) *FileServerHealthMonitor {
|
||||
func NewFileServerHealthMonitor(config *health.HealthCheckConfig, path string) *FileServerHealthMonitor {
|
||||
mon := &FileServerHealthMonitor{path: path}
|
||||
mon.monitor = newMonitor(nil, config, mon.CheckHealth)
|
||||
mon.service = alias
|
||||
return mon
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
|
||||
type (
|
||||
HealthCheckResult struct {
|
||||
Healthy bool
|
||||
Detail string
|
||||
Latency time.Duration
|
||||
Healthy bool `json:"healthy"`
|
||||
Detail string `json:"detail"`
|
||||
Latency time.Duration `json:"latency"`
|
||||
}
|
||||
WithHealthInfo interface {
|
||||
Status() Status
|
||||
|
||||
Reference in New Issue
Block a user