diff --git a/Dockerfile b/Dockerfile index 4a0498f..d81031a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1 # Build Go binary -FROM golang:1.25-alpine AS builder +FROM golang:1.22-alpine AS builder WORKDIR /src RUN apk add --no-cache ca-certificates tzdata COPY go.mod ./ @@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/ntfywui ./cmd/ntfy FROM binwiederhier/ntfy:latest AS ntfy # Runtime -FROM alpine:3.23 +FROM alpine:3.20 RUN apk add --no-cache ca-certificates tzdata \ && addgroup -S ntfywui && adduser -S -G ntfywui -h /home/ntfywui ntfywui WORKDIR /app diff --git a/cmd/ntfywui/main.go b/cmd/ntfywui/main.go index 5f7b837..9a1273c 100644 --- a/cmd/ntfywui/main.go +++ b/cmd/ntfywui/main.go @@ -62,25 +62,25 @@ func main() { logger := log.New(os.Stdout, "", log.LstdFlags) s := app.NewServer(app.Config{ - BasePath: strings.TrimRight(*basePath, "/"), - DataDir: *dataDir, - Secret: secKey, - CookieSecure: *cookieSecure, + BasePath: strings.TrimRight(*basePath, "/"), + DataDir: *dataDir, + Secret: secKey, + CookieSecure: *cookieSecure, TrustedProxies: trusted, - NtfyBin: *ntfyBin, - NtfyConfig: *ntfyConfig, - NtfyTimeout: *reqTimeout, - Logger: logger, + NtfyBin: *ntfyBin, + NtfyConfig: *ntfyConfig, + NtfyTimeout: *reqTimeout, + Logger: logger, }) httpSrv := &http.Server{ Addr: *listenAddr, Handler: s.Handler(), - ReadTimeout: 15 * time.Second, - ReadHeaderTimeout: 10 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 60 * time.Second, - MaxHeaderBytes: 1 << 20, + ReadTimeout: 15 * time.Second, + ReadHeaderTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 1 << 20, } go func() { diff --git a/internal/ntfy/ntfy.go b/internal/ntfy/ntfy.go index 322d526..ab5fae0 100644 --- a/internal/ntfy/ntfy.go +++ b/internal/ntfy/ntfy.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "os" "os/exec" "regexp" "strings" @@ -45,41 +46,67 @@ func (c *Client) Run(ctx context.Context, args []string, env map[string]string, if c.Bin == "" { return "", "", 0, errors.New("ntfy binary not set") } + + // Many (newer) ntfy versions support `--config/-c` for server-side commands + // (serve/user/access/token). Some older builds do not. We try with --config + // first (if configured) and fall back to running without it if the binary + // rejects the flag. + withConfig := args if c.Config != "" { - args = append([]string{"--config", c.Config}, args...) + withConfig = append([]string{"--config", c.Config}, args...) } + tctx := ctx var cancel context.CancelFunc if c.Timeout > 0 { tctx, cancel = context.WithTimeout(ctx, c.Timeout) defer cancel() } - cmd := exec.CommandContext(tctx, c.Bin, args...) - if stdin != "" { - cmd.Stdin = strings.NewReader(stdin) + // helper to execute once + runOnce := func(a []string) (string, string, int, error) { + cmd := exec.CommandContext(tctx, c.Bin, a...) + if stdin != "" { + cmd.Stdin = strings.NewReader(stdin) + } + var outb, errb bytes.Buffer + cmd.Stdout = &outb + cmd.Stderr = &errb + if env != nil { + // inherit env + add/override provided vars + cmd.Env = append([]string{}, os.Environ()...) + for k, v := range env { + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) + } + } + err := cmd.Run() + exit := 0 + if err != nil { + var ee *exec.ExitError + if errors.As(err, &ee) { + exit = ee.ExitCode() + } else if errors.Is(err, context.DeadlineExceeded) { + return outb.String(), errb.String(), -1, fmt.Errorf("ntfy timeout") + } else { + return outb.String(), errb.String(), -1, err + } + } + return outb.String(), errb.String(), exit, nil } - var outb, errb bytes.Buffer - cmd.Stdout = &outb - cmd.Stderr = &errb - if env != nil { - // inherit env automatically - for k, v := range env { - cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) + + // first attempt (maybe with --config) + out, errOut, exit, err := runOnce(withConfig) + if c.Config == "" { + return out, errOut, exit, err + } + // fallback for older ntfy binaries that don't know --config + if exit != 0 { + errTrim := strings.TrimSpace(errOut) + if strings.Contains(errTrim, "flag provided but not defined: -config") || + strings.Contains(errTrim, "unknown flag") && strings.Contains(errTrim, "config") { + return runOnce(args) } } - err := cmd.Run() - exit := 0 - if err != nil { - var ee *exec.ExitError - if errors.As(err, &ee) { - exit = ee.ExitCode() - } else if errors.Is(err, context.DeadlineExceeded) { - return outb.String(), errb.String(), -1, fmt.Errorf("ntfy timeout") - } else { - return outb.String(), errb.String(), -1, err - } - } - return outb.String(), errb.String(), exit, nil + return out, errOut, exit, err } func (c *Client) ListUsers(ctx context.Context) ([]User, error) {