Skip to content

Commit 5dc8c02

Browse files
committed
Merge develop — Release v0.7.5: per-IP rate limiting
2 parents 257d9f4 + 7f33b36 commit 5dc8c02

17 files changed

Lines changed: 850 additions & 25 deletions

File tree

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,10 @@ GGHSTATS_ENABLED_LOCALES=en,es,de,fr,pt-br
6464
# Spanish-first example:
6565
# GGHSTATS_DEFAULT_LOCALE=es
6666
# GGHSTATS_ENABLED_LOCALES=en,es,de
67+
68+
# --- Rate limiting (enabled by default) ---
69+
# GGHSTATS_RATE_LIMIT_ENABLED=true
70+
# GGHSTATS_RATE_LIMIT_REQUESTS=120
71+
# GGHSTATS_RATE_LIMIT_PERIOD=1m
72+
# GGHSTATS_RATE_LIMIT_BURST=20
73+

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
!go.mod
2828
!go.sum
2929

30+
# Install script (curl | sh from GitHub raw)
31+
!scripts/
32+
!scripts/**
33+
3034
# Allowlist: source trees
3135
!cmd/
3236
!cmd/**

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ All notable changes to this project are documented in this file.
55
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66
This project adheres to [Semantic Versioning](https://semver.org/).
77

8-
## [Unreleased]
8+
## [0.7.5] - 2026-06-14
9+
10+
### Added
11+
12+
- **`scripts/install.sh`:** one-liner install from GitHub releases (`curl -fsSL https://raw.githubusercontent.com/hrodrig/gghstats/main/scripts/install.sh | sh`).
13+
- **Per-IP rate limiting:** protects the HTTP server from abuse with a configurable token-bucket middleware. Enabled by default (120 req/min, burst 20). Skipped for `/metrics` and `/api/v1/healthz`. Configure via `GGHSTATS_RATE_LIMIT_*` env vars; set `GGHSTATS_RATE_LIMIT_ENABLED=false` to disable.
14+
15+
### Security
16+
17+
- Rate limiting mitigates M7 from the professionalism audit (no protection on `POST /api/v1/sync`).
918

1019
## [0.7.4] - 2026-06-10
1120

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ RUN go build -ldflags "-s -w \
2121
-o gghstats ./cmd/gghstats
2222

2323
# Alpine 3.22+: fresher busybox/ca-certificates vs Grype noise on 3.21; apk upgrade pulls security revisions.
24-
FROM alpine:3.22
24+
FROM alpine:3.24
2525
RUN apk update \
2626
&& apk add --no-cache ca-certificates \
2727
&& apk upgrade --no-cache \

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,13 @@ Tap: [homebrew-gghstats](https://github.com/hrodrig/homebrew-gghstats). The cask
118118
### Local binary (fastest try)
119119

120120
```bash
121-
# From Releases: extract gghstats_*_linux_* archive, or: go install github.com/hrodrig/gghstats/cmd/gghstats@latest
121+
curl -fsSL https://raw.githubusercontent.com/hrodrig/gghstats/main/scripts/install.sh | sh
122122
export GGHSTATS_GITHUB_TOKEN=ghp_xxx
123123
gghstats run --open
124124
```
125125

126+
Or extract a [Release](https://github.com/hrodrig/gghstats/releases) tarball, or `go install github.com/hrodrig/gghstats/cmd/gghstats@latest`. Pin a version: `VERSION=v0.7.4 curl -fsSL …/install.sh | sh`.
127+
126128
Open <http://localhost:8080> if you did not use **`--open`**. Data is stored in `./data/gghstats.db` (override with `GGHSTATS_DB`). A first sync may take a while if the default filter includes many repositories — narrow `GGHSTATS_FILTER` in [Configuration](#configuration) when you move beyond this smoke test.
127129

128130
### Docker (one command, no clone — UI smoke test)
@@ -159,6 +161,14 @@ Open <http://localhost:8080>. The template [`.env.example`](.env.example) docume
159161

160162
**Quick install** — get the binary on your machine. **Configuration on a server, systemd, `.deb`/`.rpm` setup, Compose, Traefik, Helm, env files, and VPS deployment** are documented only in **[gghstats-selfhosted](https://github.com/hrodrig/gghstats-selfhosted)** ([`run/`](https://github.com/hrodrig/gghstats-selfhosted/tree/main/run), [`run/standalone/linux/`](https://github.com/hrodrig/gghstats-selfhosted/blob/main/run/standalone/linux/README.md) for Linux packages and systemd).
161163

164+
**One-liner (Linux, macOS, BSD — no Docker):**
165+
166+
```bash
167+
curl -fsSL https://raw.githubusercontent.com/hrodrig/gghstats/main/scripts/install.sh | sh
168+
```
169+
170+
Downloads the latest [release](https://github.com/hrodrig/gghstats/releases) archive for your OS/arch into `/usr/local/bin` (override with `BINDIR=~/bin`). Review [`scripts/install.sh`](scripts/install.sh) before piping to `sh`; pin with `VERSION=v0.7.4`.
171+
162172
**From source (recommended for developers):**
163173

164174
```bash
@@ -361,6 +371,10 @@ Copy [`.env.example`](.env.example) → `.env` in this repository when running `
361371
| `GGHSTATS_CUSTOM_CSS` | (none) | Optional **regular** `.css` file: loaded **after** built-in `app.css` at `/theme/custom.css` so you can tone down neo-brutalism or replace accents (see [Custom UI theme](#custom-ui-theme-optional)) |
362372
| `GGHSTATS_DEFAULT_LOCALE` | `en` | Default **dashboard** language when no cookie, `?lang=`, or `Accept-Language` match (see [Web UI languages](#web-ui-languages-i18n)) |
363373
| `GGHSTATS_ENABLED_LOCALES` | `en,es,de` | Comma-separated locales shown in the sidebar selector and accepted from `?lang=` / cookie |
374+
| `GGHSTATS_RATE_LIMIT_ENABLED` | `true` | Set to `false` to disable per-IP rate limiting |
375+
| `GGHSTATS_RATE_LIMIT_REQUESTS` | `120` | Requests per time window before limiting (per IP) |
376+
| `GGHSTATS_RATE_LIMIT_PERIOD` | `1m` | Time window for rate limiting (Go duration, e.g. `30s`, `5m`) |
377+
| `GGHSTATS_RATE_LIMIT_BURST` | `20` | Maximum burst of requests allowed before smoothing kicks in |
364378

365379
### Web UI languages (i18n)
366380

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.7.4
1+
0.7.5

cmd/gghstats/serve.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ func runServe(args []string) error {
127127
gh.SetMetrics(domainMetrics)
128128
}
129129

130+
rateLimiter := setupRateLimiter()
131+
if rateLimiter != nil {
132+
defer rateLimiter.Shutdown()
133+
}
134+
130135
syncOpts := sync.Options{
131136
IncludePrivate: cfg.IncludePrivate,
132137
Filter: cfg.Filter,
@@ -142,11 +147,7 @@ func runServe(args []string) error {
142147
defer cancel()
143148
go startScheduler(ctx, coord, cfg.SyncInterval, cfg.SyncOnStartup)
144149

145-
cssAbs, cssQuery := server.ResolveCustomCSS(os.Getenv("GGHSTATS_CUSTOM_CSS"))
146-
if strings.TrimSpace(os.Getenv("GGHSTATS_CUSTOM_CSS")) != "" && cssAbs == "" {
147-
slog.Warn("GGHSTATS_CUSTOM_CSS ignored: path is missing or not a regular file",
148-
"GGHSTATS_CUSTOM_CSS", os.Getenv("GGHSTATS_CUSTOM_CSS"))
149-
}
150+
cssAbs, cssQuery := resolveCSSPath()
150151

151152
// Start HTTP server
152153
handler := server.New(server.Config{
@@ -163,6 +164,7 @@ func runServe(args []string) error {
163164
CustomCSSQuery: cssQuery,
164165
DefaultLocale: i18n.EnvDefaultLocale(),
165166
EnabledLocales: i18n.EnvEnabledLocales(),
167+
RateLimiter: rateLimiter,
166168
})
167169

168170
addr := cfg.Host + ":" + cfg.Port
@@ -238,3 +240,26 @@ func startScheduler(ctx context.Context, coord *sync.Coordinator, interval time.
238240
}
239241
}
240242
}
243+
244+
func resolveCSSPath() (cssAbs, cssQuery string) {
245+
cssAbs, cssQuery = server.ResolveCustomCSS(os.Getenv("GGHSTATS_CUSTOM_CSS"))
246+
if strings.TrimSpace(os.Getenv("GGHSTATS_CUSTOM_CSS")) != "" && cssAbs == "" {
247+
slog.Warn("GGHSTATS_CUSTOM_CSS ignored: path is missing or not a regular file",
248+
"GGHSTATS_CUSTOM_CSS", os.Getenv("GGHSTATS_CUSTOM_CSS"))
249+
}
250+
return
251+
}
252+
253+
func setupRateLimiter() *server.RateLimiter {
254+
rlCfg := server.ParseRateLimitEnv()
255+
if !rlCfg.Enabled {
256+
return nil
257+
}
258+
rl := server.NewRateLimiter(rlCfg)
259+
slog.Info("rate limiter enabled",
260+
"requests", rlCfg.Requests,
261+
"period", rlCfg.Period,
262+
"burst", rlCfg.Burst,
263+
)
264+
return rl
265+
}

contrib/freebsd/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PORTNAME= gghstats
2-
PORTVERSION= 0.7.4
2+
PORTVERSION= 0.7.5
33
DISTVERSIONPREFIX= v
44
CATEGORIES= sysutils
55
# Local test before release: copy the tarball into DISTDIR (see `make -V DISTDIR`

contrib/gghstats.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,9 @@ GGHSTATS_DEFAULT_LOCALE=en
3939
GGHSTATS_ENABLED_LOCALES=en,es,de,fr,pt-br
4040

4141
# GGHSTATS_LOG_LEVEL=info
42+
43+
# Rate limiting (enabled by default; set to false to disable)
44+
# GGHSTATS_RATE_LIMIT_ENABLED=true
45+
# GGHSTATS_RATE_LIMIT_REQUESTS=120
46+
# GGHSTATS_RATE_LIMIT_PERIOD=1m
47+
# GGHSTATS_RATE_LIMIT_BURST=20

contrib/man/man1/gghstats.1

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.\" Man page for gghstats(1)
2-
.TH GGHSTATS 1 "June 2026" "gghstats v0.7.4" "User Commands"
2+
.TH GGHSTATS 1 "June 2026" "gghstats v0.7.5" "User Commands"
33
.SH NAME
44
gghstats \- self-hosted GitHub repository traffic dashboard and CLI
55
.SH SYNOPSIS
@@ -104,6 +104,18 @@ Allow public badge SVG URLs (default: \fBtrue\fR).
104104
.TP
105105
.B GGHSTATS_METRICS
106106
Set to \fBfalse\fR to disable Prometheus \fB/metrics\fR.
107+
.TP
108+
.B GGHSTATS_RATE_LIMIT_ENABLED
109+
Set to \fBfalse\fR to disable per-IP rate limiting (default: \fBtrue\fR).
110+
.TP
111+
.B GGHSTATS_RATE_LIMIT_REQUESTS
112+
Requests per time window before limiting per IP (default: \fB120\fR).
113+
.TP
114+
.B GGHSTATS_RATE_LIMIT_PERIOD
115+
Time window for rate limiting as a Go duration (default: \fB1m\fR).
116+
.TP
117+
.B GGHSTATS_RATE_LIMIT_BURST
118+
Maximum burst of requests before smoothing (default: \fB20\fR).
107119
.SH EXAMPLES
108120
.TP
109121
Run the dashboard locally:

0 commit comments

Comments
 (0)