Compare commits

..

20 Commits

Author SHA1 Message Date
yusing
f79a15bac6 update license 2025-05-01 07:29:48 +08:00
yusing
2b4a70a550 fix(docker): fixed retry mechanism 2025-05-01 06:48:38 +08:00
yusing
f06741428c fix(idlewatcher): log error and retry instead instead of stopping 2025-05-01 06:46:24 +08:00
yusing
16e6e72454 feat(access_log): dynamic buffer size 2025-05-01 05:57:02 +08:00
yusing
100d2c392f chore: memory optimization for access log 2025-04-30 18:30:46 +08:00
yusing
829eb08e37 feat: tunable rotate interval 2025-04-30 18:19:00 +08:00
yusing
53d54a09b0 fix: rotate result file size, add "saved" and omit empty values 2025-04-30 18:17:09 +08:00
yusing
62c551c7fe fix: tests 2025-04-30 17:42:51 +08:00
yusing
80e59bb481 fix: nil panic on unmarshaling zero value 2025-04-30 12:06:49 +08:00
yusing
7a5afc3612 fix; compose example 2025-04-30 04:03:11 +08:00
yusing
2c0349c11c chore: remove debug statement 2025-04-30 00:14:53 +08:00
yusing
8e3c2cc8d4 fix: issues when using socket-proxy 2025-04-29 23:56:15 +08:00
yusing
d35afdb3c9 security: exclude socket-proxy from proxying 2025-04-29 16:23:30 +08:00
yusing
ae093ebf40 docs: update wiki URL, add website URL 2025-04-29 15:22:31 +08:00
yusing
aa8af4185b chore: update schema url 2025-04-29 14:45:38 +08:00
yusing
0029cf69d6 fix: setup script and compose 2025-04-29 09:24:22 +08:00
Yuzerion
33e400a17e security: run in rootless by default and drop unnecessary caps (#101)
Co-authored-by: yusing <yusing@6uo.me>
2025-04-29 08:42:30 +08:00
yusing
1d22bcfed9 fix(access_log): file size calculation 2025-04-29 07:33:51 +08:00
yusing
978d82060e docs: move schema to frontend 2025-04-29 07:26:14 +08:00
yusing
7aa1215491 refactor: rename Deserialize to MapUnmarshalValidate 2025-04-29 07:26:14 +08:00
95 changed files with 474 additions and 2731 deletions

View File

@@ -4,6 +4,10 @@ TAG=latest
# set timezone to get correct log timestamp
TZ=ETC/UTC
# container uid and gid (must match the owner of mounted directories)
GODOXY_UID=1000
GODOXY_GID=1000
# API JWT Configuration (common)
# generate secret with `openssl rand -base64 32`
GODOXY_API_JWT_SECRET=
@@ -44,9 +48,19 @@ GODOXY_API_PASSWORD=password
GODOXY_HTTP_ADDR=:80
GODOXY_HTTPS_ADDR=:443
# Enable HTTP3
GODOXY_HTTP3_ENABLED=true
# API listening address
GODOXY_API_ADDR=127.0.0.1:8888
# Metrics
GODOXY_METRICS_DISABLE_CPU=false
GODOXY_METRICS_DISABLE_MEMORY=false
GODOXY_METRICS_DISABLE_DISK=false
GODOXY_METRICS_DISABLE_NETWORK=false
GODOXY_METRICS_DISABLE_SENSORS=false
# Frontend listening port
GODOXY_FRONTEND_PORT=3000
@@ -56,6 +70,7 @@ GODOXY_FRONTEND_ALIASES=godoxy
# Docker socket
# /var/run/podman/podman.sock for podman
DOCKER_SOCKET=/var/run/docker.sock
SOCKET_PROXY_LISTEN_ADDR=127.0.0.1:2375
# Debug mode
GODOXY_DEBUG=false

View File

@@ -1,10 +1,10 @@
{
"yaml.schemas": {
"https://github.com/yusing/go-proxy/raw/main/schemas/config.schema.json": [
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/config.schema.json": [
"config.example.yml",
"config.yml"
],
"https://github.com/yusing/go-proxy/raw/main/schemas/routes.schema.json": [
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/routes.schema.json": [
"providers.example.yml"
]
}

View File

@@ -6,6 +6,16 @@ HEALTHCHECK NONE
# trunk-ignore(hadolint/DL3018)
RUN apk add --no-cache tzdata make libcap-setcap
ENV GOPATH=/root/go
WORKDIR /src
COPY go.mod go.sum ./
COPY agent ./agent
COPY internal/dnsproviders ./internal/dnsproviders
RUN go mod download -x
# Stage 2: builder
FROM deps AS builder
@@ -17,12 +27,6 @@ COPY internal ./internal
COPY pkg ./pkg
COPY agent ./agent
# Only copy go.mod and go.sum initially for better caching
COPY go.mod go.sum /src/
ENV GOPATH=/root/go
RUN go mod download -x
ARG VERSION
ENV VERSION=${VERSION}
@@ -31,9 +35,8 @@ ENV MAKE_ARGS=${MAKE_ARGS}
ENV GOCACHE=/root/.cache/go-build
ENV GOPATH=/root/go
RUN make ${MAKE_ARGS} build link-binary && \
mv bin /app/ && \
mkdir -p /app/error_pages /app/certs
RUN make ${MAKE_ARGS} docker=1 build
# Stage 3: Final image
FROM scratch
@@ -45,10 +48,7 @@ LABEL proxy.exclude=1
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# copy binary
COPY --from=builder /app /app
# copy example config
COPY config.example.yml /app/config/config.yml
COPY --from=builder /app/run /app/run
# copy certs
COPY --from=builder /etc/ssl/certs /etc/ssl/certs

26
LICENSE
View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 [fullname]
Copyright (c) 2024 - present Yusing
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,3 +19,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
internal/net/gphttp/reverseproxy/reverse_proxy_mod.go is copied from et/http/httputil/reverseproxy.go with modifications to adapt to this project.
Copyright 2011 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
---
internal/utils/io.go has a modified version of io.Copy with context and HTTP flusher handling.
Copyright 2009 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
---
internal/utils/strutils/split_join.go is copied from strings.Split and strings.Join with modifications to adapt to this project.
Copyright 2009 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.

View File

@@ -1,3 +1,4 @@
shell := /bin/sh
export VERSION ?= $(shell git describe --tags --abbrev=0)
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
export GOOS = linux
@@ -59,20 +60,29 @@ else
SETCAP_CMD = sudo setcap
endif
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
POST_BUILD = $(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH};
ifeq ($(docker), 1)
POST_BUILD += mkdir -p /app && mv ${BIN_PATH} /app/run;
endif
.PHONY: debug
test:
GODOXY_TEST=1 go test ./internal/...
docker-build-test:
docker build -t godoxy .
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
get:
for dir in ${PWD} ${PWD}/agent; do cd $$dir && go get -u ./... && go mod tidy; done
build:
mkdir -p bin
mkdir -p $(shell dirname ${BIN_PATH})
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ${CMD_PATH}
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
$(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep ${BIN_PATH}
${POST_BUILD}
run:
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH}
@@ -82,7 +92,7 @@ debug:
sh -c 'HTTP_ADDR=:81 HTTPS_ADDR=:8443 API_ADDR=:8899 DEBUG=1 bin/godoxy-test'
mtrace:
bin/godoxy debug-ls-mtrace > mtrace.json
${BIN_PATH} debug-ls-mtrace > mtrace.json
rapid-crash:
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
@@ -99,8 +109,5 @@ ci-test:
cloc:
cloc --not-match-f '_test.go$$' cmd internal pkg
link-binary:
ln -s /app/${NAME} bin/run
push-github:
git push origin $(shell git rev-parse --abbrev-ref HEAD)

View File

@@ -10,9 +10,11 @@
A lightweight, simple, and [performant](https://github.com/yusing/godoxy/wiki/Benchmarks) reverse proxy with WebUI.
For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki)**
<h5>
<a href="https://docs.godoxy.dev">Website</a> | <a href="https://docs.godoxy.dev/Home.html">Wiki</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
</h5>
**EN** | <a href="README_CHT.md">中文</a>
<h5>EN | <a href="README_CHT.md">中文</a></h5>
<img src="screenshots/webui.jpg" style="max-width: 650">

View File

@@ -10,9 +10,11 @@
輕量、易用、 [高效能](https://github.com/yusing/godoxy/wiki/Benchmarks),且帶有主頁和配置面板的反向代理
完整文檔請查閱 **[Wiki](https://github.com/yusing/godoxy/wiki)**(暫未有中文翻譯)
<h5>
<a href="https://docs.godoxy.dev">網站</a> | <a href="https://docs.godoxy.dev/Home.html">文檔</a> | <a href="https://discord.gg/umReR62nRd">Discord</a>
</h5>
<a href="README.md">EN</a> | **中文**
<h5><a href="README.md">EN</a> | 中文</h5>
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">

View File

@@ -1,21 +1,48 @@
---
services:
socket-proxy:
container_name: socket-proxy
image: lscr.io/linuxserver/socket-proxy:latest
environment:
- ALLOW_START=1
- ALLOW_STOP=1
- ALLOW_RESTARTS=1
- CONTAINERS=1
- EVENTS=1
- INFO=1
- PING=1
- POST=1
- VERSION=1
volumes:
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
restart: unless-stopped
tmpfs:
- /run
ports:
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
labels:
proxy.exclude: true
frontend:
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
container_name: godoxy-frontend
restart: unless-stopped
network_mode: host # do not change this
env_file: .env
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- all
depends_on:
- app
environment:
HOSTNAME: 127.0.0.1
PORT: ${GODOXY_FRONTEND_PORT:-3000}
# modify below to fit your needs
labels:
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
proxy.godoxy.port: ${GODOXY_FRONTEND_PORT:-3000}
# proxy.godoxy.middlewares.cidr_whitelist: |
proxy.#1.port: ${GODOXY_FRONTEND_PORT:-3000}
# proxy.#1.middlewares.cidr_whitelist: |
# status: 403
# message: IP not allowed
# allow:
@@ -29,11 +56,22 @@ services:
restart: always
network_mode: host # do not change this
env_file: .env
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
depends_on:
socket-proxy:
condition: service_started
security_opt:
- no-new-privileges:true
cap_drop:
- all
cap_add:
- NET_BIND_SERVICE
environment:
- DOCKER_HOST=tcp://${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}
volumes:
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
- ./config:/app/config
- ./logs:/app/logs
- ./error_pages:/app/error_pages
- ./error_pages:/app/error_pages:ro
- ./data:/app/data
# To use autocert, certs will be stored in "./certs".

View File

@@ -98,7 +98,7 @@ func TestUserPassLoginCallbackHandler(t *testing.T) {
Host: "app.example.com",
Body: io.NopCloser(bytes.NewReader(Must(json.Marshal(tt.creds)))),
}
auth.LoginHandler(w, req)
auth.PostAuthCallbackHandler(w, req)
if tt.wantErr {
ExpectEqual(t, w.Code, http.StatusUnauthorized)
} else {

View File

@@ -45,6 +45,6 @@ oauth2_config:
testYaml = testYaml[1:] // remove first \n
opt := make(map[string]any)
require.NoError(t, yaml.Unmarshal([]byte(testYaml), &opt))
require.NoError(t, utils.Deserialize(opt, cfg))
require.NoError(t, utils.MapUnmarshalValidate(opt, cfg))
require.Equal(t, cfg, cfgExpected)
}

View File

@@ -16,7 +16,7 @@ func DNSProvider[CT any, PT challenge.Provider](
) Generator {
return func(opt map[string]any) (challenge.Provider, gperr.Error) {
cfg := defaultCfg()
err := utils.Deserialize(opt, &cfg)
err := utils.MapUnmarshalValidate(opt, &cfg)
if err != nil {
return nil, err
}

View File

@@ -220,7 +220,7 @@ func (cfg *Config) load() gperr.Error {
}
model := config.DefaultConfig()
if err := utils.DeserializeYAML(data, model); err != nil {
if err := utils.UnmarshalValidateYAML(data, model); err != nil {
gperr.LogFatal(errMsg, err)
}

View File

@@ -88,7 +88,7 @@ func HasInstance() bool {
func Validate(data []byte) gperr.Error {
var model Config
return utils.DeserializeYAML(data, &model)
return utils.UnmarshalValidateYAML(data, &model)
}
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)

View File

@@ -126,11 +126,27 @@ func (c *Container) isDatabase() bool {
return false
}
func (c *Container) isLocal() bool {
if strings.HasPrefix(c.DockerHost, "unix://") {
return true
}
url, err := url.Parse(c.DockerHost)
if err != nil {
return false
}
switch url.Hostname() {
case "localhost", "127.0.0.1", "::1":
return true
default:
return false
}
}
func (c *Container) setPublicHostname() {
if !c.Running {
return
}
if strings.HasPrefix(c.DockerHost, "unix://") {
if c.isLocal() {
c.PublicHostname = "127.0.0.1"
return
}
@@ -144,18 +160,17 @@ func (c *Container) setPublicHostname() {
}
func (c *Container) setPrivateHostname(helper containerHelper) {
if !strings.HasPrefix(c.DockerHost, "unix://") && c.Agent == nil {
if !c.isLocal() && c.Agent == nil {
return
}
if helper.NetworkSettings == nil {
return
}
for _, v := range helper.NetworkSettings.Networks {
if v.IPAddress == "" {
continue
if v.IPAddress != "" {
c.PrivateHostname = v.IPAddress
return
}
c.PrivateHostname = v.IPAddress
return
}
}
@@ -178,7 +193,7 @@ func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
ContainerName: c.ContainerName,
},
}
err := utils.Deserialize(cfg, idwCfg)
err := utils.MapUnmarshalValidate(cfg, idwCfg)
if err != nil {
gperr.LogWarn("invalid idlewatcher config", gperr.PrependSubject(c.ContainerName, err))
} else {

View File

@@ -262,7 +262,7 @@ func (w *Watcher) watchUntilDestroy() (returnCause gperr.Error) {
case <-w.task.Context().Done():
return gperr.Wrap(w.task.FinishCause())
case err := <-errCh:
return err
gperr.LogError("watcher error", err, &w.l)
case e := <-eventCh:
w.l.Debug().Stringer("action", e.Action).Msg("state changed")
if e.Action == events.ActionContainerDestroy {

View File

@@ -13,15 +13,16 @@ func TestNewJSON(t *testing.T) {
}
func TestSaveLoadStore(t *testing.T) {
defer clear(stores)
storesPath = t.TempDir()
store := Store[string]("test")
store.Store("a", "1")
if err := save(); err != nil {
t.Fatal(err)
}
if err := load(); err != nil {
t.Fatal(err)
}
// reload
clear(stores)
loaded := Store[string]("test")
v, ok := loaded.Load("a")
if !ok {
@@ -43,6 +44,8 @@ type testObject struct {
func (*testObject) Initialize() {}
func TestSaveLoadObject(t *testing.T) {
defer clear(stores)
storesPath = t.TempDir()
obj := Object[*testObject]("test")
obj.I = 1
@@ -50,9 +53,8 @@ func TestSaveLoadObject(t *testing.T) {
if err := save(); err != nil {
t.Fatal(err)
}
if err := load(); err != nil {
t.Fatal(err)
}
// reload
clear(stores)
loaded := Object[*testObject]("test")
if loaded.I != 1 || loaded.S != "1" {
t.Fatalf("expected 1, got %d, %s", loaded.I, loaded.S)

View File

@@ -4,8 +4,8 @@ import (
"bufio"
"io"
"net/http"
"os"
"sync"
"sync/atomic"
"time"
"github.com/rs/zerolog"
@@ -13,6 +13,7 @@ import (
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/internal/utils/synk"
"golang.org/x/time/rate"
)
@@ -22,12 +23,17 @@ type (
task *task.Task
cfg *Config
rawWriter io.Writer
closer []io.Closer
supportRotate []supportRotate
writer *bufio.Writer
writeLock sync.Mutex
closed bool
wps int64
bufSize int
lastAdjust time.Time
lineBufPool *synk.BytesPool // buffer pool for formatting a single log line
errRateLimiter *rate.Limiter
@@ -60,15 +66,13 @@ type (
)
const (
StdoutbufSize = 64
MinBufferSize = 4 * kilobyte
MaxBufferSize = 1 * megabyte
MaxBufferSize = 8 * megabyte
bufferAdjustInterval = time.Second // How often we check & adjust
)
const (
flushInterval = 30 * time.Second
rotateInterval = time.Hour
)
const defaultRotateInterval = time.Hour
const (
errRateLimit = 200 * time.Millisecond
@@ -105,24 +109,17 @@ func unwrap[Writer any](w io.Writer) []Writer {
func NewAccessLoggerWithIO(parent task.Parent, writer WriterWithName, anyCfg AnyConfig) *AccessLogger {
cfg := anyCfg.ToConfig()
if cfg.BufferSize == 0 {
cfg.BufferSize = DefaultBufferSize
}
if cfg.BufferSize < MinBufferSize {
cfg.BufferSize = MinBufferSize
}
if cfg.BufferSize > MaxBufferSize {
cfg.BufferSize = MaxBufferSize
}
if _, ok := writer.(*os.File); ok {
cfg.BufferSize = StdoutbufSize
if cfg.RotateInterval == 0 {
cfg.RotateInterval = defaultRotateInterval
}
l := &AccessLogger{
task: parent.Subtask("accesslog."+writer.Name(), true),
cfg: cfg,
writer: bufio.NewWriterSize(writer, cfg.BufferSize),
lineBufPool: synk.NewBytesPool(512, 8192),
rawWriter: writer,
writer: bufio.NewWriterSize(writer, MinBufferSize),
bufSize: MinBufferSize,
lineBufPool: synk.NewBytesPool(256, 768), // for common/combined usually < 256B; for json < 512B
errRateLimiter: rate.NewLimiter(rate.Every(errRateLimit), errBurst),
logger: logging.With().Str("file", writer.Name()).Logger(),
}
@@ -220,9 +217,9 @@ func (l *AccessLogger) Rotate() (result *RotateResult, err error) {
func (l *AccessLogger) handleErr(err error) {
if l.errRateLimiter.Allow() {
gperr.LogError("failed to write access log", err)
gperr.LogError("failed to write access log", err, &l.logger)
} else {
gperr.LogError("too many errors, stopping access log", err)
gperr.LogError("too many errors, stopping access log", err, &l.logger)
l.task.Finish(err)
}
}
@@ -234,19 +231,16 @@ func (l *AccessLogger) start() {
l.task.Finish(nil)
}()
// flushes the buffer every 30 seconds
flushTicker := time.NewTicker(30 * time.Second)
defer flushTicker.Stop()
rotateTicker := time.NewTicker(rotateInterval)
rotateTicker := time.NewTicker(l.cfg.RotateInterval)
defer rotateTicker.Stop()
bufAdjTicker := time.NewTicker(bufferAdjustInterval)
defer bufAdjTicker.Stop()
for {
select {
case <-l.task.Context().Done():
return
case <-flushTicker.C:
l.Flush()
case <-rotateTicker.C:
if !l.ShouldRotate() {
continue
@@ -259,6 +253,8 @@ func (l *AccessLogger) start() {
} else {
l.logger.Info().Msg("no rotation needed")
}
case <-bufAdjTicker.C:
l.adjustBuffer()
}
}
}
@@ -295,8 +291,55 @@ func (l *AccessLogger) write(data []byte) {
if l.closed {
return
}
_, err := l.writer.Write(data)
n, err := l.writer.Write(data)
if err != nil {
l.handleErr(err)
} else if n < len(data) {
l.handleErr(gperr.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, len(data), n))
}
atomic.AddInt64(&l.wps, int64(n))
}
func (l *AccessLogger) adjustBuffer() {
wps := int(atomic.SwapInt64(&l.wps, 0))
origBufSize := l.bufSize
newBufSize := origBufSize
halfDiff := (wps - origBufSize) / 2
if halfDiff < 0 {
halfDiff = -halfDiff
}
step := max(halfDiff, wps/2)
switch {
case origBufSize < wps:
newBufSize += step
if newBufSize > MaxBufferSize {
newBufSize = MaxBufferSize
}
case origBufSize > wps:
newBufSize -= step
if newBufSize < MinBufferSize {
newBufSize = MinBufferSize
}
}
if newBufSize == origBufSize {
return
}
l.writeLock.Lock()
defer l.writeLock.Unlock()
if l.closed {
return
}
l.logger.Info().
Str("wps", strutils.FormatByteSize(wps)).
Str("old", strutils.FormatByteSize(origBufSize)).
Str("new", strutils.FormatByteSize(newBufSize)).
Msg("adjusted buffer size")
l.writer = bufio.NewWriterSize(l.rawWriter, newBufSize)
l.bufSize = newBufSize
}

View File

@@ -26,12 +26,8 @@ type BackScanner struct {
// NewBackScanner creates a new Scanner to read the file backward.
// chunkSize determines the size of each read chunk from the end of the file.
func NewBackScanner(file ReaderAtSeeker, chunkSize int) *BackScanner {
size, err := file.Seek(0, io.SeekEnd)
if err != nil {
return &BackScanner{err: err}
}
return newBackScanner(file, size, make([]byte, chunkSize))
func NewBackScanner(file ReaderAtSeeker, fileSize int64, chunkSize int) *BackScanner {
return newBackScanner(file, fileSize, make([]byte, chunkSize))
}
func newBackScanner(file ReaderAtSeeker, fileSize int64, buf []byte) *BackScanner {
@@ -111,11 +107,6 @@ func (s *BackScanner) Bytes() []byte {
return s.line
}
// FileSize returns the size of the file.
func (s *BackScanner) FileSize() int64 {
return s.size
}
// Err returns the first non-EOF error encountered by the scanner.
func (s *BackScanner) Err() error {
return s.err

View File

@@ -67,7 +67,7 @@ func TestBackScanner(t *testing.T) {
}
// Create scanner with small chunk size to test chunking
scanner := NewBackScanner(mockFile, 10)
scanner := NewBackScanner(mockFile, mockFile.MustSize(), 10)
// Collect all lines
var lines [][]byte
@@ -108,7 +108,7 @@ func TestBackScannerWithVaryingChunkSizes(t *testing.T) {
t.Fatalf("failed to write to mock file: %v", err)
}
scanner := NewBackScanner(mockFile, chunkSize)
scanner := NewBackScanner(mockFile, mockFile.MustSize(), chunkSize)
var lines [][]byte
for scanner.Scan() {
@@ -170,7 +170,8 @@ func TestReset(t *testing.T) {
}
}
linesRead := 0
s := NewBackScanner(file, defaultChunkSize)
stat, _ := file.Stat()
s := NewBackScanner(file, stat.Size(), defaultChunkSize)
for s.Scan() {
linesRead++
}
@@ -199,7 +200,7 @@ func BenchmarkBackScanner(b *testing.B) {
}
for i := range 14 {
chunkSize := (2 << i) * kilobyte
scanner := NewBackScanner(mockFile, chunkSize)
scanner := NewBackScanner(mockFile, mockFile.MustSize(), chunkSize)
name := strutils.FormatByteSize(chunkSize)
b.ResetTimer()
b.Run(name, func(b *testing.B) {
@@ -226,7 +227,8 @@ func BenchmarkBackScannerRealFile(b *testing.B) {
}
}
scanner := NewBackScanner(file, 256*kilobyte)
stat, _ := file.Stat()
scanner := NewBackScanner(file, stat.Size(), 256*kilobyte)
b.ResetTimer()
for scanner.Scan() {
}

View File

@@ -1,16 +1,19 @@
package accesslog
import (
"time"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/utils"
)
type (
ConfigBase struct {
BufferSize int `json:"buffer_size"`
Path string `json:"path"`
Stdout bool `json:"stdout"`
Retention *Retention `json:"retention" aliases:"keep"`
B int `json:"buffer_size"` // Deprecated: buffer size is adjusted dynamically
Path string `json:"path"`
Stdout bool `json:"stdout"`
Retention *Retention `json:"retention" aliases:"keep"`
RotateInterval time.Duration `json:"rotate_interval,omitempty"`
}
ACLLoggerConfig struct {
ConfigBase
@@ -55,8 +58,6 @@ var (
ReqLoggerFormats = []Format{FormatCommon, FormatCombined, FormatJSON}
)
const DefaultBufferSize = 64 * kilobyte // 64KB
func (cfg *ConfigBase) Validate() gperr.Error {
if cfg.Path == "" && !cfg.Stdout {
return gperr.New("path or stdout is required")
@@ -99,8 +100,7 @@ func (cfg *RequestLoggerConfig) ToConfig() *Config {
func DefaultRequestLoggerConfig() *RequestLoggerConfig {
return &RequestLoggerConfig{
ConfigBase: ConfigBase{
BufferSize: DefaultBufferSize,
Retention: &Retention{Days: 30},
Retention: &Retention{Days: 30},
},
Format: FormatCombined,
Fields: Fields{
@@ -120,8 +120,7 @@ func DefaultRequestLoggerConfig() *RequestLoggerConfig {
func DefaultACLLoggerConfig() *ACLLoggerConfig {
return &ACLLoggerConfig{
ConfigBase: ConfigBase{
BufferSize: DefaultBufferSize,
Retention: &Retention{Days: 30},
Retention: &Retention{Days: 30},
},
}
}

View File

@@ -11,7 +11,6 @@ import (
func TestNewConfig(t *testing.T) {
labels := map[string]string{
"proxy.buffer_size": "10",
"proxy.format": "combined",
"proxy.path": "/tmp/access.log",
"proxy.filters.status_codes.values": "200-299",
@@ -30,10 +29,9 @@ func TestNewConfig(t *testing.T) {
expect.NoError(t, err)
var config RequestLoggerConfig
err = utils.Deserialize(parsed, &config)
err = utils.MapUnmarshalValidate(parsed, &config)
expect.NoError(t, err)
expect.Equal(t, config.BufferSize, 10)
expect.Equal(t, config.Format, FormatCombined)
expect.Equal(t, config.Path, "/tmp/access.log")
expect.Equal(t, config.Filters.StatusCodes.Values, []*StatusCodeRange{{Start: 200, End: 299}})

View File

@@ -72,6 +72,14 @@ func (f *File) Seek(offset int64, whence int) (int64, error) {
return f.f.Seek(offset, whence)
}
func (f *File) Size() (int64, error) {
stat, err := f.f.Stat()
if err != nil {
return 0, err
}
return stat.Size(), nil
}
func (f *File) Truncate(size int64) error {
return f.f.Truncate(size)
}

View File

@@ -50,7 +50,6 @@ func TestConcurrentAccessLoggerLogAndFlush(t *testing.T) {
file := NewMockFile()
cfg := DefaultRequestLoggerConfig()
cfg.BufferSize = 1024
parent := task.RootTask("test", false)
loggerCount := 5

View File

@@ -17,8 +17,11 @@ type MockFile struct {
noLock
}
var _ SupportRotate = (*MockFile)(nil)
func NewMockFile() *MockFile {
f, _ := afero.TempFile(afero.NewMemMapFs(), "", "")
f.Seek(0, io.SeekEnd)
return &MockFile{
File: f,
}
@@ -47,3 +50,13 @@ func (m *MockFile) NumLines() int {
}
return count
}
func (m *MockFile) Size() (int64, error) {
stat, _ := m.Stat()
return stat.Size(), nil
}
func (m *MockFile) MustSize() int64 {
size, _ := m.Size()
return size
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/rs/zerolog"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/internal/utils/synk"
@@ -16,6 +17,7 @@ type supportRotate interface {
io.ReaderAt
io.WriterAt
Truncate(size int64) error
Size() (int64, error)
}
type RotateResult struct {
@@ -29,17 +31,29 @@ type RotateResult struct {
}
func (r *RotateResult) Print(logger *zerolog.Logger) {
logger.Info().
Str("original_size", strutils.FormatByteSize(r.OriginalSize)).
Str("bytes_read", strutils.FormatByteSize(r.NumBytesRead)).
Str("bytes_keep", strutils.FormatByteSize(r.NumBytesKeep)).
Int("lines_read", r.NumLinesRead).
Int("lines_keep", r.NumLinesKeep).
Int("lines_invalid", r.NumLinesInvalid).
event := logger.Info().
Str("original_size", strutils.FormatByteSize(r.OriginalSize))
if r.NumBytesRead > 0 {
event.Str("bytes_read", strutils.FormatByteSize(r.NumBytesRead))
}
if r.NumBytesKeep > 0 {
event.Str("bytes_keep", strutils.FormatByteSize(r.NumBytesKeep))
}
if r.NumLinesRead > 0 {
event.Int("lines_read", r.NumLinesRead)
}
if r.NumLinesKeep > 0 {
event.Int("lines_keep", r.NumLinesKeep)
}
if r.NumLinesInvalid > 0 {
event.Int("lines_invalid", r.NumLinesInvalid)
}
event.Str("saved", strutils.FormatByteSize(r.OriginalSize-r.NumBytesKeep)).
Msg("log rotate result")
}
func (r *RotateResult) Add(other *RotateResult) {
r.OriginalSize += other.OriginalSize
r.NumBytesRead += other.NumBytesRead
r.NumBytesKeep += other.NumBytesKeep
r.NumLinesRead += other.NumLinesRead
@@ -96,16 +110,21 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention) (result *Rotat
return nil, nil // should not happen
}
s := NewBackScanner(file, defaultChunkSize)
result = &RotateResult{
OriginalSize: s.FileSize(),
fileSize, err := file.Size()
if err != nil {
return nil, err
}
// nothing to rotate, return the nothing
if result.OriginalSize == 0 {
if fileSize == 0 {
return nil, nil
}
s := NewBackScanner(file, fileSize, defaultChunkSize)
result = &RotateResult{
OriginalSize: fileSize,
}
// Store the line positions and sizes we want to keep
linesToKeep := make([]lineInfo, 0)
lastLineValid := false
@@ -183,6 +202,8 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention) (result *Rotat
// Write it to the new position
if _, err := file.WriteAt(buf, writePos); err != nil {
return nil, err
} else if n < line.Size {
return nil, gperr.Errorf("%w, writing %d bytes, only %d written", io.ErrShortWrite, line.Size, n)
}
writePos += n
}
@@ -201,7 +222,7 @@ func rotateLogFileByPolicy(file supportRotate, config *Retention) (result *Rotat
//
// Invalid lines will not be detected and included in the result.
func rotateLogFileBySize(file supportRotate, config *Retention) (result *RotateResult, err error) {
filesize, err := file.Seek(0, io.SeekEnd)
filesize, err := file.Size()
if err != nil {
return nil, err
}

View File

@@ -118,7 +118,7 @@ func (m *Middleware) apply(optsRaw OptionsRaw) gperr.Error {
} else {
m.priority = DefaultPriority
}
return utils.Deserialize(optsRaw, m.impl)
return utils.MapUnmarshalValidate(optsRaw, m.impl)
}
func (m *Middleware) finalize() error {

View File

@@ -47,7 +47,7 @@ func (cfg *NotificationConfig) UnmarshalMap(m map[string]any) (err gperr.Error)
}
// unmarshal provider config
if err := utils.Deserialize(m, cfg.Provider); err != nil {
if err := utils.MapUnmarshalValidate(m, cfg.Provider); err != nil {
return err
}

View File

@@ -150,7 +150,7 @@ func TestNotificationConfig(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
var cfg NotificationConfig
provider := tt.cfg["provider"]
err := utils.Deserialize(tt.cfg, &cfg)
err := utils.MapUnmarshalValidate(tt.cfg, &cfg)
if tt.wantErr {
ExpectHasError(t, err)
} else {

View File

@@ -172,7 +172,7 @@ func (p *DockerProvider) routesFromContainerLabels(container *docker.Container)
}
// deserialize map into entry object
err := U.Deserialize(entryMap, r)
err := U.MapUnmarshalValidate(entryMap, r)
if err != nil {
errs.Add(err.Subject(alias))
} else {
@@ -181,7 +181,7 @@ func (p *DockerProvider) routesFromContainerLabels(container *docker.Container)
}
if wildcardProps != nil {
for _, re := range routes {
if err := U.Deserialize(wildcardProps, re); err != nil {
if err := U.MapUnmarshalValidate(wildcardProps, re); err != nil {
errs.Add(err.Subject(docker.WildcardAlias))
break
}

View File

@@ -34,7 +34,7 @@ func FileProviderImpl(filename string) (ProviderImpl, error) {
}
func validate(data []byte) (routes route.Routes, err gperr.Error) {
err = utils.DeserializeYAML(data, &routes)
err = utils.UnmarshalValidateYAML(data, &routes)
return
}

View File

@@ -28,7 +28,7 @@ func TestParseRule(t *testing.T) {
var rules struct {
Rules Rules
}
err := utils.Deserialize(utils.SerializedObject{"rules": test}, &rules)
err := utils.MapUnmarshalValidate(utils.SerializedObject{"rules": test}, &rules)
ExpectNoError(t, err)
ExpectEqual(t, len(rules.Rules), len(test))
ExpectEqual(t, rules.Rules[0].Name, "test")

View File

@@ -40,7 +40,7 @@ func TestHTTPConfigDeserialize(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
cfg := Route{}
tt.input["host"] = "internal"
err := utils.Deserialize(tt.input, &cfg)
err := utils.MapUnmarshalValidate(tt.input, &cfg)
if err != nil {
expect.NoError(t, err)
}

View File

@@ -170,8 +170,8 @@ func dive(dst reflect.Value) (v reflect.Value, t reflect.Type, err gperr.Error)
}
}
// Deserialize takes a SerializedObject and a target value, and assigns the values in the SerializedObject to the target value.
// Deserialize ignores case differences between the field names in the SerializedObject and the target.
// MapUnmarshalValidate takes a SerializedObject and a target value, and assigns the values in the SerializedObject to the target value.
// MapUnmarshalValidate ignores case differences between the field names in the SerializedObject and the target.
//
// The target value must be a struct or a map[string]any.
// If the target value is a struct , and implements the MapUnmarshaller interface,
@@ -183,11 +183,11 @@ func dive(dst reflect.Value) (v reflect.Value, t reflect.Type, err gperr.Error)
// If the target value is a map[string]any the SerializedObject will be deserialized into the map.
//
// The function returns an error if the target value is not a struct or a map[string]any, or if there is an error during deserialization.
func Deserialize(src SerializedObject, dst any) (err gperr.Error) {
return deserialize(src, dst, true)
func MapUnmarshalValidate(src SerializedObject, dst any) (err gperr.Error) {
return mapUnmarshalValidate(src, dst, true)
}
func deserialize(src SerializedObject, dst any, checkValidateTag bool) (err gperr.Error) {
func mapUnmarshalValidate(src SerializedObject, dst any, checkValidateTag bool) (err gperr.Error) {
dstV := reflect.ValueOf(dst)
dstT := dstV.Type()
@@ -314,7 +314,7 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
return gperr.Errorf("convert: dst is %w", ErrNilValue)
}
if !src.IsValid() {
if !src.IsValid() || src.IsZero() {
if dst.CanSet() {
dst.Set(reflect.Zero(dst.Type()))
return nil
@@ -378,7 +378,7 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
if !ok {
return ErrUnsupportedConversion.Subject(dstT.String() + " to " + srcT.String())
}
return deserialize(obj, dst.Addr().Interface(), checkValidateTag)
return mapUnmarshalValidate(obj, dst.Addr().Interface(), checkValidateTag)
case srcKind == reflect.Slice:
if src.Len() == 0 {
return nil
@@ -500,21 +500,21 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
return true, Convert(reflect.ValueOf(tmp), dst, true)
}
func DeserializeYAML[T any](data []byte, target *T) gperr.Error {
func UnmarshalValidateYAML[T any](data []byte, target *T) gperr.Error {
m := make(map[string]any)
if err := yaml.Unmarshal(data, &m); err != nil {
return gperr.Wrap(err)
}
return Deserialize(m, target)
return MapUnmarshalValidate(m, target)
}
func DeserializeYAMLMap[V any](data []byte) (_ functional.Map[string, V], err gperr.Error) {
func UnmarshalValidateYAMLXSync[V any](data []byte) (_ functional.Map[string, V], err gperr.Error) {
m := make(map[string]any)
if err = gperr.Wrap(yaml.Unmarshal(data, &m)); err != nil {
return
}
m2 := make(map[string]V, len(m))
if err = Deserialize(m, m2); err != nil {
if err = MapUnmarshalValidate(m, m2); err != nil {
return
}
return functional.NewMapFrom(m2), nil

View File

@@ -40,7 +40,7 @@ func TestDeserialize(t *testing.T) {
t.Run("deserialize", func(t *testing.T) {
var s2 S
err := Deserialize(testStructSerialized, &s2)
err := MapUnmarshalValidate(testStructSerialized, &s2)
ExpectNoError(t, err)
ExpectEqual(t, s2, testStruct)
})
@@ -60,13 +60,13 @@ func TestDeserializeAnonymousField(t *testing.T) {
}
// all, anon := extractFields(reflect.TypeOf(s2))
// t.Fatalf("anon %v, all %v", anon, all)
err := Deserialize(map[string]any{"a": 1, "b": 2, "c": 3}, &s)
err := MapUnmarshalValidate(map[string]any{"a": 1, "b": 2, "c": 3}, &s)
ExpectNoError(t, err)
ExpectEqual(t, s.A, 1)
ExpectEqual(t, s.B, 2)
ExpectEqual(t, s.C, 3)
err = Deserialize(map[string]any{"a": 1, "b": 2, "c": 3}, &s2)
err = MapUnmarshalValidate(map[string]any{"a": 1, "b": 2, "c": 3}, &s2)
ExpectNoError(t, err)
ExpectEqual(t, s2.A, 1)
ExpectEqual(t, s2.B, 2)
@@ -148,7 +148,7 @@ func (c *testType) Parse(v string) (err error) {
func TestConvertor(t *testing.T) {
t.Run("valid", func(t *testing.T) {
m := new(testModel)
ExpectNoError(t, Deserialize(map[string]any{"Test": "123"}, m))
ExpectNoError(t, MapUnmarshalValidate(map[string]any{"Test": "123"}, m))
ExpectEqual(t, m.Test.foo, 123)
ExpectEqual(t, m.Test.bar, "123")
@@ -156,18 +156,28 @@ func TestConvertor(t *testing.T) {
t.Run("int_to_string", func(t *testing.T) {
m := new(testModel)
ExpectNoError(t, Deserialize(map[string]any{"Test": "123"}, m))
ExpectNoError(t, MapUnmarshalValidate(map[string]any{"Test": "123"}, m))
ExpectEqual(t, m.Test.foo, 123)
ExpectEqual(t, m.Test.bar, "123")
ExpectNoError(t, Deserialize(map[string]any{"Baz": 123}, m))
ExpectNoError(t, MapUnmarshalValidate(map[string]any{"Baz": 123}, m))
ExpectEqual(t, m.Baz, "123")
})
t.Run("invalid", func(t *testing.T) {
m := new(testModel)
ExpectError(t, ErrUnsupportedConversion, Deserialize(map[string]any{"Test": struct{}{}}, m))
err := MapUnmarshalValidate(map[string]any{"Test": struct{ a int }{1}}, m)
ExpectError(t, ErrUnsupportedConversion, err)
})
t.Run("set_empty", func(t *testing.T) {
m := testModel{
Test: testType{1, "2"},
Baz: "3",
}
ExpectNoError(t, MapUnmarshalValidate(map[string]any{"Test": nil, "Baz": nil}, &m))
ExpectEqual(t, m, testModel{})
})
}

View File

@@ -136,12 +136,19 @@ func (w *DockerWatcher) EventsWithOptions(ctx context.Context, options DockerLis
err = nil
// trigger reload (clear routes)
eventCh <- reloadTrigger
for !w.checkConnection(ctx) {
retry := time.NewTicker(dockerWatcherRetryInterval)
defer retry.Stop()
ok := false
for !ok {
select {
case <-ctx.Done():
return
case <-time.After(dockerWatcherRetryInterval):
continue
case <-retry.C:
if w.checkConnection(ctx) {
ok = true
break
}
}
}
// connection successful, trigger reload (reload routes)

View File

@@ -1,33 +0,0 @@
# To generate schema
# comment out this part from typescript-json-schema.js#L884
#
# if (indexType.flags !== ts.TypeFlags.Number && !isIndexedObject) {
# throw new Error("Not supported: IndexSignatureDeclaration with index symbol other than a number or a string");
# }
gen-schema-single:
bun -bun typescript-json-schema --noExtraProps --required --skipLibCheck --tsNodeRegister=true -o "${OUT}" "${IN}" ${CLASS}
# minify
python3 -c "import json; f=open('${OUT}', 'r'); j=json.load(f); f.close(); f=open('${OUT}', 'w'); json.dump(j, f, separators=(',', ':'));"
gen-schema:
bun -bun tsc
sed -i 's#"type": "module"#"type": "commonjs"#' package.json
make IN=config/config.ts \
CLASS=Config \
OUT=config.schema.json \
gen-schema-single
make IN=providers/routes.ts \
CLASS=Routes \
OUT=routes.schema.json \
gen-schema-single
make IN=middlewares/middleware_compose.ts \
CLASS=MiddlewareCompose \
OUT=middleware_compose.schema.json \
gen-schema-single
make IN=docker.ts \
CLASS=DockerRoutes \
OUT=docker_routes.schema.json \
gen-schema-single
sed -i 's#"type": "commonjs"#"type": "module"#' package.json
bun format:write

View File

@@ -1,120 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "godoxy-schemas",
"devDependencies": {
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-json-schema": "^0.65.1",
},
},
},
"packages": {
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
"@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="],
"@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="],
"@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="],
"@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@18.19.86", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ=="],
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
"diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"path-equal": ["path-equal@1.2.5", "", {}, "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript-json-schema": ["typescript-json-schema@0.65.1", "", { "dependencies": { "@types/json-schema": "^7.0.9", "@types/node": "^18.11.9", "glob": "^7.1.7", "path-equal": "^1.2.5", "safe-stable-stringify": "^2.2.0", "ts-node": "^10.9.1", "typescript": "~5.5.0", "yargs": "^17.1.1" }, "bin": { "typescript-json-schema": "bin/typescript-json-schema" } }, "sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg=="],
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
"typescript-json-schema/typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="],
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,57 +0,0 @@
import { CIDR, HTTPHeader, HTTPMethod, StatusCodeRange, URI } from "../types";
export declare const ACCESS_LOG_FORMATS: readonly [
"combined",
"common",
"json",
];
export type AccessLogFormat = (typeof ACCESS_LOG_FORMATS)[number];
export type AccessLogConfig = {
/**
* The size of the buffer.
*
* @minimum 0
* @default 65536
* @TJS-type integer
*/
buffer_size?: number;
/** The format of the access log.
*
* @default "combined"
*/
format?: AccessLogFormat;
path: URI;
filters?: AccessLogFilters;
fields?: AccessLogFields;
};
export type AccessLogFilter<T> = {
/** Whether the filter is negative.
*
* @default false
*/
negative?: boolean;
values: T[];
};
export type AccessLogFilters = {
status_code?: AccessLogFilter<StatusCodeRange>;
method?: AccessLogFilter<HTTPMethod>;
host?: AccessLogFilter<string>;
headers?: AccessLogFilter<HTTPHeader>;
cidr?: AccessLogFilter<CIDR>;
};
export declare const ACCESS_LOG_FIELD_MODES: readonly [
"keep",
"drop",
"redact",
];
export type AccessLogFieldMode = (typeof ACCESS_LOG_FIELD_MODES)[number];
export type AccessLogField = {
default?: AccessLogFieldMode;
config: {
[key: string]: AccessLogFieldMode;
};
};
export type AccessLogFields = {
header?: AccessLogField;
query?: AccessLogField;
cookie?: AccessLogField;
};

View File

@@ -1,2 +0,0 @@
export const ACCESS_LOG_FORMATS = ["combined", "common", "json"];
export const ACCESS_LOG_FIELD_MODES = ["keep", "drop", "redact"];

View File

@@ -1,66 +0,0 @@
import { CIDR, HTTPHeader, HTTPMethod, StatusCodeRange, URI } from "../types";
export const ACCESS_LOG_FORMATS = ["combined", "common", "json"] as const;
export type AccessLogFormat = (typeof ACCESS_LOG_FORMATS)[number];
export type AccessLogConfig = {
/**
* The size of the buffer.
*
* @minimum 0
* @default 65536
* @TJS-type integer
*/
buffer_size?: number;
/** The format of the access log.
*
* @default "combined"
*/
format?: AccessLogFormat;
/* The path to the access log file. */
path: URI;
/* The access log filters. */
filters?: AccessLogFilters;
/* The access log fields. */
fields?: AccessLogFields;
};
export type AccessLogFilter<T> = {
/** Whether the filter is negative.
*
* @default false
*/
negative?: boolean;
/* The values to filter. */
values: T[];
};
export type AccessLogFilters = {
/* Status code filter. */
status_code?: AccessLogFilter<StatusCodeRange>;
/* Method filter. */
method?: AccessLogFilter<HTTPMethod>;
/* Host filter. */
host?: AccessLogFilter<string>;
/* Header filter. */
headers?: AccessLogFilter<HTTPHeader>;
/* CIDR filter. */
cidr?: AccessLogFilter<CIDR>;
};
export const ACCESS_LOG_FIELD_MODES = ["keep", "drop", "redact"] as const;
export type AccessLogFieldMode = (typeof ACCESS_LOG_FIELD_MODES)[number];
export type AccessLogField = {
default?: AccessLogFieldMode;
config: {
[key: string]: AccessLogFieldMode;
};
};
export type AccessLogFields = {
header?: AccessLogField;
query?: AccessLogField;
cookie?: AccessLogField;
};

View File

@@ -1,88 +0,0 @@
import { DomainOrWildcard, Email } from "../types";
export declare const AUTOCERT_PROVIDERS: readonly [
"local",
"cloudflare",
"clouddns",
"duckdns",
"ovh",
"porkbun",
];
export type AutocertProvider = (typeof AUTOCERT_PROVIDERS)[number];
export type AutocertConfig =
| LocalOptions
| CloudflareOptions
| CloudDNSOptions
| DuckDNSOptions
| OVHOptionsWithAppKey
| OVHOptionsWithOAuth2Config
| PorkbunOptions;
export interface AutocertConfigBase {
email: Email;
domains: DomainOrWildcard[];
cert_path?: string;
key_path?: string;
}
export interface LocalOptions {
provider: "local";
cert_path?: string;
key_path?: string;
options?: {} | null;
}
export interface CloudflareOptions extends AutocertConfigBase {
provider: "cloudflare";
options: {
auth_token: string;
};
}
export interface CloudDNSOptions extends AutocertConfigBase {
provider: "clouddns";
options: {
client_id: string;
email: Email;
password: string;
};
}
export interface DuckDNSOptions extends AutocertConfigBase {
provider: "duckdns";
options: {
token: string;
};
}
export interface PorkbunOptions extends AutocertConfigBase {
provider: "porkbun";
options: {
api_key: string;
secret_api_key: string;
};
}
export declare const OVH_ENDPOINTS: readonly [
"ovh-eu",
"ovh-ca",
"ovh-us",
"kimsufi-eu",
"kimsufi-ca",
"soyoustart-eu",
"soyoustart-ca",
];
export type OVHEndpoint = (typeof OVH_ENDPOINTS)[number];
export interface OVHOptionsWithAppKey extends AutocertConfigBase {
provider: "ovh";
options: {
application_secret: string;
consumer_key: string;
api_endpoint?: OVHEndpoint;
application_key: string;
};
}
export interface OVHOptionsWithOAuth2Config extends AutocertConfigBase {
provider: "ovh";
options: {
application_secret: string;
consumer_key: string;
api_endpoint?: OVHEndpoint;
oauth2_config: {
client_id: string;
client_secret: string;
};
};
}

View File

@@ -1,17 +0,0 @@
export const AUTOCERT_PROVIDERS = [
"local",
"cloudflare",
"clouddns",
"duckdns",
"ovh",
"porkbun",
];
export const OVH_ENDPOINTS = [
"ovh-eu",
"ovh-ca",
"ovh-us",
"kimsufi-eu",
"kimsufi-ca",
"soyoustart-eu",
"soyoustart-ca",
];

View File

@@ -1,104 +0,0 @@
import { DomainOrWildcard, Email } from "../types";
export const AUTOCERT_PROVIDERS = [
"local",
"cloudflare",
"clouddns",
"duckdns",
"ovh",
"porkbun",
] as const;
export type AutocertProvider = (typeof AUTOCERT_PROVIDERS)[number];
export type AutocertConfig =
| LocalOptions
| CloudflareOptions
| CloudDNSOptions
| DuckDNSOptions
| OVHOptionsWithAppKey
| OVHOptionsWithOAuth2Config
| PorkbunOptions;
export interface AutocertConfigBase {
/* ACME email */
email: Email;
/* ACME domains */
domains: DomainOrWildcard[];
/* ACME certificate path */
cert_path?: string;
/* ACME key path */
key_path?: string;
}
export interface LocalOptions {
provider: "local";
/* ACME certificate path */
cert_path?: string;
/* ACME key path */
key_path?: string;
options?: {} | null;
}
export interface CloudflareOptions extends AutocertConfigBase {
provider: "cloudflare";
options: { auth_token: string };
}
export interface CloudDNSOptions extends AutocertConfigBase {
provider: "clouddns";
options: {
client_id: string;
email: Email;
password: string;
};
}
export interface DuckDNSOptions extends AutocertConfigBase {
provider: "duckdns";
options: {
token: string;
};
}
export interface PorkbunOptions extends AutocertConfigBase {
provider: "porkbun";
options: {
api_key: string;
secret_api_key: string;
};
}
export const OVH_ENDPOINTS = [
"ovh-eu",
"ovh-ca",
"ovh-us",
"kimsufi-eu",
"kimsufi-ca",
"soyoustart-eu",
"soyoustart-ca",
] as const;
export type OVHEndpoint = (typeof OVH_ENDPOINTS)[number];
export interface OVHOptionsWithAppKey extends AutocertConfigBase {
provider: "ovh";
options: {
application_secret: string;
consumer_key: string;
api_endpoint?: OVHEndpoint;
application_key: string;
};
}
export interface OVHOptionsWithOAuth2Config extends AutocertConfigBase {
provider: "ovh";
options: {
application_secret: string;
consumer_key: string;
api_endpoint?: OVHEndpoint;
oauth2_config: {
client_id: string;
client_secret: string;
};
};
}

View File

@@ -1,61 +0,0 @@
import { DomainName } from "../types";
import { AutocertConfig } from "./autocert";
import { EntrypointConfig } from "./entrypoint";
import { HomepageConfig } from "./homepage";
import { Providers } from "./providers";
export type Config = {
/** Optional autocert configuration
*
* @examples require(".").autocertExamples
*/
autocert?: AutocertConfig;
entrypoint?: EntrypointConfig;
providers: Providers;
/** Optional list of domains to match
*
* @minItems 1
* @examples require(".").matchDomainsExamples
*/
match_domains?: DomainName[];
homepage?: HomepageConfig;
/**
* Optional timeout before shutdown
* @default 3
* @minimum 1
*/
timeout_shutdown?: number;
};
export declare const autocertExamples: (
| {
provider: string;
email?: undefined;
domains?: undefined;
options?: undefined;
}
| {
provider: string;
email: string;
domains: string[];
options: {
auth_token: string;
client_id?: undefined;
email?: undefined;
password?: undefined;
};
}
| {
provider: string;
email: string;
domains: string[];
options: {
client_id: string;
email: string;
password: string;
auth_token?: undefined;
};
}
)[];
export declare const matchDomainsExamples: readonly [
"example.com",
"*.example.com",
];

View File

@@ -1,20 +0,0 @@
export const autocertExamples = [
{ provider: "local" },
{
provider: "cloudflare",
email: "abc@gmail",
domains: ["example.com"],
options: { auth_token: "c1234565789-abcdefghijklmnopqrst" },
},
{
provider: "clouddns",
email: "abc@gmail",
domains: ["example.com"],
options: {
client_id: "c1234565789",
email: "abc@gmail",
password: "password",
},
},
];
export const matchDomainsExamples = ["example.com", "*.example.com"];

View File

@@ -1,52 +0,0 @@
import { DomainName } from "../types";
import { AutocertConfig } from "./autocert";
import { EntrypointConfig } from "./entrypoint";
import { HomepageConfig } from "./homepage";
import { Providers } from "./providers";
export type Config = {
/** Optional autocert configuration
*
* @examples require(".").autocertExamples
*/
autocert?: AutocertConfig;
/* Optional entrypoint configuration */
entrypoint?: EntrypointConfig;
/* Providers configuration (include file, docker, notification) */
providers: Providers;
/** Optional list of domains to match
*
* @minItems 1
* @examples require(".").matchDomainsExamples
*/
match_domains?: DomainName[];
/* Optional homepage configuration */
homepage?: HomepageConfig;
/**
* Optional timeout before shutdown
* @default 3
* @minimum 1
*/
timeout_shutdown?: number;
};
export const autocertExamples = [
{ provider: "local" },
{
provider: "cloudflare",
email: "abc@gmail",
domains: ["example.com"],
options: { auth_token: "c1234565789-abcdefghijklmnopqrst" },
},
{
provider: "clouddns",
email: "abc@gmail",
domains: ["example.com"],
options: {
client_id: "c1234565789",
email: "abc@gmail",
password: "password",
},
},
];
export const matchDomainsExamples = ["example.com", "*.example.com"] as const;

View File

@@ -1,49 +0,0 @@
import { MiddlewareCompose } from "../middlewares/middleware_compose";
import { AccessLogConfig } from "./access_log";
export type EntrypointConfig = {
/** Entrypoint middleware configuration
*
* @examples require(".").middlewaresExamples
*/
middlewares?: MiddlewareCompose;
/** Entrypoint access log configuration
*
* @examples require(".").accessLogExamples
*/
access_log?: AccessLogConfig;
};
export declare const accessLogExamples: readonly [
{
readonly path: "/var/log/access.log";
readonly format: "combined";
readonly filters: {
readonly status_codes: {
readonly values: readonly ["200-299"];
};
};
readonly fields: {
readonly headers: {
readonly default: "keep";
readonly config: {
readonly foo: "redact";
};
};
};
},
];
export declare const middlewaresExamples: readonly [
{
readonly use: "RedirectHTTP";
},
{
readonly use: "CIDRWhitelist";
readonly allow: readonly [
"127.0.0.1",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
];
readonly status: 403;
readonly message: "Forbidden";
},
];

View File

@@ -1,30 +0,0 @@
export const accessLogExamples = [
{
path: "/var/log/access.log",
format: "combined",
filters: {
status_codes: {
values: ["200-299"],
},
},
fields: {
headers: {
default: "keep",
config: {
foo: "redact",
},
},
},
},
];
export const middlewaresExamples = [
{
use: "RedirectHTTP",
},
{
use: "CIDRWhitelist",
allow: ["127.0.0.1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
status: 403,
message: "Forbidden",
},
];

View File

@@ -1,47 +0,0 @@
import { MiddlewareCompose } from "../middlewares/middleware_compose";
import { AccessLogConfig } from "./access_log";
export type EntrypointConfig = {
/** Entrypoint middleware configuration
*
* @examples require(".").middlewaresExamples
*/
middlewares?: MiddlewareCompose;
/** Entrypoint access log configuration
*
* @examples require(".").accessLogExamples
*/
access_log?: AccessLogConfig;
};
export const accessLogExamples = [
{
path: "/var/log/access.log",
format: "combined",
filters: {
status_codes: {
values: ["200-299"],
},
},
fields: {
headers: {
default: "keep",
config: {
foo: "redact",
},
},
},
},
] as const;
export const middlewaresExamples = [
{
use: "RedirectHTTP",
},
{
use: "CIDRWhitelist",
allow: ["127.0.0.1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
status: 403,
message: "Forbidden",
},
] as const;

View File

@@ -1,7 +0,0 @@
export type HomepageConfig = {
/**
* Use default app categories (uses docker image name)
* @default true
*/
use_default_categories: boolean;
};

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,7 +0,0 @@
export type HomepageConfig = {
/**
* Use default app categories (uses docker image name)
* @default true
*/
use_default_categories: boolean;
};

View File

@@ -1,69 +0,0 @@
import { URL } from "../types";
export declare const NOTIFICATION_PROVIDERS: readonly [
"webhook",
"gotify",
"ntfy",
];
export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number];
export type NotificationConfig = {
name: string;
url: URL;
};
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",
"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];
export type WebhookMimeType = (typeof WEBHOOK_MIME_TYPES)[number];
export type WebhookColorMode = (typeof WEBHOOK_COLOR_MODES)[number];
export interface WebhookConfig extends NotificationConfig {
provider: "webhook";
/**
* Webhook template
*
* @default "discord"
*/
template?: WebhookTemplate;
token?: string;
/**
* Webhook message (usally JSON),
* required when template is not defined
*/
payload?: string;
/**
* Webhook method
*
* @default "POST"
*/
method?: WebhookMethod;
/**
* Webhook mime type
*
* @default "application/json"
*/
mime_type?: WebhookMimeType;
/**
* Webhook color mode
*
* @default "hex"
*/
color_mode?: WebhookColorMode;
}

View File

@@ -1,11 +0,0 @@
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,78 +0,0 @@
import { URL } from "../types";
export const NOTIFICATION_PROVIDERS = ["webhook", "gotify", "ntfy"] as const;
export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number];
export type NotificationConfig = {
/* Name of the notification provider */
name: string;
/* URL of the notification provider */
url: URL;
};
export interface GotifyConfig extends NotificationConfig {
provider: "gotify";
/* Gotify token */
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;
export type WebhookTemplate = (typeof WEBHOOK_TEMPLATES)[number];
export type WebhookMethod = (typeof WEBHOOK_METHODS)[number];
export type WebhookMimeType = (typeof WEBHOOK_MIME_TYPES)[number];
export type WebhookColorMode = (typeof WEBHOOK_COLOR_MODES)[number];
export interface WebhookConfig extends NotificationConfig {
provider: "webhook";
/**
* Webhook template
*
* @default "discord"
*/
template?: WebhookTemplate;
/* Webhook token */
token?: string;
/**
* Webhook message (usally JSON),
* required when template is not defined
*/
payload?: string;
/**
* Webhook method
*
* @default "POST"
*/
method?: WebhookMethod;
/**
* Webhook mime type
*
* @default "application/json"
*/
mime_type?: WebhookMimeType;
/**
* Webhook color mode
*
* @default "hex"
*/
color_mode?: WebhookColorMode;
}

View File

@@ -1,58 +0,0 @@
import { URI, URL } from "../types";
import { GotifyConfig, NtfyConfig, WebhookConfig } from "./notification";
export type Providers = {
/** List of route definition files to include
*
* @minItems 1
* @examples require(".").includeExamples
* @items.pattern ^[\w\d\-_]+\.(yaml|yml)$
*/
include?: URI[];
/** Name-value mapping of docker hosts to retrieve routes from
*
* @minProperties 1
* @examples require(".").dockerExamples
*/
docker?: {
[name: string]: URL | "$DOCKER_HOST";
};
/** List of GoDoxy agents
*
* @minItems 1
* @examples require(".").agentExamples
*/
agents?: `${string}:${number}`[];
/** List of notification providers
*
* @minItems 1
* @examples require(".").notificationExamples
*/
notification?: (WebhookConfig | GotifyConfig | NtfyConfig)[];
};
export declare const includeExamples: readonly ["file1.yml", "file2.yml"];
export declare const dockerExamples: readonly [
{
readonly local: "$DOCKER_HOST";
},
{
readonly remote: "tcp://10.0.2.1:2375";
},
{
readonly remote2: "ssh://root:1234@10.0.2.2";
},
];
export declare const notificationExamples: readonly [
{
readonly name: "gotify";
readonly provider: "gotify";
readonly url: "https://gotify.domain.tld";
readonly token: "abcd";
},
{
readonly name: "discord";
readonly provider: "webhook";
readonly template: "discord";
readonly url: "https://discord.com/api/webhooks/1234/abcd";
},
];
export declare const agentExamples: readonly ["10.0.2.3:8890", "10.0.2.4:8890"];

View File

@@ -1,21 +0,0 @@
export const includeExamples = ["file1.yml", "file2.yml"];
export const dockerExamples = [
{ local: "$DOCKER_HOST" },
{ remote: "tcp://10.0.2.1:2375" },
{ remote2: "ssh://root:1234@10.0.2.2" },
];
export const notificationExamples = [
{
name: "gotify",
provider: "gotify",
url: "https://gotify.domain.tld",
token: "abcd",
},
{
name: "discord",
provider: "webhook",
template: "discord",
url: "https://discord.com/api/webhooks/1234/abcd",
},
];
export const agentExamples = ["10.0.2.3:8890", "10.0.2.4:8890"];

View File

@@ -1,52 +0,0 @@
import { URI, URL } from "../types";
import { GotifyConfig, NtfyConfig, WebhookConfig } from "./notification";
export type Providers = {
/** List of route definition files to include
*
* @minItems 1
* @examples require(".").includeExamples
* @items.pattern ^[\w\d\-_]+\.(yaml|yml)$
*/
include?: URI[];
/** Name-value mapping of docker hosts to retrieve routes from
*
* @minProperties 1
* @examples require(".").dockerExamples
*/
docker?: { [name: string]: URL | "$DOCKER_HOST" };
/** List of GoDoxy agents
*
* @minItems 1
* @examples require(".").agentExamples
*/
agents?: `${string}:${number}`[];
/** List of notification providers
*
* @minItems 1
* @examples require(".").notificationExamples
*/
notification?: (WebhookConfig | GotifyConfig | NtfyConfig)[];
};
export const includeExamples = ["file1.yml", "file2.yml"] as const;
export const dockerExamples = [
{ local: "$DOCKER_HOST" },
{ remote: "tcp://10.0.2.1:2375" },
{ remote2: "ssh://root:1234@10.0.2.2" },
] as const;
export const notificationExamples = [
{
name: "gotify",
provider: "gotify",
url: "https://gotify.domain.tld",
token: "abcd",
},
{
name: "discord",
provider: "webhook",
template: "discord",
url: "https://discord.com/api/webhooks/1234/abcd",
},
] as const;
export const agentExamples = ["10.0.2.3:8890", "10.0.2.4:8890"] as const;

5
schemas/docker.d.ts vendored
View File

@@ -1,5 +0,0 @@
import { IdleWatcherConfig } from "./providers/idlewatcher";
import { Route } from "./providers/routes";
export type DockerRoutes = {
[key: string]: Route & IdleWatcherConfig;
};

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,7 +0,0 @@
import { IdleWatcherConfig } from "./providers/idlewatcher";
import { Route } from "./providers/routes";
//FIXME: fix this
export type DockerRoutes = {
[key: string]: Route & IdleWatcherConfig;
};

File diff suppressed because one or more lines are too long

38
schemas/index.d.ts vendored
View File

@@ -1,38 +0,0 @@
import * as AccessLog from "./config/access_log";
import * as Autocert from "./config/autocert";
import * as Config from "./config/config";
import * as Entrypoint from "./config/entrypoint";
import * as Notification from "./config/notification";
import * as Providers from "./config/providers";
import * as MiddlewareCompose from "./middlewares/middleware_compose";
import * as Middlewares from "./middlewares/middlewares";
import * as Healthcheck from "./providers/healthcheck";
import * as Homepage from "./providers/homepage";
import * as IdleWatcher from "./providers/idlewatcher";
import * as LoadBalance from "./providers/loadbalance";
import * as Routes from "./providers/routes";
import * as GoDoxy from "./types";
import ConfigSchema from "./config.schema.json";
import DockerRoutesSchema from "./docker_routes.schema.json";
import MiddlewareComposeSchema from "./middleware_compose.schema.json";
import RoutesSchema from "./routes.schema.json";
export {
AccessLog,
Autocert,
Config,
ConfigSchema,
DockerRoutesSchema,
Entrypoint,
GoDoxy,
Healthcheck,
Homepage,
IdleWatcher,
LoadBalance,
MiddlewareCompose,
MiddlewareComposeSchema,
Middlewares,
Notification,
Providers,
Routes,
RoutesSchema,
};

View File

@@ -1,19 +0,0 @@
import * as AccessLog from "./config/access_log";
import * as Autocert from "./config/autocert";
import * as Config from "./config/config";
import * as Entrypoint from "./config/entrypoint";
import * as Notification from "./config/notification";
import * as Providers from "./config/providers";
import * as MiddlewareCompose from "./middlewares/middleware_compose";
import * as Middlewares from "./middlewares/middlewares";
import * as Healthcheck from "./providers/healthcheck";
import * as Homepage from "./providers/homepage";
import * as IdleWatcher from "./providers/idlewatcher";
import * as LoadBalance from "./providers/loadbalance";
import * as Routes from "./providers/routes";
import * as GoDoxy from "./types";
import ConfigSchema from "./config.schema.json";
import DockerRoutesSchema from "./docker_routes.schema.json";
import MiddlewareComposeSchema from "./middleware_compose.schema.json";
import RoutesSchema from "./routes.schema.json";
export { AccessLog, Autocert, Config, ConfigSchema, DockerRoutesSchema, Entrypoint, GoDoxy, Healthcheck, Homepage, IdleWatcher, LoadBalance, MiddlewareCompose, MiddlewareComposeSchema, Middlewares, Notification, Providers, Routes, RoutesSchema, };

View File

@@ -1,43 +0,0 @@
import * as AccessLog from "./config/access_log";
import * as Autocert from "./config/autocert";
import * as Config from "./config/config";
import * as Entrypoint from "./config/entrypoint";
import * as Notification from "./config/notification";
import * as Providers from "./config/providers";
import * as MiddlewareCompose from "./middlewares/middleware_compose";
import * as Middlewares from "./middlewares/middlewares";
import * as Healthcheck from "./providers/healthcheck";
import * as Homepage from "./providers/homepage";
import * as IdleWatcher from "./providers/idlewatcher";
import * as LoadBalance from "./providers/loadbalance";
import * as Routes from "./providers/routes";
import * as GoDoxy from "./types";
import ConfigSchema from "./config.schema.json";
import DockerRoutesSchema from "./docker_routes.schema.json";
import MiddlewareComposeSchema from "./middleware_compose.schema.json";
import RoutesSchema from "./routes.schema.json";
export {
AccessLog,
Autocert,
Config,
ConfigSchema,
DockerRoutesSchema,
Entrypoint,
GoDoxy,
Healthcheck,
Homepage,
IdleWatcher,
LoadBalance,
MiddlewareCompose,
MiddlewareComposeSchema,
Middlewares,
Notification,
Providers,
Routes,
RoutesSchema,
};

View File

@@ -1 +0,0 @@
{"$schema":"http://json-schema.org/draft-07/schema#","definitions":{"CIDR":{"anyOf":[{"pattern":"^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*$","type":"string"},{"pattern":"^.*:.*:.*:.*:.*:.*:.*:.*$","type":"string"},{"pattern":"^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*/[0-9]*$","type":"string"},{"pattern":"^::[0-9]*$","type":"string"},{"pattern":"^.*::/[0-9]*$","type":"string"},{"pattern":"^.*:.*::/[0-9]*$","type":"string"}]},"Duration":{"pattern":"^([0-9]+(ms|s|m|h))+$","type":"string"},"HTTPHeader":{"description":"HTTP Header","pattern":"^[a-zA-Z0-9\\-]+$","type":"string"},"MiddlewareComposeMap":{"anyOf":[{"additionalProperties":false,"properties":{"use":{"enum":["CustomErrorPage","ErrorPage","customErrorPage","custom_error_page","errorPage","error_page"],"type":"string"}},"required":["use"],"type":"object"},{"additionalProperties":false,"properties":{"bypass":{"additionalProperties":false,"description":"Bypass redirect","properties":{"user_agents":{"description":"Bypass redirect for user agents","items":{"type":"string"},"type":"array"}},"type":"object"},"use":{"enum":["RedirectHTTP","redirectHTTP","redirect_http"],"type":"string"}},"required":["use"],"type":"object"},{"additionalProperties":false,"properties":{"use":{"enum":["SetXForwarded","setXForwarded","set_x_forwarded"],"type":"string"}},"required":["use"],"type":"object"},{"additionalProperties":false,"properties":{"use":{"enum":["HideXForwarded","hideXForwarded","hide_x_forwarded"],"type":"string"}},"required":["use"],"type":"object"},{"additionalProperties":false,"properties":{"allow":{"items":{"$ref":"#/definitions/CIDR"},"type":"array"},"message":{"default":"IP not allowed","description":"Error message when blocked","type":"string"},"status":{"$ref":"#/definitions/StatusCode","default":403,"description":"HTTP status code when blocked (alias of status_code)"},"status_code":{"$ref":"#/definitions/StatusCode","default":403,"description":"HTTP status code when blocked"},"use":{"enum":["CIDRWhitelist","cidrWhitelist","cidr_whitelist"],"type":"string"}},"required":["allow","use"],"type":"object"},{"additionalProperties":false,"properties":{"recursive":{"default":false,"description":"Recursively resolve the IP","type":"boolean"},"use":{"enum":["CloudflareRealIP","cloudflareRealIp","cloudflare_real_ip"],"type":"string"}},"required":["use"],"type":"object"},{"additionalProperties":false,"properties":{"add_headers":{"additionalProperties":false,"description":"Add HTTP headers","items":{"type":"string"},"type":"array"},"add_prefix":{"description":"Add prefix to request URL","type":"string"},"hide_headers":{"description":"Hide HTTP headers","items":{"$ref":"#/definitions/HTTPHeader"},"type":"array"},"set_headers":{"additionalProperties":false,"description":"Set HTTP headers","items":{"type":"string"},"type":"array"},"use":{"enum":["ModifyRequest","Request","modifyRequest","modify_request","request"],"type":"string"}},"required":["use"],"type":"object"},{"additionalProperties":false,"properties":{"add_headers":{"additionalProperties":false,"description":"Add HTTP headers","items":{"type":"string"},"type":"array"},"hide_headers":{"description":"Hide HTTP headers","items":{"$ref":"#/definitions/HTTPHeader"},"type":"array"},"set_headers":{"additionalProperties":false,"description":"Set HTTP headers","items":{"type":"string"},"type":"array"},"use":{"enum":["ModifyResponse","Response","modifyResponse","modify_response","response"],"type":"string"}},"required":["use"],"type":"object"},{"additionalProperties":false,"properties":{"allowed_groups":{"description":"Allowed groups","items":{"type":"string"},"minItems":1,"type":"array"},"allowed_users":{"description":"Allowed users","items":{"type":"string"},"minItems":1,"type":"array"},"use":{"enum":["OIDC","oidc"],"type":"string"}},"required":["use"],"type":"object"},{"additionalProperties":false,"properties":{"average":{"description":"Average number of requests allowed in a period","type":"number"},"burst":{"description":"Maximum number of requests allowed in a period","type":"number"},"period":{"$ref":"#/definitions/Duration","default":"1s","description":"Duration of the rate limit"},"use":{"enum":["RateLimit","rateLimit","rate_limit"],"type":"string"}},"required":["average","burst","use"],"type":"object"},{"additionalProperties":false,"properties":{"from":{"items":{"$ref":"#/definitions/CIDR"},"type":"array"},"header":{"$ref":"#/definitions/HTTPHeader","default":"X-Real-IP","description":"Header to get the client IP from"},"recursive":{"default":false,"description":"Recursive resolve the IP","type":"boolean"},"use":{"enum":["RealIP","realIP","real_ip"],"type":"string"}},"required":["from","use"],"type":"object"}]},"StatusCode":{"anyOf":[{"pattern":"^[0-9]*$","type":"string"},{"type":"number"}]}},"items":{"$ref":"#/definitions/MiddlewareComposeMap"},"type":"array"}

View File

@@ -1,2 +0,0 @@
import { MiddlewareComposeMap } from "./middlewares";
export type MiddlewareCompose = MiddlewareComposeMap[];

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,3 +0,0 @@
import { MiddlewareComposeMap } from "./middlewares";
export type MiddlewareCompose = MiddlewareComposeMap[];

View File

@@ -1,186 +0,0 @@
import * as types from "../types";
export type KeyOptMapping<
T extends {
use: string;
},
> = {
[key in T["use"]]?: Omit<T, "use">;
};
export declare const ALL_MIDDLEWARES: readonly [
"ErrorPage",
"RedirectHTTP",
"SetXForwarded",
"HideXForwarded",
"CIDRWhitelist",
"CloudflareRealIP",
"ModifyRequest",
"ModifyResponse",
"OIDC",
"RateLimit",
"RealIP",
];
/**
* @type object
* @patternProperties {"^.*@file$": {"type": "null"}}
*/
export type MiddlewareFileRef = {
[key: `${string}@file`]: null;
};
export type MiddlewaresMap =
| (KeyOptMapping<CustomErrorPage> &
KeyOptMapping<RedirectHTTP> &
KeyOptMapping<SetXForwarded> &
KeyOptMapping<HideXForwarded> &
KeyOptMapping<CIDRWhitelist> &
KeyOptMapping<CloudflareRealIP> &
KeyOptMapping<ModifyRequest> &
KeyOptMapping<ModifyResponse> &
KeyOptMapping<OIDC> &
KeyOptMapping<RateLimit> &
KeyOptMapping<RealIP>)
| MiddlewareFileRef;
export type MiddlewareComposeMap =
| CustomErrorPage
| RedirectHTTP
| SetXForwarded
| HideXForwarded
| CIDRWhitelist
| CloudflareRealIP
| ModifyRequest
| ModifyResponse
| OIDC
| RateLimit
| RealIP;
export type CustomErrorPage = {
use:
| "error_page"
| "errorPage"
| "ErrorPage"
| "custom_error_page"
| "customErrorPage"
| "CustomErrorPage";
};
export type RedirectHTTP = {
use: "redirect_http" | "redirectHTTP" | "RedirectHTTP";
/** Bypass redirect */
bypass?: {
/** Bypass redirect for user agents */
user_agents?: string[];
};
};
export type SetXForwarded = {
use: "set_x_forwarded" | "setXForwarded" | "SetXForwarded";
};
export type HideXForwarded = {
use: "hide_x_forwarded" | "hideXForwarded" | "HideXForwarded";
};
export type CIDRWhitelist = {
use: "cidr_whitelist" | "cidrWhitelist" | "CIDRWhitelist";
allow: types.CIDR[];
/** HTTP status code when blocked
*
* @default 403
*/
status_code?: types.StatusCode;
/** HTTP status code when blocked (alias of status_code)
*
* @default 403
*/
status?: types.StatusCode;
/** Error message when blocked
*
* @default "IP not allowed"
*/
message?: string;
};
export type CloudflareRealIP = {
use: "cloudflare_real_ip" | "cloudflareRealIp" | "CloudflareRealIP";
/** Recursively resolve the IP
*
* @default false
*/
recursive?: boolean;
};
export type ModifyRequest = {
use:
| "request"
| "Request"
| "modify_request"
| "modifyRequest"
| "ModifyRequest";
/** Set HTTP headers */
set_headers?: {
[key: types.HTTPHeader]: string;
};
/** Add HTTP headers */
add_headers?: {
[key: types.HTTPHeader]: string;
};
/** Hide HTTP headers */
hide_headers?: types.HTTPHeader[];
/** Add prefix to request URL */
add_prefix?: string;
};
export type ModifyResponse = {
use:
| "response"
| "Response"
| "modify_response"
| "modifyResponse"
| "ModifyResponse";
/** Set HTTP headers */
set_headers?: {
[key: types.HTTPHeader]: string;
};
/** Add HTTP headers */
add_headers?: {
[key: types.HTTPHeader]: string;
};
/** Hide HTTP headers */
hide_headers?: types.HTTPHeader[];
};
export type OIDC = {
use: "oidc" | "OIDC";
/** Allowed users
*
* @minItems 1
*/
allowed_users?: string[];
/** Allowed groups
*
* @minItems 1
*/
allowed_groups?: string[];
};
export type RateLimit = {
use: "rate_limit" | "rateLimit" | "RateLimit";
/** Average number of requests allowed in a period
*
* @min 1
*/
average: number;
/** Maximum number of requests allowed in a period
*
* @min 1
*/
burst: number;
/** Duration of the rate limit
*
* @default 1s
*/
period?: types.Duration;
};
export type RealIP = {
use: "real_ip" | "realIP" | "RealIP";
/** Header to get the client IP from
*
* @default "X-Real-IP"
*/
header?: types.HTTPHeader;
from: types.CIDR[];
/** Recursive resolve the IP
*
* @default false
*/
recursive?: boolean;
};

View File

@@ -1,13 +0,0 @@
export const ALL_MIDDLEWARES = [
"ErrorPage",
"RedirectHTTP",
"SetXForwarded",
"HideXForwarded",
"CIDRWhitelist",
"CloudflareRealIP",
"ModifyRequest",
"ModifyResponse",
"OIDC",
"RateLimit",
"RealIP",
];

View File

@@ -1,190 +0,0 @@
import * as types from "../types";
export type KeyOptMapping<T extends { use: string }> = {
[key in T["use"]]?: Omit<T, "use">;
};
export const ALL_MIDDLEWARES = [
"ErrorPage",
"RedirectHTTP",
"SetXForwarded",
"HideXForwarded",
"CIDRWhitelist",
"CloudflareRealIP",
"ModifyRequest",
"ModifyResponse",
"OIDC",
"RateLimit",
"RealIP",
] as const;
/**
* @type object
* @patternProperties {"^.*@file$": {"type": "null"}}
*/
export type MiddlewareFileRef = {
[key: `${string}@file`]: null;
};
export type MiddlewaresMap =
| (KeyOptMapping<CustomErrorPage> &
KeyOptMapping<RedirectHTTP> &
KeyOptMapping<SetXForwarded> &
KeyOptMapping<HideXForwarded> &
KeyOptMapping<CIDRWhitelist> &
KeyOptMapping<CloudflareRealIP> &
KeyOptMapping<ModifyRequest> &
KeyOptMapping<ModifyResponse> &
KeyOptMapping<OIDC> &
KeyOptMapping<RateLimit> &
KeyOptMapping<RealIP>)
| MiddlewareFileRef;
export type MiddlewareComposeMap =
| CustomErrorPage
| RedirectHTTP
| SetXForwarded
| HideXForwarded
| CIDRWhitelist
| CloudflareRealIP
| ModifyRequest
| ModifyResponse
| OIDC
| RateLimit
| RealIP;
export type CustomErrorPage = {
use:
| "error_page"
| "errorPage"
| "ErrorPage"
| "custom_error_page"
| "customErrorPage"
| "CustomErrorPage";
};
export type RedirectHTTP = {
use: "redirect_http" | "redirectHTTP" | "RedirectHTTP";
/** Bypass redirect */
bypass?: {
/** Bypass redirect for user agents */
user_agents?: string[];
};
};
export type SetXForwarded = {
use: "set_x_forwarded" | "setXForwarded" | "SetXForwarded";
};
export type HideXForwarded = {
use: "hide_x_forwarded" | "hideXForwarded" | "HideXForwarded";
};
export type CIDRWhitelist = {
use: "cidr_whitelist" | "cidrWhitelist" | "CIDRWhitelist";
/* Allowed CIDRs/IPs */
allow: types.CIDR[];
/** HTTP status code when blocked
*
* @default 403
*/
status_code?: types.StatusCode;
/** HTTP status code when blocked (alias of status_code)
*
* @default 403
*/
status?: types.StatusCode;
/** Error message when blocked
*
* @default "IP not allowed"
*/
message?: string;
};
export type CloudflareRealIP = {
use: "cloudflare_real_ip" | "cloudflareRealIp" | "CloudflareRealIP";
/** Recursively resolve the IP
*
* @default false
*/
recursive?: boolean;
};
export type ModifyRequest = {
use:
| "request"
| "Request"
| "modify_request"
| "modifyRequest"
| "ModifyRequest";
/** Set HTTP headers */
set_headers?: { [key: types.HTTPHeader]: string };
/** Add HTTP headers */
add_headers?: { [key: types.HTTPHeader]: string };
/** Hide HTTP headers */
hide_headers?: types.HTTPHeader[];
/** Add prefix to request URL */
add_prefix?: string;
};
export type ModifyResponse = {
use:
| "response"
| "Response"
| "modify_response"
| "modifyResponse"
| "ModifyResponse";
/** Set HTTP headers */
set_headers?: { [key: types.HTTPHeader]: string };
/** Add HTTP headers */
add_headers?: { [key: types.HTTPHeader]: string };
/** Hide HTTP headers */
hide_headers?: types.HTTPHeader[];
};
export type OIDC = {
use: "oidc" | "OIDC";
/** Allowed users
*
* @minItems 1
*/
allowed_users?: string[];
/** Allowed groups
*
* @minItems 1
*/
allowed_groups?: string[];
};
export type RateLimit = {
use: "rate_limit" | "rateLimit" | "RateLimit";
/** Average number of requests allowed in a period
*
* @min 1
*/
average: number;
/** Maximum number of requests allowed in a period
*
* @min 1
*/
burst: number;
/** Duration of the rate limit
*
* @default 1s
*/
period?: types.Duration;
};
export type RealIP = {
use: "real_ip" | "realIP" | "RealIP";
/** Header to get the client IP from
*
* @default "X-Real-IP"
*/
header?: types.HTTPHeader;
from: types.CIDR[];
/** Recursive resolve the IP
*
* @default false
*/
recursive?: boolean;
};

View File

@@ -1,37 +0,0 @@
{
"name": "godoxy-schemas",
"version": "0.10.1-6",
"description": "JSON Schema and typescript types for GoDoxy configuration",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yusing/godoxy"
},
"files": [
"**/*.ts",
"**/*.js",
"*.schema.json",
"../README.md",
"../LICENSE"
],
"type": "module",
"main": "./index.ts",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.ts",
"require": "./index.js"
}
},
"devDependencies": {
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-json-schema": "^0.65.1"
},
"displayName": "GoDoxy Types",
"packageManager": "bun@1.2.9",
"publisher": "yusing",
"scripts": {
"format:write": "prettier --write \"**/*.ts\" --cache"
}
}

View File

@@ -1,32 +0,0 @@
import { Duration, URI } from "../types";
/**
* @additionalProperties false
*/
export type HealthcheckConfig = {
/** Disable healthcheck
*
* @default false
*/
disable?: boolean;
/** Healthcheck path
*
* @default /
*/
path?: URI;
/**
* Use GET instead of HEAD
*
* @default false
*/
use_get?: boolean;
/** Healthcheck interval
*
* @default 5s
*/
interval?: Duration;
/** Healthcheck timeout
*
* @default 5s
*/
timeout?: Duration;
};

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,33 +0,0 @@
import { Duration, URI } from "../types";
/**
* @additionalProperties false
*/
export type HealthcheckConfig = {
/** Disable healthcheck
*
* @default false
*/
disable?: boolean;
/** Healthcheck path
*
* @default /
*/
path?: URI;
/**
* Use GET instead of HEAD
*
* @default false
*/
use_get?: boolean;
/** Healthcheck interval
*
* @default 5s
*/
interval?: Duration;
/** Healthcheck timeout
*
* @default 5s
*/
timeout?: Duration;
};

View File

@@ -1,27 +0,0 @@
import { URL } from "../types";
/**
* @additionalProperties false
*/
export type HomepageConfig = {
/** Whether show in dashboard
*
* @default true
*/
show?: boolean;
name?: string;
icon?: URL | WalkxcodeIcon | ExternalIcon | TargetRelativeIconPath;
description?: string;
url?: URL;
category?: string;
widget_config?: {
[key: string]: any;
};
};
/** Walkxcode icon
*
* @pattern ^(png|svg|webp)\/[\w\d\-_]+\.\1
* @type string
*/
export type WalkxcodeIcon = string & {};
export type ExternalIcon = `@${"selfhst" | "walkxcode"}/${string}.${string}`;
export type TargetRelativeIconPath = `@target/${string}` | `/${string}`;

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,39 +0,0 @@
import { URL } from "../types";
/**
* @additionalProperties false
*/
export type HomepageConfig = {
/** Whether show in dashboard
*
* @default true
*/
show?: boolean;
/* Display name on dashboard */
name?: string;
/* Display icon on dashboard */
icon?: URL | WalkxcodeIcon | ExternalIcon | TargetRelativeIconPath;
/* App description */
description?: string;
/* Override url */
url?: URL;
/* App category */
category?: string;
/* Widget config */
widget_config?: {
[key: string]: any;
};
};
/** Walkxcode icon
*
* @pattern ^(png|svg|webp)\/[\w\d\-_]+\.\1
* @type string
*/
export type WalkxcodeIcon = string & {};
/* Walkxcode / selfh.st icon */
export type ExternalIcon = `@${"selfhst" | "walkxcode"}/${string}.${string}`;
/* Relative path to proxy target */
export type TargetRelativeIconPath = `@target/${string}` | `/${string}`;

View File

@@ -1,35 +0,0 @@
import { Duration, URI } from "../types";
export declare const STOP_METHODS: readonly ["pause", "stop", "kill"];
export type StopMethod = (typeof STOP_METHODS)[number];
export declare const STOP_SIGNALS: readonly [
"",
"SIGINT",
"SIGTERM",
"SIGHUP",
"SIGQUIT",
"INT",
"TERM",
"HUP",
"QUIT",
];
export type Signal = (typeof STOP_SIGNALS)[number];
export type IdleWatcherConfig = {
idle_timeout?: Duration;
/** Wake timeout
*
* @default 30s
*/
wake_timeout?: Duration;
/** Stop timeout
*
* @default 30s
*/
stop_timeout?: Duration;
/** Stop method
*
* @default stop
*/
stop_method?: StopMethod;
stop_signal?: Signal;
start_endpoint?: URI;
};

View File

@@ -1,12 +0,0 @@
export const STOP_METHODS = ["pause", "stop", "kill"];
export const STOP_SIGNALS = [
"",
"SIGINT",
"SIGTERM",
"SIGHUP",
"SIGQUIT",
"INT",
"TERM",
"HUP",
"QUIT",
];

View File

@@ -1,41 +0,0 @@
import { Duration, URI } from "../types";
export const STOP_METHODS = ["pause", "stop", "kill"] as const;
export type StopMethod = (typeof STOP_METHODS)[number];
export const STOP_SIGNALS = [
"",
"SIGINT",
"SIGTERM",
"SIGHUP",
"SIGQUIT",
"INT",
"TERM",
"HUP",
"QUIT",
] as const;
export type Signal = (typeof STOP_SIGNALS)[number];
export type IdleWatcherConfig = {
/* Idle timeout */
idle_timeout?: Duration;
/** Wake timeout
*
* @default 30s
*/
wake_timeout?: Duration;
/** Stop timeout
*
* @default 30s
*/
stop_timeout?: Duration;
/** Stop method
*
* @default stop
*/
stop_method?: StopMethod;
/* Stop signal */
stop_signal?: Signal;
/* Start endpoint (any path can wake the container if not specified) */
start_endpoint?: URI;
};

View File

@@ -1,38 +0,0 @@
import { RealIP } from "../middlewares/middlewares";
export declare const LOAD_BALANCE_MODES: readonly [
"round_robin",
"least_conn",
"ip_hash",
];
export type LoadBalanceMode = (typeof LOAD_BALANCE_MODES)[number];
export type LoadBalanceConfigBase = {
/** Alias (subdomain or FDN) of load-balancer
*
* @minLength 1
*/
link: string;
/** Load-balance weight (reserved for future use)
*
* @minimum 0
* @maximum 100
*/
weight?: number;
};
export type LoadBalanceConfig = LoadBalanceConfigBase &
(
| {}
| RoundRobinLoadBalanceConfig
| LeastConnLoadBalanceConfig
| IPHashLoadBalanceConfig
);
export type IPHashLoadBalanceConfig = {
mode: "ip_hash";
/** Real IP config, header to get client IP from */
config: RealIP;
};
export type LeastConnLoadBalanceConfig = {
mode: "least_conn";
};
export type RoundRobinLoadBalanceConfig = {
mode: "round_robin";
};

View File

@@ -1,5 +0,0 @@
export const LOAD_BALANCE_MODES = [
"round_robin",
"least_conn",
"ip_hash",
];

View File

@@ -1,44 +0,0 @@
import { RealIP } from "../middlewares/middlewares";
export const LOAD_BALANCE_MODES = [
"round_robin",
"least_conn",
"ip_hash",
] as const;
export type LoadBalanceMode = (typeof LOAD_BALANCE_MODES)[number];
export type LoadBalanceConfigBase = {
/** Alias (subdomain or FDN) of load-balancer
*
* @minLength 1
*/
link: string;
/** Load-balance weight (reserved for future use)
*
* @minimum 0
* @maximum 100
*/
weight?: number;
};
export type LoadBalanceConfig = LoadBalanceConfigBase &
(
| {} // linking other routes
| RoundRobinLoadBalanceConfig
| LeastConnLoadBalanceConfig
| IPHashLoadBalanceConfig
);
export type IPHashLoadBalanceConfig = {
mode: "ip_hash";
/** Real IP config, header to get client IP from */
config: RealIP;
};
export type LeastConnLoadBalanceConfig = {
mode: "least_conn";
};
export type RoundRobinLoadBalanceConfig = {
mode: "round_robin";
};

View File

@@ -1,148 +0,0 @@
import { AccessLogConfig } from "../config/access_log";
import { accessLogExamples } from "../config/entrypoint";
import { MiddlewaresMap } from "../middlewares/middlewares";
import {
Duration,
Hostname,
IPv4,
IPv6,
PathPattern,
Port,
StreamPort,
} from "../types";
import { HealthcheckConfig } from "./healthcheck";
import { HomepageConfig } from "./homepage";
import { LoadBalanceConfig } from "./loadbalance";
export declare const PROXY_SCHEMES: readonly ["http", "https"];
export declare const STREAM_SCHEMES: readonly ["tcp", "udp"];
export type ProxyScheme = (typeof PROXY_SCHEMES)[number];
export type StreamScheme = (typeof STREAM_SCHEMES)[number];
export type Route = ReverseProxyRoute | FileServerRoute | StreamRoute;
export type Routes = {
[key: string]: Route;
};
export type ReverseProxyRoute = {
/** Alias (subdomain or FDN)
* @minLength 1
*/
alias?: string;
/** Proxy scheme
*
* @default http
*/
scheme?: ProxyScheme;
/** Proxy host
*
* @default localhost
*/
host?: Hostname | IPv4 | IPv6;
/** Proxy port
*
* @default 80
*/
port?: Port;
/** Skip TLS verification
*
* @default false
*/
no_tls_verify?: boolean;
/** Response header timeout
*
* @default 60s
*/
response_header_timeout?: Duration;
/** Path patterns (only patterns that match will be proxied).
*
* See https://pkg.go.dev/net/http#hdr-Patterns-ServeMux
*/
path_patterns?: PathPattern[];
/** Healthcheck config */
healthcheck?: HealthcheckConfig;
/** Load balance config */
load_balance?: LoadBalanceConfig;
/** Middlewares */
middlewares?: MiddlewaresMap;
/** Homepage config
*
* @examples require(".").homepageExamples
*/
homepage?: HomepageConfig;
/** Access log config
*
* @examples require(".").accessLogExamples
*/
access_log?: AccessLogConfig;
};
export type FileServerRoute = {
/** Alias (subdomain or FDN)
* @minLength 1
*/
alias?: string;
scheme: "fileserver";
root: string;
/** Path patterns (only patterns that match will be proxied).
*
* See https://pkg.go.dev/net/http#hdr-Patterns-ServeMux
*/
path_patterns?: PathPattern[];
/** Middlewares */
middlewares?: MiddlewaresMap;
/** Homepage config
*
* @examples require(".").homepageExamples
*/
homepage?: HomepageConfig;
/** Access log config
*
* @examples require(".").accessLogExamples
*/
access_log?: AccessLogConfig;
/** Healthcheck config */
healthcheck?: HealthcheckConfig;
};
export type StreamRoute = {
/** Alias (subdomain or FDN)
* @minLength 1
*/
alias?: string;
/** Stream scheme
*
* @default tcp
*/
scheme?: StreamScheme;
/** Stream host
*
* @default localhost
*/
host?: Hostname | IPv4 | IPv6;
port: StreamPort;
/** Healthcheck config */
healthcheck?: HealthcheckConfig;
};
export declare const homepageExamples: (
| {
name: string;
icon: string;
category: string;
}
| {
name: string;
icon: string;
category?: undefined;
}
)[];
export declare const loadBalanceExamples: (
| {
link: string;
mode: string;
config?: undefined;
}
| {
link: string;
mode: string;
config: {
header: string;
};
}
)[];
export { accessLogExamples };

View File

@@ -1,28 +0,0 @@
import { accessLogExamples } from "../config/entrypoint";
export const PROXY_SCHEMES = ["http", "https"];
export const STREAM_SCHEMES = ["tcp", "udp"];
export const homepageExamples = [
{
name: "Sonarr",
icon: "png/sonarr.png",
category: "Arr suite",
},
{
name: "App",
icon: "@target/favicon.ico",
},
];
export const loadBalanceExamples = [
{
link: "flaresolverr",
mode: "round_robin",
},
{
link: "service.domain.com",
mode: "ip_hash",
config: {
header: "X-Real-IP",
},
},
];
export { accessLogExamples };

View File

@@ -1,156 +0,0 @@
import { AccessLogConfig } from "../config/access_log";
import { accessLogExamples } from "../config/entrypoint";
import { MiddlewaresMap } from "../middlewares/middlewares";
import {
Duration,
Hostname,
IPv4,
IPv6,
PathPattern,
Port,
StreamPort,
} from "../types";
import { HealthcheckConfig } from "./healthcheck";
import { HomepageConfig } from "./homepage";
import { LoadBalanceConfig } from "./loadbalance";
export const PROXY_SCHEMES = ["http", "https"] as const;
export const STREAM_SCHEMES = ["tcp", "udp"] as const;
export type ProxyScheme = (typeof PROXY_SCHEMES)[number];
export type StreamScheme = (typeof STREAM_SCHEMES)[number];
export type Route = ReverseProxyRoute | FileServerRoute | StreamRoute;
export type Routes = {
[key: string]: Route;
};
export type ReverseProxyRoute = {
/** Alias (subdomain or FDN)
* @minLength 1
*/
alias?: string;
/** Proxy scheme
*
* @default http
*/
scheme?: ProxyScheme;
/** Proxy host
*
* @default localhost
*/
host?: Hostname | IPv4 | IPv6;
/** Proxy port
*
* @default 80
*/
port?: Port;
/** Skip TLS verification
*
* @default false
*/
no_tls_verify?: boolean;
/** Response header timeout
*
* @default 60s
*/
response_header_timeout?: Duration;
/** Path patterns (only patterns that match will be proxied).
*
* See https://pkg.go.dev/net/http#hdr-Patterns-ServeMux
*/
path_patterns?: PathPattern[];
/** Healthcheck config */
healthcheck?: HealthcheckConfig;
/** Load balance config */
load_balance?: LoadBalanceConfig;
/** Middlewares */
middlewares?: MiddlewaresMap;
/** Homepage config
*
* @examples require(".").homepageExamples
*/
homepage?: HomepageConfig;
/** Access log config
*
* @examples require(".").accessLogExamples
*/
access_log?: AccessLogConfig;
};
export type FileServerRoute = {
/** Alias (subdomain or FDN)
* @minLength 1
*/
alias?: string;
scheme: "fileserver";
/* File server root path */
root: string;
/** Path patterns (only patterns that match will be proxied).
*
* See https://pkg.go.dev/net/http#hdr-Patterns-ServeMux
*/
path_patterns?: PathPattern[];
/** Middlewares */
middlewares?: MiddlewaresMap;
/** Homepage config
*
* @examples require(".").homepageExamples
*/
homepage?: HomepageConfig;
/** Access log config
*
* @examples require(".").accessLogExamples
*/
access_log?: AccessLogConfig;
/** Healthcheck config */
healthcheck?: HealthcheckConfig;
};
export type StreamRoute = {
/** Alias (subdomain or FDN)
* @minLength 1
*/
alias?: string;
/** Stream scheme
*
* @default tcp
*/
scheme?: StreamScheme;
/** Stream host
*
* @default localhost
*/
host?: Hostname | IPv4 | IPv6;
/* Stream port */
port: StreamPort;
/** Healthcheck config */
healthcheck?: HealthcheckConfig;
};
export const homepageExamples = [
{
name: "Sonarr",
icon: "png/sonarr.png",
category: "Arr suite",
},
{
name: "App",
icon: "@target/favicon.ico",
},
];
export const loadBalanceExamples = [
{
link: "flaresolverr",
mode: "round_robin",
},
{
link: "service.domain.com",
mode: "ip_hash",
config: {
header: "X-Real-IP",
},
},
];
export { accessLogExamples };

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"incremental": true,
"skipLibCheck": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"strict": false,
"esModuleInterop": false,
"forceConsistentCasingInFileNames": true,
"allowJs": false,
"resolveJsonModule": true,
"declaration": true,
"allowSyntheticDefaultImports": true
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}

109
schemas/types.d.ts vendored
View File

@@ -1,109 +0,0 @@
/**
* @type "null"
*/
export type Null = null;
export type Nullable<T> = T | Null;
export type NullOrEmptyMap = {} | Null;
export declare const HTTP_METHODS: readonly [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"CONNECT",
"HEAD",
"OPTIONS",
"TRACE",
];
export type HTTPMethod = (typeof HTTP_METHODS)[number];
/**
* HTTP Header
* @pattern ^[a-zA-Z0-9\-]+$
* @type string
*/
export type HTTPHeader = string & {};
/**
* HTTP Query
* @pattern ^[a-zA-Z0-9\-_]+$
* @type string
*/
export type HTTPQuery = string & {};
/**
* HTTP Cookie
* @pattern ^[a-zA-Z0-9\-_]+$
* @type string
*/
export type HTTPCookie = string & {};
export type StatusCode = number | `${number}`;
export type StatusCodeRange = number | `${number}` | `${number}-${number}`;
/**
* @pattern ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
*/
export type DomainName = string & {};
/**
* @pattern ^(\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
*/
export type DomainOrWildcard = string & {};
/**
* @format hostname
* @type string
*/
export type Hostname = string & {};
/**
* @format ipv4
* @type string
*/
export type IPv4 = string & {};
/**
* @format ipv6
* @type string
*/
export type IPv6 = string & {};
export type CIDR =
| `${number}.${number}.${number}.${number}`
| `${string}:${string}:${string}:${string}:${string}:${string}:${string}:${string}`
| `${number}.${number}.${number}.${number}/${number}`
| `::${number}`
| `${string}::/${number}`
| `${string}:${string}::/${number}`;
/**
* @type integer
* @minimum 0
* @maximum 65535
*/
export type Port = number | `${number}`;
/**
* @pattern ^\d+:\d+$
* @type string
*/
export type StreamPort = string & {};
/**
* @format email
* @type string
*/
export type Email = string & {};
/**
* @format uri
* @type string
*/
export type URL = string & {};
/**
* @format uri-reference
* @type string
*/
export type URI = string & {};
/**
* @pattern ^(?:([A-Z]+) )?(?:([a-zA-Z0-9.-]+)\\/)?(\\/[^\\s]*)$
* @type string
*/
export type PathPattern = string & {};
/**
* @pattern ^([0-9]+(ms|s|m|h))+$
* @type string
*/
export type Duration = string & {};
/**
* @format date-time
* @type string
*/
export type DateTime = string & {};

View File

@@ -1,11 +0,0 @@
export const HTTP_METHODS = [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"CONNECT",
"HEAD",
"OPTIONS",
"TRACE",
];

View File

@@ -1,127 +0,0 @@
/**
* @type "null"
*/
export type Null = null;
export type Nullable<T> = T | Null;
export type NullOrEmptyMap = {} | Null;
export const HTTP_METHODS = [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"CONNECT",
"HEAD",
"OPTIONS",
"TRACE",
] as const;
export type HTTPMethod = (typeof HTTP_METHODS)[number];
// "string & {}" Prevents skipping schema generation
/**
* HTTP Header
* @pattern ^[a-zA-Z0-9\-]+$
* @type string
*/
export type HTTPHeader = string & {};
/**
* HTTP Query
* @pattern ^[a-zA-Z0-9\-_]+$
* @type string
*/
export type HTTPQuery = string & {};
/**
* HTTP Cookie
* @pattern ^[a-zA-Z0-9\-_]+$
* @type string
*/
export type HTTPCookie = string & {};
export type StatusCode = number | `${number}`;
export type StatusCodeRange = number | `${number}` | `${number}-${number}`;
/**
* @pattern ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
*/
export type DomainName = string & {};
/**
* @pattern ^(\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
*/
export type DomainOrWildcard = string & {};
/**
* @format hostname
* @type string
*/
export type Hostname = string & {};
/**
* @format ipv4
* @type string
*/
export type IPv4 = string & {};
/**
* @format ipv6
* @type string
*/
export type IPv6 = string & {};
/* CIDR / IPv4 / IPv6 */
export type CIDR =
| `${number}.${number}.${number}.${number}`
| `${string}:${string}:${string}:${string}:${string}:${string}:${string}:${string}`
| `${number}.${number}.${number}.${number}/${number}`
| `::${number}`
| `${string}::/${number}`
| `${string}:${string}::/${number}`;
/**
* @type integer
* @minimum 0
* @maximum 65535
*/
export type Port = number | `${number}`;
/**
* @pattern ^\d+:\d+$
* @type string
*/
export type StreamPort = string & {};
/**
* @format email
* @type string
*/
export type Email = string & {};
/**
* @format uri
* @type string
*/
export type URL = string & {};
/**
* @format uri-reference
* @type string
*/
export type URI = string & {};
/**
* @pattern ^(?:([A-Z]+) )?(?:([a-zA-Z0-9.-]+)\\/)?(\\/[^\\s]*)$
* @type string
*/
export type PathPattern = string & {};
/**
* @pattern ^([0-9]+(ms|s|m|h))+$
* @type string
*/
export type Duration = string & {};
/**
* @format date-time
* @type string
*/
export type DateTime = string & {};

View File

@@ -2,6 +2,33 @@
set -e # Exit on error
check_cmd() {
not_available=()
for cmd in "$@"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
not_available+=("$cmd")
fi
done
if [ "${#not_available[@]}" -gt 0 ]; then
echo "Error: ${not_available[*]} unavailable, please install it first"
exit 1
fi
}
check_cmd openssl docker
# quit if running user is root
if [ "$EUID" -eq 0 ]; then
echo "Error: Please do not run this script as root"
exit 1
fi
# check if user has docker permission
if ! docker ps > /dev/null 2>&1; then
echo "Error: User $USER does not have permission to run docker, please add it to docker group"
exit 1
fi
# Detect download tool
if command -v curl >/dev/null 2>&1; then
DOWNLOAD_TOOL="curl"
@@ -10,13 +37,8 @@ elif command -v wget >/dev/null 2>&1; then
DOWNLOAD_TOOL="wget"
DOWNLOAD_CMD="wget -qO"
else
read -p "Neither curl nor wget is installed, install curl? (y/n): " INSTALL
if [ "$INSTALL" == "y" ]; then
install_pkg "curl"
else
echo "Error: Neither curl nor wget is installed. Please install one of them and try again."
exit 1
fi
echo "Error: Neither curl nor wget is installed. Please install one of them and try again."
exit 1
fi
echo "Using ${DOWNLOAD_TOOL} for downloads"
@@ -36,43 +58,12 @@ COMPOSE_FILE_NAME="compose.yml"
COMPOSE_EXAMPLE_FILE_NAME="compose.example.yml"
CONFIG_FILE_NAME="config.yml"
CONFIG_EXAMPLE_FILE_NAME="config.example.yml"
CONFIG_FILE_PATH="${CONFIG_BASE_PATH}/${CONFIG_FILE_NAME}"
REQUIRED_DIRECTORIES=("config" "logs" "error_pages" "data" "certs")
echo "Setting up GoDoxy"
echo "Branch: ${BRANCH}"
install_pkg() {
# detect package manager
if command -v apt >/dev/null 2>&1; then
apt install -y "$1"
elif command -v yum >/dev/null 2>&1; then
yum install -y "$1"
elif command -v pacman >/dev/null 2>&1; then
pacman -S --noconfirm "$1"
else
echo "Error: No supported package manager found"
exit 1
fi
}
check_pkg() {
local cmd="$1"
local pkg="$2"
if ! command -v "$cmd" >/dev/null 2>&1; then
# check if user is root
if [ "$EUID" -ne 0 ]; then
echo "Error: $pkg is not installed and you are not running as root. Please install it and try again."
exit 1
fi
read -p "$pkg is not installed, install it? (y/n): " INSTALL
if [ "$INSTALL" == "y" ]; then
install_pkg "$pkg"
else
echo "Error: $pkg is not installed. Please install it and try again."
exit 1
fi
fi
}
# Function to check if file/directory exists
has_file_or_dir() {
[ -e "$1" ]
@@ -133,6 +124,32 @@ ask_while_empty() {
eval "$var_name=\"$value\""
}
ask_multiple_choice() {
local var_name="$1"
local prompt="$2"
shift 2
local choices=("$@")
local n_choices="${#choices[@]}"
local value=""
local valid=0
while [ $valid -eq 0 ]; do
echo -e "$prompt"
for i in "${!choices[@]}"; do
echo "$((i + 1)). ${choices[$i]}"
done
read -p "Enter your choice: " value
if [ -z "$value" ]; then
echo "Error: $var_name cannot be empty, please try again"
fi
if [ "$value" -gt "$n_choices" ] || [ "$value" -lt 1 ]; then
echo "Error: invalid choice, please try again"
else
valid=1
fi
done
eval "$var_name=\"${choices[$((value - 1))]}\""
}
get_timezone() {
if [ -f /etc/timezone ]; then
TIMEZONE=$(cat /etc/timezone)
@@ -142,38 +159,45 @@ get_timezone() {
elif command -v timedatectl >/dev/null 2>&1; then
TIMEZONE=$(timedatectl status | grep "Time zone" | awk '{print $3}')
if [ -n "$TIMEZONE" ]; then
echo "$TIMEZONE"
echo "Detected timezone: $TIMEZONE"
fi
else
echo "Warning: could not detect timezone, you may set it manually in ${DOT_ENV_PATH} to have correct time in logs"
fi
}
check_pkg "openssl" "openssl"
check_pkg "docker" "docker-ce"
setenv() {
local key="$1"
local value="$2"
# uncomment line if it is commented
sed -i "/^# *${key}=/s/^# *//" "$DOT_ENV_PATH"
sed -i "s|${key}=.*|${key}=\"${value}\"|" "$DOT_ENV_PATH"
echo "${key}=${value}"
}
# Setup required configurations
# 1. Config base directory
mkdir_if_not_exists "$CONFIG_BASE_PATH"
# 1. Setup required directories
for dir in "${REQUIRED_DIRECTORIES[@]}"; do
mkdir_if_not_exists "$dir"
done
# 2. .env file
fetch_file "$DOT_ENV_EXAMPLE_PATH" "$DOT_ENV_PATH"
# set random JWT secret
JWT_SECRET=$(openssl rand -base64 32)
sed -i "s|GODOXY_API_JWT_SECRET=.*|GODOXY_API_JWT_SECRET=${JWT_SECRET}|" "$DOT_ENV_PATH"
setenv "GODOXY_API_JWT_SECRET" "$(openssl rand -base64 32)"
# set timezone
get_timezone
if [ -n "$TIMEZONE" ]; then
sed -i "s|TZ=.*|TZ=${TIMEZONE}|" "$DOT_ENV_PATH"
setenv "TZ" "$TIMEZONE"
fi
# 3. docker-compose.yml
fetch_file "$COMPOSE_EXAMPLE_FILE_NAME" "$COMPOSE_FILE_NAME"
# 4. config.yml
fetch_file "$CONFIG_EXAMPLE_FILE_NAME" "${CONFIG_BASE_PATH}/${CONFIG_FILE_NAME}"
fetch_file "$CONFIG_EXAMPLE_FILE_NAME" "$CONFIG_FILE_PATH"
# 5. setup authentication
@@ -182,44 +206,71 @@ echo "Setting up login user"
ask_while_empty "Enter login username: " LOGIN_USERNAME
ask_while_empty "Enter login password: " LOGIN_PASSWORD
echo "Setting up login user \"$LOGIN_USERNAME\" with password \"$LOGIN_PASSWORD\""
sed -i "s|GODOXY_API_USERNAME=.*|GODOXY_API_USERNAME=${LOGIN_USERNAME}|" "$DOT_ENV_PATH"
sed -i "s|GODOXY_API_PASSWORD=.*|GODOXY_API_PASSWORD=${LOGIN_PASSWORD}|" "$DOT_ENV_PATH"
setenv "GODOXY_API_USER" "$LOGIN_USERNAME"
setenv "GODOXY_API_PASSWORD" "$LOGIN_PASSWORD"
# 6. setup autocert
# ask if want to enable autocert
echo "Setting up autocert for SSL certificate"
ask_while_empty "Do you want to enable autocert? (y/n): " ENABLE_AUTOCERT
# quit if not using autocert
if [ "$ENABLE_AUTOCERT" == "y" ]; then
# ask for domain
echo "Setting up autocert"
ask_while_empty "Enter domain (e.g. example.com): " DOMAIN
skip=false
# ask for email
ask_while_empty "Enter email for Let's Encrypt: " EMAIL
# ask if using cloudflare
ask_while_empty "Is cloudflare the current DNS nameserver? (y/n): " USE_CLOUDFLARE
# select dns provider
ask_multiple_choice DNS_PROVIDER "Select DNS provider:" \
"Cloudflare" \
"CloudDNS" \
"DuckDNS" \
"Other"
# ask for cloudflare api key
if [ "$USE_CLOUDFLARE" = "y" ]; then
ask_while_empty "Enter cloudflare zone api key: " CLOUDFLARE_API_KEY
cat <<EOF >>"$CONFIG_BASE_PATH/$CONFIG_FILE_NAME"
autocert:
provider: cloudflare
email: $EMAIL
domains:
- "*.${DOMAIN}"
- "${DOMAIN}"
options:
auth_token: "$CLOUDFLARE_API_KEY"
EOF
# ask for dns provider credentials
if [ "$DNS_PROVIDER" == "Cloudflare" ]; then
provider="cloudflare"
read -p "Enter cloudflare zone api key: " auth_token
options=("auth_token: \"$auth_token\"")
elif [ "$DNS_PROVIDER" == "CloudDNS" ]; then
provider="clouddns"
read -p "Enter clouddns client_id: " client_id
read -p "Enter clouddns email: " email
read -p "Enter clouddns password: " password
options=(
"client_id: \"$client_id\""
"email: \"$email\""
"password: \"$password\""
)
elif [ "$DNS_PROVIDER" == "DuckDNS" ]; then
provider="duckdns"
read -p "Enter duckdns token: " token
options=("token: \"$token\"")
else
echo "Not using cloudflare, skipping autocert setup"
echo "Please refer to ${WIKI_URL}/Supported-DNS-01-Providers for more information"
echo "Please check Wiki for other DNS providers: ${WIKI_URL}/Supported-DNS%E2%80%9001-Providers"
echo "Skipping autocert setup"
skip=true
fi
if [ "$skip" == false ]; then
autocert_config="
autocert:
provider: \"${provider}\"
email: \"${EMAIL}\"
domains:
- \"*.${BASE_DOMAIN}\"
- \"${BASE_DOMAIN}\"
options:
"
for option in "${options[@]}"; do
autocert_config+=" ${option}\n"
done
autocert_config+="\n"
echo -e "${autocert_config}$(<"$CONFIG_FILE_PATH")" >"$CONFIG_FILE_PATH"
fi
fi
# 7. set uid and gid
setenv "GODOXY_UID" "$(id -u)"
setenv "GODOXY_GID" "$(id -g)"
echo "Setup finished"