Compare commits

..

10 Commits
0.9 ... 0.9.1

Author SHA1 Message Date
yusing
0c7b188587 api: fix search icon returning null when no match 2025-02-02 03:31:52 +08:00
yusing
4c97b79adf log prometheus enabled 2025-02-02 03:21:39 +08:00
yusing
8ae9573b07 add timeout to notification context 2025-02-01 14:42:21 +08:00
yusing
43fce6e739 fix two tests 2025-02-01 14:41:22 +08:00
Yuzerion
78900772bb Feat/ntfy (#57)
* implement ntfy notification

* fix notification fields order

* fix schema for ntfy

---------

Co-authored-by: yusing <yusing@6uo.me>
2025-02-01 13:07:44 +08:00
yusing
c16a0444ca fix main.go and update next release doc 2025-02-01 12:51:52 +08:00
yusing
0d518166ee api: move prometheus handler inside api handler /v1/metrics 2025-02-01 02:09:43 +08:00
yusing
6ae391a3c9 make POST and JSON as notification defaults 2025-01-31 14:56:55 +08:00
yusing
357897a0cd remove schema stuff from code 2025-01-31 05:21:32 +08:00
yusing
10a0a8fe09 update readme 2025-01-31 03:33:20 +08:00
31 changed files with 223 additions and 274 deletions

View File

@@ -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

View File

@@ -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"
]
}

View File

@@ -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**
![Screenshot](screenshots/webui.png)
![Screenshot](https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f)
_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

View File

@@ -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`
### 資料夾結構

View File

@@ -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")

View File

@@ -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)

View File

@@ -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())

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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).

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
View 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")
}
}

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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)
})
}
}

View File

@@ -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, ""},

View File

@@ -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(&notif.LogMessage{
Title: "✅ Service is up ✅",
Extras: extras,
Color: notif.Green,
Color: notif.ColorSuccess,
})
} else {
logger.Warn().Msg("service went down")
notif.Notify(&notif.LogMessage{
Title: "❌ Service went down ❌",
Extras: extras,
Color: notif.Red,
Color: notif.ColorError,
})
}
}

View File

@@ -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
![{7829FA41-5733-4BAD-8183-CDF093CEC6F2}](https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f)
![{29A4608C-607F-43C9-A542-15EC6B9D024E}](https://github.com/user-attachments/assets/8469cfaf-dc37-4b6e-9f29-c44eea91bb82)
![{83118DF5-9D46-4D00-9CEF-C0F6C8D18C4B}](https://github.com/user-attachments/assets/856140f0-78bb-4a76-98f2-ad47544a3515)
- **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 isnt 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`

View File

@@ -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

View File

@@ -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];

View File

@@ -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"];

View File

@@ -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;

View File

@@ -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 [{

View File

@@ -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;