Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.25'
- uses: goreleaser/goreleaser-action@v6
with:
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 changes: 33 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Test
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: proxy
POSTGRES_PASSWORD: proxy
POSTGRES_DB: etherpad_proxy
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U proxy"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Vet
run: go vet ./...
- name: Test (race)
env:
PG_TEST_DSN: postgres://proxy:proxy@localhost:5432/etherpad_proxy?sslmode=disable
run: go test -race ./...
32 changes: 32 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
version: 2
project_name: etherpad-proxy
builds:
- id: etherpad-proxy
main: .
binary: etherpad-proxy
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
archives:
- id: default
formats: [tar.gz]
format_overrides:
- goos: windows
formats: [zip]
files:
- README.md
- LICENSE.md
- settings.json.template
- support/etherpad-proxy.service
checksum:
name_template: 'checksums.txt'
release:
github:
owner: ether
name: etherpad-proxy
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,94 @@ This is now a production grade reverse proxy written in Golang.

Visit http://localhost:9000

## Installation without Docker

The proxy is a single static Go binary. You can run it directly on a host
without Docker.

### Option A: download a prebuilt binary

1. Go to the [Releases page](https://github.com/ether/etherpad-proxy/releases)
and download the archive for your OS/architecture.
2. Extract it: `tar xzf etherpad-proxy_*.tar.gz` (or unzip on Windows).
3. Continue with **Configure** below.

### Option B: build from source

Requires Go 1.25 or newer.

```bash
git clone https://github.com/ether/etherpad-proxy.git
cd etherpad-proxy
go build -o etherpad-proxy .
```

### Configure

1. Copy the template and edit it:
```bash
cp settings.json.template settings.json
```
2. Set `port` (proxy listen port), optionally `managementPort`
(default `8081`, serves `/pads`, `/metrics`, `/healthz`, `/readyz`), your
`backends`, and a database (see **Database** below).
3. The settings path defaults to `./settings.json`; override it with the
`SETTINGS_FILE` environment variable.
Comment on lines +49 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Broken config template 🐞 Bug ≡ Correctness

README now instructs copying settings.json.template, but the shipped templates are invalid JSON
(trailing commas) and use tokenURL while the code expects tokenUrl, so startup will fatally fail
at json.Unmarshal/Validate for common OAuth configs.
Agent Prompt
## Issue description
The project recommends copying `settings.json.template`, but the template(s) are not parseable by `encoding/json` (trailing commas) and use the key `tokenURL` while the code unmarshals `tokenUrl` and then enforces it via `Settings.Validate()`. This makes the documented setup path fail at startup.

## Issue Context
- `main.go` uses `encoding/json.Unmarshal` and then calls `Settings.Validate()`.
- `models.Backend.TokenURL` is tagged `json:"tokenUrl"` and validation requires it when OAuth is configured.
- Both `settings.json.template` and `settings.json.sqlite.template` currently use `tokenURL` and include trailing commas.

## Fix Focus Areas
- README.md[49-60]
- settings.json.template[1-27]
- settings.json.sqlite.template[1-27]
- models/Settings.go[22-60]
- main.go[21-33]

## Suggested fix
1. Make both template files valid JSON (remove trailing commas).
2. Decide on one key name and make it consistent across:
   - templates
   - README OAuth section (currently says `tokenURL`)
   - code (`models.Backend.TokenURL` tag and validation message)
3. Strongly consider backward compatibility for existing configs that likely followed the README (`tokenURL`): implement custom unmarshalling or accept both `tokenURL` and `tokenUrl` and normalize into one field before validation/usage.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


### Run

```bash
SETTINGS_FILE=/path/to/settings.json ./etherpad-proxy
```

The proxy listens on `port`; management/metrics/health endpoints listen on
`managementPort`.

### Database

- **SQLite (default, single instance):** set `dbSettings.filename`, e.g.
`"db/etherpad-proxy.db"`. No external services required. Create the directory
first: `mkdir -p db`.
- **Postgres (recommended for multiple proxy instances):** set
`dbSettings.postgresConnstr`. With a shared Postgres, several proxy instances
share routing state and assign new pads atomically, so they never route the
same pad to different backends.

Provision a database and user, for example:
```sql
CREATE USER proxy WITH PASSWORD 'changeme';
CREATE DATABASE etherpad_proxy OWNER proxy;
```
Then set:
```json
"dbSettings": {
"postgresConnstr": "postgres://proxy:changeme@db-host:5432/etherpad_proxy?sslmode=disable"
}
```
The proxy creates its tables automatically on first start. Set exactly one of
`filename` or `postgresConnstr`.

### Run as a systemd service (Linux)

A sample unit is provided at `support/etherpad-proxy.service`.

```bash
sudo useradd --system --no-create-home --shell /usr/sbin/nologin etherpad-proxy
sudo mkdir -p /opt/etherpad-proxy/db
sudo cp etherpad-proxy /opt/etherpad-proxy/
sudo cp settings.json /opt/etherpad-proxy/
sudo cp support/etherpad-proxy.service /etc/systemd/system/
sudo chown -R etherpad-proxy:etherpad-proxy /opt/etherpad-proxy
sudo systemctl daemon-reload
sudo systemctl enable --now etherpad-proxy
```

Check status and logs:
```bash
systemctl status etherpad-proxy
journalctl -u etherpad-proxy -f
```

## Settings

Settings come from ``settings.json``, see ``settings.json.template`` for an example to modify for your environment.
Expand Down
59 changes: 59 additions & 0 deletions checkAvailability_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package main

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"slices"
"strconv"
"testing"

"github.com/ether/etherpad-proxy/models"
)

// newStatsBackend starts a test server replying to /stats with the given
// activePads count, and returns its host and port.
func newStatsBackend(t *testing.T, activePads int) (string, int) {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/stats" {
fmt.Fprintf(w, `{"activePads": %d}`, activePads)
return
}
w.WriteHeader(http.StatusNotFound)
}))
t.Cleanup(srv.Close)
u, _ := url.Parse(srv.URL)
port, _ := strconv.Atoi(u.Port())
return u.Hostname(), port
}

func TestCheckAvailabilityCapacityAndDown(t *testing.T) {
okHost, okPort := newStatsBackend(t, 1) // under capacity -> up + available
fullHost, fullPort := newStatsBackend(t, 99) // over capacity -> up but not available

settings := models.Settings{
MaxPadsPerInstance: 5,
Backends: map[string]models.Backend{
"ok": {Host: okHost, Port: okPort},
"full": {Host: fullHost, Port: fullPort},
"down": {Host: "127.0.0.1", Port: 1}, // nothing listening -> not up
},
}

got := checkAvailability(settings)

if !slices.Contains(got.Up, "ok") || !slices.Contains(got.Up, "full") {
t.Fatalf("expected ok and full to be up, got %v", got.Up)
}
if slices.Contains(got.Up, "down") {
t.Fatalf("down backend should not be up, got %v", got.Up)
}
if !slices.Contains(got.Available, "ok") {
t.Fatalf("ok should be available, got %v", got.Available)
}
if slices.Contains(got.Available, "full") {
t.Fatalf("full backend should not be available, got %v", got.Available)
}
}
4 changes: 4 additions & 0 deletions databases/interfaces/iDB.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ type IDB interface {
Set(id string, backend models.DBBackend) error
GetAllPads() (map[string]string, error)
GetClashByPadID(id string) ([]string, error)
// Assign stores candidate as the backend for padId only if no backend is
// already stored, and returns the backend now authoritative for padId (the
// pre-existing one if there was a race, otherwise candidate).
Assign(padId string, candidate string) (string, error)
}
Loading
Loading