mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-21 01:47:43 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c7b188587 | ||
|
|
4c97b79adf | ||
|
|
8ae9573b07 | ||
|
|
43fce6e739 | ||
|
|
78900772bb | ||
|
|
c16a0444ca | ||
|
|
0d518166ee | ||
|
|
6ae391a3c9 | ||
|
|
357897a0cd | ||
|
|
10a0a8fe09 |
@@ -42,8 +42,8 @@ GODOXY_HTTPS_ADDR=:443
|
||||
# API listening address
|
||||
GODOXY_API_ADDR=127.0.0.1:8888
|
||||
|
||||
# Prometheus Metrics listening address (uncomment to enable)
|
||||
#GODOXY_PROMETHEUS_ADDR=:8889
|
||||
# Prometheus Metrics
|
||||
GODOXY_PROMETHEUS_ENABLED=true
|
||||
|
||||
# Debug mode
|
||||
GODOXY_DEBUG=false
|
||||
4
.vscode/settings.example.json
vendored
4
.vscode/settings.example.json
vendored
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"https://github.com/yusing/go-proxy/raw/v0.8/schemas/config.schema.json": [
|
||||
"https://github.com/yusing/go-proxy/raw/v0.9/schemas/config.schema.json": [
|
||||
"config.example.yml",
|
||||
"config.yml"
|
||||
],
|
||||
"https://github.com/yusing/go-proxy/raw/v0.8/schemas/routes.schema.json": [
|
||||
"https://github.com/yusing/go-proxy/raw/v0.9/schemas/routes.schema.json": [
|
||||
"providers.example.yml"
|
||||
]
|
||||
}
|
||||
|
||||
17
README.md
17
README.md
@@ -11,9 +11,7 @@
|
||||
|
||||
A lightweight, easy-to-use, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with a Web UI and dashboard.
|
||||
|
||||
**v0.9 will be out soon, with a brand new [WebUI](next-release.md)!!! Below one is the old one**
|
||||
|
||||

|
||||

|
||||
|
||||
_Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
|
||||
@@ -45,6 +43,7 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [screenshots](#idlesleeper))_
|
||||
- HTTP(s) reserve proxy
|
||||
- OpenID Connect support
|
||||
- [HTTP middleware support](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- TCP and UDP port forwarding
|
||||
@@ -79,7 +78,7 @@ Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/godoxy setup
|
||||
```
|
||||
|
||||
3. _(Optional)_ setup WebUI login
|
||||
3. _(Optional)_ setup WebUI login (skip if you use OIDC)
|
||||
|
||||
- set random JWT secret
|
||||
|
||||
@@ -99,9 +98,7 @@ Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
|
||||
5. Start the container `docker compose up -d`
|
||||
|
||||
6. You may now do some extra configuration
|
||||
- With text editor (e.g. Visual Studio Code)
|
||||
- With Web UI via `https://gp.y.z`
|
||||
6. You may now do some extra configuration on WebUI `https://gp.y.z`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -109,15 +106,15 @@ Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
|
||||
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
||||
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/config.example.yml -O config/config.yml`
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/config.example.yml -O config/config.yml`
|
||||
|
||||
2. Grab `.env.example` into `.env`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/.env.example -O .env`
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/.env.example -O .env`
|
||||
|
||||
3. Grab `compose.example.yml` into `compose.yml`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/compose.example.yml -O compose.yml`
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/compose.example.yml -O compose.yml`
|
||||
|
||||
### Folder structrue
|
||||
|
||||
|
||||
@@ -107,15 +107,15 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
|
||||
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
|
||||
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/config.example.yml -O config/config.yml`
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/config.example.yml -O config/config.yml`
|
||||
|
||||
2. 將 `.env.example` 下載到 `.env`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/.env.example -O .env`
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/.env.example -O .env`
|
||||
|
||||
3. 將 `compose.example.yml` 下載到 `compose.yml`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/compose.example.yml -O compose.yml`
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/compose.example.yml -O compose.yml`
|
||||
|
||||
### 資料夾結構
|
||||
|
||||
|
||||
@@ -128,8 +128,7 @@ func main() {
|
||||
}
|
||||
|
||||
cfg.Start(&config.StartServersOptions{
|
||||
Proxy: true,
|
||||
Metrics: true,
|
||||
Proxy: true,
|
||||
})
|
||||
if err := auth.Initialize(); err != nil {
|
||||
logging.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||
|
||||
@@ -3,10 +3,13 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||
"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/utils/strutils"
|
||||
)
|
||||
|
||||
@@ -36,6 +39,11 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||
mux.HandleFunc("GET", "/v1/favicon", auth.RequireAuth(favicon.GetFavIcon))
|
||||
mux.HandleFunc("POST", "/v1/homepage/set", auth.RequireAuth(v1.SetHomePageOverrides))
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
||||
logging.Info().Msg("prometheus metrics enabled")
|
||||
}
|
||||
|
||||
defaultAuth := auth.GetDefaultAuth()
|
||||
if defaultAuth != nil {
|
||||
mux.HandleFunc("GET", "/v1/auth/redirect", defaultAuth.RedirectLoginPage)
|
||||
|
||||
@@ -71,6 +71,9 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
U.RespondError(w, err)
|
||||
return
|
||||
}
|
||||
if icons == nil {
|
||||
icons = []string{}
|
||||
}
|
||||
U.RespondJSON(w, r, icons)
|
||||
case ListTasks:
|
||||
U.RespondJSON(w, r, task.DebugTaskList())
|
||||
|
||||
@@ -26,10 +26,6 @@ const (
|
||||
|
||||
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
||||
|
||||
SchemasBasePath = "schemas"
|
||||
ConfigSchemaPath = SchemasBasePath + "/config.schema.json"
|
||||
FileProviderSchemaPath = SchemasBasePath + "/providers.schema.json"
|
||||
|
||||
ComposeFileName = "compose.yml"
|
||||
ComposeExampleFileName = "compose.example.yml"
|
||||
|
||||
@@ -38,7 +34,6 @@ const (
|
||||
|
||||
var RequiredDirectories = []string{
|
||||
ConfigBasePath,
|
||||
SchemasBasePath,
|
||||
ErrorPagesBasePath,
|
||||
MiddlewareComposeBasePath,
|
||||
}
|
||||
|
||||
@@ -38,11 +38,7 @@ var (
|
||||
APIHTTPPort,
|
||||
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
||||
|
||||
MetricsHTTPAddr,
|
||||
MetricsHTTPHost,
|
||||
MetricsHTTPPort,
|
||||
MetricsHTTPURL = GetAddrEnv("PROMETHEUS_ADDR", "", "http")
|
||||
PrometheusEnabled = MetricsHTTPURL != ""
|
||||
PrometheusEnabled = GetEnvBool("PROMETHEUS_ENABLED", false)
|
||||
|
||||
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
|
||||
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour)
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/entrypoint"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/metrics"
|
||||
"github.com/yusing/go-proxy/internal/net/http/server"
|
||||
"github.com/yusing/go-proxy/internal/notif"
|
||||
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
||||
@@ -182,7 +181,7 @@ func (cfg *Config) StartProxyProviders() {
|
||||
}
|
||||
|
||||
type StartServersOptions struct {
|
||||
Proxy, API, Metrics bool
|
||||
Proxy, API bool
|
||||
}
|
||||
|
||||
func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
||||
@@ -207,14 +206,6 @@ func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
||||
Handler: api.NewHandler(cfg),
|
||||
})
|
||||
}
|
||||
if opt.Metrics && common.PrometheusEnabled {
|
||||
server.StartServer(cfg.task, server.Options{
|
||||
Name: "metrics",
|
||||
CertProvider: cfg.AutoCertProvider(),
|
||||
HTTPAddr: common.MetricsHTTPAddr,
|
||||
Handler: metrics.NewHandler(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) load() E.Error {
|
||||
|
||||
@@ -4,14 +4,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"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/utils/testing"
|
||||
)
|
||||
|
||||
func TestRebalance(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("zero", func(t *testing.T) {
|
||||
lb := New(new(loadbalance.Config))
|
||||
lb := New(new(types.Config))
|
||||
for range 10 {
|
||||
lb.AddServer(types.TestNewServer(0))
|
||||
}
|
||||
@@ -19,7 +18,7 @@ func TestRebalance(t *testing.T) {
|
||||
ExpectEqual(t, lb.sumWeight, maxWeight)
|
||||
})
|
||||
t.Run("less", func(t *testing.T) {
|
||||
lb := New(new(loadbalance.Config))
|
||||
lb := New(new(types.Config))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .3))
|
||||
@@ -30,7 +29,7 @@ func TestRebalance(t *testing.T) {
|
||||
ExpectEqual(t, lb.sumWeight, maxWeight)
|
||||
})
|
||||
t.Run("more", func(t *testing.T) {
|
||||
lb := New(new(loadbalance.Config))
|
||||
lb := New(new(types.Config))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .3))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package notif
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
@@ -45,3 +47,23 @@ func (base *ProviderBase) GetURL() string {
|
||||
func (base *ProviderBase) GetToken() string {
|
||||
return base.Token
|
||||
}
|
||||
|
||||
func (base *ProviderBase) GetMethod() string {
|
||||
return http.MethodPost
|
||||
}
|
||||
|
||||
func (base *ProviderBase) GetMIMEType() string {
|
||||
return "application/json"
|
||||
}
|
||||
|
||||
func (base *ProviderBase) SetHeaders(logMsg *LogMessage, headers http.Header) {
|
||||
// no-op by default
|
||||
}
|
||||
|
||||
func (base *ProviderBase) makeRespError(resp *http.Response) error {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
return E.Errorf("%s status %d: %s", base.Name, resp.StatusCode, body)
|
||||
}
|
||||
return E.Errorf("%s status %d", base.Name, resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import "fmt"
|
||||
type Color uint
|
||||
|
||||
const (
|
||||
Red Color = 0xff0000
|
||||
Green Color = 0x00ff00
|
||||
Blue Color = 0x0000ff
|
||||
ColorError Color = 0xff0000
|
||||
ColorSuccess Color = 0x00ff00
|
||||
ColorInfo Color = 0x0000ff
|
||||
)
|
||||
|
||||
func (c Color) HexString() string {
|
||||
|
||||
@@ -38,6 +38,8 @@ func (cfg *NotificationConfig) UnmarshalMap(m map[string]any) (err E.Error) {
|
||||
cfg.Provider = &Webhook{}
|
||||
case ProviderGotify:
|
||||
cfg.Provider = &GotifyClient{}
|
||||
case ProviderNtfy:
|
||||
cfg.Provider = &Ntfy{}
|
||||
default:
|
||||
return ErrUnknownNotifProvider.
|
||||
Subject(cfg.ProviderName).
|
||||
|
||||
@@ -14,10 +14,15 @@ type (
|
||||
logCh chan *LogMessage
|
||||
providers F.Set[Provider]
|
||||
}
|
||||
LogField struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
LogFields []LogField
|
||||
LogMessage struct {
|
||||
Level zerolog.Level
|
||||
Title string
|
||||
Extras map[string]any
|
||||
Extras LogFields
|
||||
Color Color
|
||||
}
|
||||
)
|
||||
@@ -48,6 +53,10 @@ func Notify(msg *LogMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *LogFields) Add(name, value string) {
|
||||
*f = append(*f, LogField{Name: name, Value: value})
|
||||
}
|
||||
|
||||
func (disp *Dispatcher) RegisterProvider(cfg *NotificationConfig) {
|
||||
disp.providers.Add(cfg.Provider)
|
||||
}
|
||||
|
||||
@@ -3,32 +3,22 @@ package notif
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func formatMarkdown(extras map[string]interface{}) string {
|
||||
func formatMarkdown(extras LogFields) string {
|
||||
msg := bytes.NewBufferString("")
|
||||
for k, v := range extras {
|
||||
for _, field := range extras {
|
||||
msg.WriteString("#### ")
|
||||
msg.WriteString(k)
|
||||
msg.WriteString(field.Name)
|
||||
msg.WriteRune('\n')
|
||||
msg.WriteString(fmt.Sprintf("%v", v))
|
||||
msg.WriteString(field.Value)
|
||||
msg.WriteRune('\n')
|
||||
}
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
func formatDiscord(extras map[string]interface{}) (string, error) {
|
||||
fieldsMap := make([]map[string]any, len(extras))
|
||||
i := 0
|
||||
for k, extra := range extras {
|
||||
fieldsMap[i] = map[string]any{
|
||||
"name": k,
|
||||
"value": extra,
|
||||
}
|
||||
i++
|
||||
}
|
||||
fields, err := json.Marshal(fieldsMap)
|
||||
func formatDiscord(extras LogFields) (string, error) {
|
||||
fields, err := json.Marshal(extras)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -24,16 +24,6 @@ func (client *GotifyClient) GetURL() string {
|
||||
return client.URL + gotifyMsgEndpoint
|
||||
}
|
||||
|
||||
// GetMethod implements Provider.
|
||||
func (client *GotifyClient) GetMethod() string {
|
||||
return http.MethodPost
|
||||
}
|
||||
|
||||
// GetMIMEType implements Provider.
|
||||
func (client *GotifyClient) GetMIMEType() string {
|
||||
return "application/json"
|
||||
}
|
||||
|
||||
// MakeBody implements Provider.
|
||||
func (client *GotifyClient) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
var priority int
|
||||
@@ -71,7 +61,7 @@ func (client *GotifyClient) makeRespError(resp *http.Response) error {
|
||||
var errm model.Error
|
||||
err := json.NewDecoder(resp.Body).Decode(&errm)
|
||||
if err != nil {
|
||||
return fmt.Errorf(ProviderGotify+" status %d, but failed to decode err response: %w", resp.StatusCode, err)
|
||||
return fmt.Errorf("%s status %d, but failed to decode err response: %w", client.Name, resp.StatusCode, err)
|
||||
}
|
||||
return fmt.Errorf(ProviderGotify+" status %d %s: %s", resp.StatusCode, errm.Error, errm.ErrorDescription)
|
||||
return fmt.Errorf("%s status %d %s: %s", client.Name, resp.StatusCode, errm.Error, errm.ErrorDescription)
|
||||
}
|
||||
|
||||
89
internal/notif/ntfy.go
Normal file
89
internal/notif/ntfy.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package notif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
// See https://docs.ntfy.sh/publish
|
||||
type Ntfy struct {
|
||||
ProviderBase
|
||||
Topic string `json:"topic"`
|
||||
Style NtfyStyle `json:"style"`
|
||||
}
|
||||
|
||||
type NtfyStyle string
|
||||
|
||||
const (
|
||||
NtfyStyleMarkdown NtfyStyle = "markdown"
|
||||
NtfyStylePlain NtfyStyle = "plain"
|
||||
)
|
||||
|
||||
func (n *Ntfy) Validate() E.Error {
|
||||
if n.URL == "" {
|
||||
return E.New("url is required")
|
||||
}
|
||||
if n.Topic == "" {
|
||||
return E.New("topic is required")
|
||||
}
|
||||
if n.Topic[0] == '/' {
|
||||
return E.New("topic should not start with a slash")
|
||||
}
|
||||
switch n.Style {
|
||||
case "":
|
||||
n.Style = NtfyStyleMarkdown
|
||||
case NtfyStyleMarkdown, NtfyStylePlain:
|
||||
default:
|
||||
return E.Errorf("invalid style, expecting %q or %q, got %q", NtfyStyleMarkdown, NtfyStylePlain, n.Style)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Ntfy) GetURL() string {
|
||||
if n.URL[len(n.URL)-1] == '/' {
|
||||
return n.URL + n.Topic
|
||||
}
|
||||
return n.URL + "/" + n.Topic
|
||||
}
|
||||
|
||||
func (n *Ntfy) GetMIMEType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *Ntfy) GetToken() string {
|
||||
return n.Token
|
||||
}
|
||||
|
||||
func (n *Ntfy) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
switch n.Style {
|
||||
case NtfyStyleMarkdown:
|
||||
return strings.NewReader(formatMarkdown(logMsg.Extras)), nil
|
||||
default:
|
||||
return &bytes.Buffer{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Ntfy) SetHeaders(logMsg *LogMessage, headers http.Header) {
|
||||
headers.Set("Title", logMsg.Title)
|
||||
|
||||
switch logMsg.Level {
|
||||
// warning (or other unspecified) uses default priority
|
||||
case zerolog.FatalLevel:
|
||||
headers.Set("Priority", "urgent")
|
||||
case zerolog.ErrorLevel:
|
||||
headers.Set("Priority", "high")
|
||||
case zerolog.InfoLevel:
|
||||
headers.Set("Priority", "low")
|
||||
case zerolog.DebugLevel:
|
||||
headers.Set("Priority", "min")
|
||||
}
|
||||
|
||||
if n.Style == NtfyStyleMarkdown {
|
||||
headers.Set("Markdown", "yes")
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
@@ -21,6 +22,7 @@ type (
|
||||
GetMIMEType() string
|
||||
|
||||
MakeBody(logMsg *LogMessage) (io.Reader, error)
|
||||
SetHeaders(logMsg *LogMessage, headers http.Header)
|
||||
|
||||
makeRespError(resp *http.Response) error
|
||||
}
|
||||
@@ -30,6 +32,7 @@ type (
|
||||
|
||||
const (
|
||||
ProviderGotify = "gotify"
|
||||
ProviderNtfy = "ntfy"
|
||||
ProviderWebhook = "webhook"
|
||||
)
|
||||
|
||||
@@ -38,6 +41,10 @@ func notifyProvider(ctx context.Context, provider Provider, msg *LogMessage) err
|
||||
if err != nil {
|
||||
return E.PrependSubject(provider.GetName(), err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
@@ -52,6 +59,7 @@ func notifyProvider(ctx context.Context, provider Provider, msg *LogMessage) err
|
||||
if provider.GetToken() != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+provider.GetToken())
|
||||
}
|
||||
provider.SetHeaders(msg, req.Header)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -92,12 +92,12 @@ func (webhook *Webhook) GetMIMEType() string {
|
||||
func (webhook *Webhook) makeRespError(resp *http.Response) error {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("webhook status %d, failed to read body: %w", resp.StatusCode, err)
|
||||
return fmt.Errorf("%s status %d, failed to read body: %w", webhook.Name, resp.StatusCode, err)
|
||||
}
|
||||
if len(body) > 0 {
|
||||
return fmt.Errorf("webhook status %d: %s", resp.StatusCode, body)
|
||||
return fmt.Errorf("%s status %d: %s", webhook.Name, resp.StatusCode, body)
|
||||
}
|
||||
return fmt.Errorf("webhook status %d", resp.StatusCode)
|
||||
return fmt.Errorf("%s status %d", webhook.Name, resp.StatusCode)
|
||||
}
|
||||
|
||||
func (webhook *Webhook) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestHTTPConfigDeserialize(t *testing.T) {
|
||||
if err != nil {
|
||||
ExpectNoError(t, err)
|
||||
}
|
||||
ExpectDeepEqual(t, cfg.HTTPConfig, &tt.expected)
|
||||
ExpectDeepEqual(t, cfg.HTTPConfig, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
branch = common.GetEnvString("BRANCH", "v0.8")
|
||||
branch = common.GetEnvString("BRANCH", "v0.9")
|
||||
baseURL = "https://github.com/yusing/go-proxy/raw/" + branch
|
||||
requiredConfigs = []Config{
|
||||
{common.ConfigBasePath, true, false, ""},
|
||||
|
||||
@@ -198,33 +198,33 @@ func (mon *monitor) checkUpdateHealth() error {
|
||||
status = health.StatusUnhealthy
|
||||
}
|
||||
if result.Healthy != (mon.status.Swap(status) == health.StatusHealthy) {
|
||||
extras := map[string]any{
|
||||
"Service Name": mon.service,
|
||||
"Time": strutils.FormatTime(time.Now()),
|
||||
extras := notif.LogFields{
|
||||
{Name: "Service Name", Value: mon.service},
|
||||
{Name: "Time", Value: strutils.FormatTime(time.Now())},
|
||||
}
|
||||
if !result.Healthy {
|
||||
extras["Last Seen"] = strutils.FormatLastSeen(GetLastSeen(mon.service))
|
||||
extras.Add("Last Seen", strutils.FormatLastSeen(GetLastSeen(mon.service)))
|
||||
}
|
||||
if !mon.url.Load().Nil() {
|
||||
extras["Service URL"] = mon.url.Load().String()
|
||||
extras.Add("Service URL", mon.url.Load().String())
|
||||
}
|
||||
if result.Detail != "" {
|
||||
extras["Detail"] = result.Detail
|
||||
extras.Add("Detail", result.Detail)
|
||||
}
|
||||
if result.Healthy {
|
||||
logger.Info().Msg("service is up")
|
||||
extras["Ping"] = fmt.Sprintf("%d ms", result.Latency.Milliseconds())
|
||||
extras.Add("Ping", fmt.Sprintf("%d ms", result.Latency.Milliseconds()))
|
||||
notif.Notify(¬if.LogMessage{
|
||||
Title: "✅ Service is up ✅",
|
||||
Extras: extras,
|
||||
Color: notif.Green,
|
||||
Color: notif.ColorSuccess,
|
||||
})
|
||||
} else {
|
||||
logger.Warn().Msg("service went down")
|
||||
notif.Notify(¬if.LogMessage{
|
||||
Title: "❌ Service went down ❌",
|
||||
Extras: extras,
|
||||
Color: notif.Red,
|
||||
Color: notif.ColorError,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
180
next-release.md
180
next-release.md
@@ -1,176 +1,6 @@
|
||||
GoDoxy v0.9.0 expected changes
|
||||
GoDoxy v0.9.1 expected changes
|
||||
|
||||
- **new** Brand new rewritten WebUI
|
||||
- View logs directly from WebUI
|
||||
- Edit dashboard app config (e.g. icon, name, category, etc.)
|
||||
- Toggle show / hide apps
|
||||
- Health bubbles, latency, etc. rich info on dashboard items
|
||||
- UI config editor
|
||||

|
||||

|
||||

|
||||
- **new** Support selfh.st icons: `@selfhst/<reference>.<format>` _(e.g. `@selfhst/adguard-home.webp`)_
|
||||
- also uses the display name on https://selfh.st/icons/ as default for our dashboard!
|
||||
- **new** GoDoxy server side favicon retreiving and caching
|
||||
- deliver smooth dashboard experience by caching favicons
|
||||
- correct icon can show without setting `homepage.icon` by parsing it from app's root path "/", selecting `link[rel=icon]` from HTML as default icon
|
||||
|
||||
- **Thanks [polds](https://github.com/polds)**
|
||||
Optionally allow a user to specify a “warm-up” endpoint to start the container, returning a 403 if the endpoint isn’t hit and the container has been stopped.
|
||||
|
||||
This can help prevent bots from starting random containers, or allow health check systems to run some probes. Or potentially lock the start endpoints behind a different authentication mechanism, etc.
|
||||
|
||||
Sample service showing this:
|
||||
|
||||
```yaml
|
||||
hello-world:
|
||||
image: nginxdemos/hello
|
||||
container_name: hello-world
|
||||
restart: "no"
|
||||
ports:
|
||||
- "9100:80"
|
||||
labels:
|
||||
proxy.aliases: hello-world
|
||||
proxy.#1.port: 9100
|
||||
proxy.idle_timeout: 45s
|
||||
proxy.wake_timeout: 30s
|
||||
proxy.stop_method: stop
|
||||
proxy.stop_timeout: 10s
|
||||
proxy.stop_signal: SIGTERM
|
||||
proxy.start_endpoint: "/start"
|
||||
```
|
||||
|
||||
Hitting `/` on this service when the container is down:
|
||||
|
||||
```curl
|
||||
$ curl -sv -X GET -H "Host: hello-world.godoxy.local" http://localhost/
|
||||
* Host localhost:80 was resolved.
|
||||
* IPv6: ::1
|
||||
* IPv4: 127.0.0.1
|
||||
* Trying [::1]:80...
|
||||
* Connected to localhost (::1) port 80
|
||||
> GET / HTTP/1.1
|
||||
> Host: hello-world.godoxy.local
|
||||
> User-Agent: curl/8.7.1
|
||||
> Accept: */*
|
||||
>
|
||||
* Request completely sent off
|
||||
< HTTP/1.1 403 Forbidden
|
||||
< Content-Type: text/plain; charset=utf-8
|
||||
< X-Content-Type-Options: nosniff
|
||||
< Date: Wed, 08 Jan 2025 02:04:51 GMT
|
||||
< Content-Length: 71
|
||||
<
|
||||
Forbidden: Container can only be started via configured start endpoint
|
||||
* Connection #0 to host localhost left intact
|
||||
```
|
||||
|
||||
Hitting `/start` when the container is down:
|
||||
|
||||
```curl
|
||||
curl -sv -X GET -H "Host: hello-world.godoxy.local" -H "X-Goproxy-Check-Redirect: skip" http://localhost/start
|
||||
* Host localhost:80 was resolved.
|
||||
* IPv6: ::1
|
||||
* IPv4: 127.0.0.1
|
||||
* Trying [::1]:80...
|
||||
* Connected to localhost (::1) port 80
|
||||
> GET /start HTTP/1.1
|
||||
> Host: hello-world.godoxy.local
|
||||
> User-Agent: curl/8.7.1
|
||||
> Accept: */*
|
||||
> X-Goproxy-Check-Redirect: skip
|
||||
>
|
||||
* Request completely sent off
|
||||
< HTTP/1.1 200 OK
|
||||
< Date: Wed, 08 Jan 2025 02:13:39 GMT
|
||||
< Content-Length: 0
|
||||
<
|
||||
* Connection #0 to host localhost left intact
|
||||
```
|
||||
|
||||
- **Thanks [polds](https://github.com/polds)**
|
||||
Support WebUI authentication via OIDC by setting these environment variables:
|
||||
- `GODOXY_OIDC_ISSUER_URL` e.g.:
|
||||
- Pocket ID: `https://pocker-id.yourdomain.com`
|
||||
- Authentik: `https://authentik.yourdomain.com/application/o/<application_slug>/` **The ending slash is required**
|
||||
- `GODOXY_OIDC_LOGOUT_URL` _(if your issuer supports it, e.g.)_
|
||||
- Authentik: `https://authentik.yourdomain.com/application/o/<application_slug>/end-session`
|
||||
- `GODOXY_OIDC_CLIENT_ID`
|
||||
- `GODOXY_OIDC_CLIENT_SECRET`
|
||||
- `GODOXY_OIDC_REDIRECT_URL`
|
||||
- `GODOXY_OIDC_SCOPES` _(optional)_
|
||||
- `GODOXY_OIDC_ALLOWED_USERS`
|
||||
- `GODOXY_OIDC_ALLOWED_GROUPS` _(optional)_
|
||||
|
||||
- Use OpenID Connect to authenticate GoDoxy's WebUI and all your services (SSO)
|
||||
|
||||
```yaml
|
||||
# default
|
||||
labels:
|
||||
proxy.app.middlewares.oidc:
|
||||
|
||||
# with overridden allowed users
|
||||
labels:
|
||||
proxy.app.middlewares.oidc.allowed_users: user1, user2
|
||||
|
||||
# with overridden allowed groups
|
||||
labels:
|
||||
proxy.app.middlewares.oidc.allowed_groups: group1, group2
|
||||
|
||||
# with both overridden (can use inline YAML string for less typing)
|
||||
labels:
|
||||
proxy.app.middlewares.oidc: |
|
||||
allowed_users: [user1, user2]
|
||||
allowed_groups: [group1, group2]
|
||||
```
|
||||
|
||||
- Caddyfile like rules (experimental)
|
||||
|
||||
```yaml
|
||||
proxy.goaccess.rules: |
|
||||
- name: default
|
||||
do: |
|
||||
rewrite / /index.html
|
||||
serve /var/www/goaccess
|
||||
- name: ws
|
||||
on: |
|
||||
header Connection Upgrade
|
||||
header Upgrade websocket
|
||||
do: bypass # do nothing, pass to reverse proxy
|
||||
|
||||
proxy.app.rules: |
|
||||
- name: default
|
||||
do: bypass # do nothing, pass to reverse proxy
|
||||
- name: block POST and PUT
|
||||
on: method POST | method PUT
|
||||
do: error 403 Forbidden
|
||||
```
|
||||
- config reload will now cause a server full restart (i.e. proxy, api, prometheus, etc), eliminating some incorrect behaviors
|
||||
- drop support of inline yaml string list without hyphen `-` prefix, e.g.
|
||||
```yaml
|
||||
# old
|
||||
proxy.app.middlewares.request.hide_headers: |
|
||||
X-Header1
|
||||
X-Header2
|
||||
|
||||
# new
|
||||
proxy.app.middlewares.request.hide_headers: |
|
||||
- X-Header1
|
||||
- X-Header2
|
||||
```
|
||||
- autocert now supports hot-reload
|
||||
- middleware compose now supports cross-referencing, e.g.
|
||||
```yaml
|
||||
foo:
|
||||
- use: RedirectHTTP
|
||||
bar: # in the same file or different file
|
||||
- use: foo@file
|
||||
```
|
||||
- changed default `ResponseHeaderTimeout` to `60s`
|
||||
- allow customizing `ResponseHeaderTimeout` for each app, e.g.
|
||||
```yaml
|
||||
proxy.<app>.response_header_timeout: 3m
|
||||
```
|
||||
- Fixes
|
||||
- bug: cert renewal failure no longer causes renew schdueler to stuck forever
|
||||
- bug: access log writes to closed file after config reload
|
||||
- Support Ntfy notifications
|
||||
- Prometheus metrics server now inside API server under `/v1/metrics`
|
||||
- `GODOXY_PROMETHEUS_ADDR` removed
|
||||
- `GODOXY_PROMETHEUS_ENABLED` added, default `false`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "godoxy-schemas",
|
||||
"version": "0.9.0-22",
|
||||
"version": "0.9.1-1",
|
||||
"description": "JSON Schema and typescript types for GoDoxy configuration",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
12
schemas/config/notification.d.ts
vendored
12
schemas/config/notification.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { URL } from "../types";
|
||||
export declare const NOTIFICATION_PROVIDERS: readonly ["webhook", "gotify"];
|
||||
export declare const NOTIFICATION_PROVIDERS: readonly ["webhook", "gotify", "ntfy"];
|
||||
export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number];
|
||||
export type NotificationConfig = {
|
||||
name: string;
|
||||
@@ -9,9 +9,17 @@ export interface GotifyConfig extends NotificationConfig {
|
||||
provider: "gotify";
|
||||
token: string;
|
||||
}
|
||||
export declare const NTFY_MSG_STYLES: string[];
|
||||
export type NtfyStyle = (typeof NTFY_MSG_STYLES)[number];
|
||||
export interface NtfyConfig extends NotificationConfig {
|
||||
provider: "ntfy";
|
||||
topic: string;
|
||||
token?: string;
|
||||
style?: NtfyStyle;
|
||||
}
|
||||
export declare const WEBHOOK_TEMPLATES: readonly ["", "discord"];
|
||||
export declare const WEBHOOK_METHODS: readonly ["POST", "GET", "PUT"];
|
||||
export declare const WEBHOOK_MIME_TYPES: readonly ["application/json", "application/x-www-form-urlencoded", "text/plain"];
|
||||
export declare const WEBHOOK_MIME_TYPES: readonly ["application/json", "application/x-www-form-urlencoded", "text/plain", "text/markdown"];
|
||||
export declare const WEBHOOK_COLOR_MODES: readonly ["hex", "dec"];
|
||||
export type WebhookTemplate = (typeof WEBHOOK_TEMPLATES)[number];
|
||||
export type WebhookMethod = (typeof WEBHOOK_METHODS)[number];
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify"];
|
||||
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify", "ntfy"];
|
||||
export const NTFY_MSG_STYLES = ["markdown", "plain"];
|
||||
export const WEBHOOK_TEMPLATES = ["", "discord"];
|
||||
export const WEBHOOK_METHODS = ["POST", "GET", "PUT"];
|
||||
export const WEBHOOK_MIME_TYPES = [
|
||||
"application/json",
|
||||
"application/x-www-form-urlencoded",
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
];
|
||||
export const WEBHOOK_COLOR_MODES = ["hex", "dec"];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { URL } from "../types";
|
||||
|
||||
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify"] as const;
|
||||
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify", "ntfy"] as const;
|
||||
|
||||
export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number];
|
||||
|
||||
@@ -17,12 +17,23 @@ export interface GotifyConfig extends NotificationConfig {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const NTFY_MSG_STYLES = ["markdown", "plain"];
|
||||
export type NtfyStyle = (typeof NTFY_MSG_STYLES)[number];
|
||||
|
||||
export interface NtfyConfig extends NotificationConfig {
|
||||
provider: "ntfy";
|
||||
topic: string;
|
||||
token?: string;
|
||||
style?: NtfyStyle;
|
||||
}
|
||||
|
||||
export const WEBHOOK_TEMPLATES = ["", "discord"] as const;
|
||||
export const WEBHOOK_METHODS = ["POST", "GET", "PUT"] as const;
|
||||
export const WEBHOOK_MIME_TYPES = [
|
||||
"application/json",
|
||||
"application/x-www-form-urlencoded",
|
||||
"text/plain",
|
||||
"text/markdown",
|
||||
] as const;
|
||||
export const WEBHOOK_COLOR_MODES = ["hex", "dec"] as const;
|
||||
|
||||
|
||||
4
schemas/config/providers.d.ts
vendored
4
schemas/config/providers.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { URI, URL } from "../types";
|
||||
import { GotifyConfig, WebhookConfig } from "./notification";
|
||||
import { GotifyConfig, NtfyConfig, WebhookConfig } from "./notification";
|
||||
export type Providers = {
|
||||
/** List of route definition files to include
|
||||
*
|
||||
@@ -21,7 +21,7 @@ export type Providers = {
|
||||
* @minItems 1
|
||||
* @examples require(".").notificationExamples
|
||||
*/
|
||||
notification?: (WebhookConfig | GotifyConfig)[];
|
||||
notification?: (WebhookConfig | GotifyConfig | NtfyConfig)[];
|
||||
};
|
||||
export declare const includeExamples: readonly ["file1.yml", "file2.yml"];
|
||||
export declare const dockerExamples: readonly [{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { URI, URL } from "../types";
|
||||
import { GotifyConfig, WebhookConfig } from "./notification";
|
||||
import { GotifyConfig, NtfyConfig, WebhookConfig } from "./notification";
|
||||
|
||||
export type Providers = {
|
||||
/** List of route definition files to include
|
||||
@@ -20,7 +20,7 @@ export type Providers = {
|
||||
* @minItems 1
|
||||
* @examples require(".").notificationExamples
|
||||
*/
|
||||
notification?: (WebhookConfig | GotifyConfig)[];
|
||||
notification?: (WebhookConfig | GotifyConfig | NtfyConfig)[];
|
||||
};
|
||||
|
||||
export const includeExamples = ["file1.yml", "file2.yml"] as const;
|
||||
|
||||
Reference in New Issue
Block a user