Mercurial Hosting > d2o
changeset 1:3e7247db5c6e
show index.html
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Mon, 09 Mar 2026 01:04:16 +0500 |
| parents | 48bdab3eec8a |
| children | d19133be91ba |
| files | README.md main.go |
| diffstat | 2 files changed, 296 insertions(+), 15 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.md Mon Mar 09 01:04:16 2026 +0500 @@ -0,0 +1,273 @@ +# d2o + +Минималистичный веб-сервер на Go с конфигурацией в формате ICF. + +## Структура проекта + +``` +d2o/ + go.mod + cmd/d2o/ + main.go — точка входа, HTTP-обработчик, сборка листеров + icf/ + icf.go — парсер формата ICF (переиспользуемая библиотека) + fcgi/ + fcgi.go — минимальный FastCGI-клиент +``` + +## Сборка и запуск + +```sh +go build -o d2o ./cmd/d2o/ +./d2o # читает /etc/d2obase +./d2o /path/to/config # альтернативный путь +``` + +--- + +## Формат конфигурации ICF + +**ICF (Inherited Configuration Format)** — текстовый формат правил вида «паттерн → директивы». +Вдохновлён синтаксисом bash и базами данных CoreDNS. + +### Основные правила синтаксиса + +``` +; Это комментарий + +; Переменная (без пробелов вокруг =) +KEY=value + +; Абстрактный блок (миксин) +@name +|> directive arg1 arg2 + +; Конкретный блок с наследованием миксина +block.id @mixin +|> directive arg1 arg2 + +; Блок с именованным capture-группой +<sub>.example.com +|> root /srv/$sub +``` + +### Переменные + +Объявляются как `KEY=value` на уровне файла. Подставляются через `$KEY` в аргументах директив. + +``` +WWW=/srv/www +CERT=/etc/acme/example.com + +example.com +|> root $WWW/root +|> tls $CERT.{crt,key} +``` + +### Brace expansion + +Аргумент `prefix.{a,b,c}` раскрывается в три отдельных аргумента: `prefix.a`, `prefix.b`, `prefix.c`. +Порядок важен — используется для `tls`, где первый аргумент сертификат, второй ключ. + +``` +|> tls /etc/acme/example.com.{crt,key} +; эквивалентно: +|> tls /etc/acme/example.com.crt /etc/acme/example.com.key +``` + +### Capture-группы + +В идентификаторе блока `<name>` захватывает любую подстроку до следующего литерала. +`<_>` — анонимный wildcard, ничего не сохраняет. + +``` +<sub>.example.com +|> root /srv/$sub ; $sub подставляется из захваченного значения + +<_>.static.example.com +|> root /srv/static ; совпадает с чем угодно, capture не нужен +``` + +Capture жадный слева: `<sub>.example.com` на запрос `a.b.example.com` даст `sub=a.b`. + +### Кавычки в аргументах + +Аргументы с пробелами берутся в двойные кавычки: + +``` +|> fcgi unix:/run/php-fpm.sock "*.php" +``` + +### Абстрактные блоки и наследование + +`@name` задаёт набор директив без привязки к паттерну. +Конкретный блок наследует их через `block.id @name` — директивы миксина идут первыми, директивы блока их перекрывают. + +``` +@base +|> port 80 +|> port+tls 443 + +example.com @base +|> root /srv/www ; наследует port 80 и port+tls 443 + +other.com @base +|> root /srv/other ; то же самое +``` + +### Матчинг блоков + +- Домен матчится точно (с учётом capture-групп). +- Путь матчится как префикс: блок `example.com/api` сработает на `/api/users`. +- При нескольких совпадениях выигрывает наиболее специфичный (больше совпавших литеральных символов). +- Сначала проверяется `host/path`, потом `host`. + +``` +example.com +|> root /srv/www + +example.com/api +|> rprx 127.0.0.1:8080 ; перекрывает блок выше для /api/* +``` + +--- + +## Конфигурация d2o + +Файл по умолчанию: `/etc/d2obase` + +### @d2o — настройки сервера + +``` +@d2o +|> threads 512 ; GOMAXPROCS +``` + +| Директива | Аргументы | Описание | +|------------|-----------|----------| +| `threads` | N | Количество потоков ОС (GOMAXPROCS) | + +### port — HTTP-листенер + +``` +|> port 80 +``` + +| # | Тип | Описание | +|---|-------|--------------| +| 1 | `int` | Номер порта | + +### port+tls — HTTPS-листенер + +``` +|> port+tls 443 +; сертификат берётся из директивы tls того же блока +``` + +Знак `+` в имени директивы — обычный символ, не оператор. + +| # | Тип | Описание | +|---|--------|-----------------------------------------------| +| 1 | `int` | Номер порта | +| 2 | `path` | Путь к сертификату (необязателен, если есть `tls`) | +| 3 | `path` | Путь к ключу (необязателен, если есть `tls`) | + +### tls — пути к сертификату и ключу + +``` +|> tls /etc/acme/example.com.{crt,key} +``` + +Используется как источник сертификата для `port+tls` в том же блоке, если пути не указаны явно. + +| # | Тип | Описание | +|---|--------|-------------------| +| 1 | `path` | Путь к сертификату | +| 2 | `path` | Путь к ключу | + +### root — отдача статики + +``` +|> root /srv/www/example.com +|> root /srv/www/example.com hide ; листинг запрещён (по умолчанию) +|> root /srv/www/example.com show ; листинг разрешён, index-файл index.html +|> root /srv/www/example.com show index.php index.html ; свои index-файлы +``` + +| # | Тип | Описание | +|---|--------|---------------------------------| +| 1 | `path` | Корневая директория | +| 2 | `show\|hide` | Режим директорий (по умолчанию `hide`) | +| 3–14 | `filename` | Index-файлы (только с `show`), проверяются по порядку | + +При `show`: сначала ищутся index-файлы по списку, если ни один не найден — отдаётся листинг директории. +При `hide` или без аргумента: запрос к директории возвращает 403. + +### fcgi — FastCGI + +``` +|> fcgi unix:/run/php-fpm.sock +|> fcgi unix:/run/php-fpm.sock "*.php" ; только .php файлы +|> fcgi 127.0.0.1:9000 "*.php" +``` + +| # | Тип | Описание | +|---|---------|----------------------------------------------------| +| 1 | `addr` | Адрес сокета: `unix:/path` или `host:port` | +| 2 | `glob` | Паттерн файлов (по умолчанию `*`, все запросы) | + +Если glob не совпадает, запрос падает в `root` (если задан). + +### rprx — обратный прокси + +``` +|> rprx 127.0.0.1:3000 +|> rprx http://127.0.0.1:3000 +``` + +| # | Тип | Описание | +|---|-------|---------------------------------| +| 1 | `url` | Адрес backend (схема необязательна, по умолчанию `http://`) | + +### Приоритет директив + +При одновременном наличии нескольких директив порядок обработки: **rprx > fcgi > root**. + +--- + +## Пример полного конфига + +``` +; /etc/d2obase + +ACME=/etc/acme/qwaderton.org +WWW=/srv/www/qwaderton.org + +@d2o +|> threads 512 + +@ports +|> tls $ACME.{crt,key} +|> port 80 +|> port+tls 443 + +qwaderton.org @ports +|> root $WWW/root show index.php index.html + +qwaderton.org/webfeather @ports +|> root $WWW/root/webfeather +|> fcgi unix:/run/php-fpm.sock *.php + +<sub>.qwaderton.org @ports +|> root $WWW/$sub +``` + +--- + +## Известные ограничения + +- **Один миксин на блок.** Множественное наследование не поддерживается. +- **Brace expansion без вложенности.** `{a,{b,c}}` не работает. +- **FastCGI — один запрос на соединение.** Keep-alive с FPM не реализован. +- **Нет HTTP→HTTPS редиректа** из коробки — нужно реализовывать отдельным блоком на порту 80. +- **Нет hot reload** конфига — требуется перезапуск процесса. \ No newline at end of file
--- a/main.go Mon Mar 09 00:37:49 2026 +0500 +++ b/main.go Mon Mar 09 01:04:16 2026 +0500 @@ -36,7 +36,6 @@ log.Fatalf("d2o: config error: %v", err) } - // Apply @d2o global settings for _, d := range cfg.Abstract("d2o") { switch d.Key { case "threads": @@ -94,7 +93,6 @@ return http.Serve(ln, h) } -// collectPorts scans all blocks for port / port+tls directives. func collectPorts(cfg *icf.Config) []portConfig { seen := make(map[string]bool) var out []portConfig @@ -102,7 +100,6 @@ for _, b := range cfg.Blocks { dirs := cfg.ResolveBlock(b, nil) - // Collect tls paths defined in this block var cert, key string for _, d := range dirs { if d.Key == "tls" { @@ -149,7 +146,6 @@ host := stripPort(r.Host) reqPath := path.Clean(r.URL.Path) - // Try host+path first, then host alone dirs, caps := h.cfg.Match(host + reqPath) if dirs == nil { dirs, caps = h.cfg.Match(host) @@ -164,11 +160,11 @@ func (h *handler) serve(w http.ResponseWriter, r *http.Request, dirs []icf.Directive, _ map[string]string) { var ( - rootDir string - rootShow bool - fcgiAddr string - fcgiPat string - rprxAddr string + rootDir string + rootIndex []string + fcgiAddr string + fcgiPat string + rprxAddr string ) for _, d := range dirs { @@ -177,9 +173,13 @@ rootDir = safeArg(d.Args, 0) switch safeArg(d.Args, 1) { case "show": - rootShow = true + if len(d.Args) >= 3 { + rootIndex = d.Args[2:] + } else { + rootIndex = []string{"index.html"} + } case "hide", "": - rootShow = false + rootIndex = nil default: log.Printf("d2o: root: unknown mode %q (want show|hide)", safeArg(d.Args, 1)) } @@ -194,7 +194,6 @@ } } - // Priority: rprx > fcgi > static root if rprxAddr != "" { serveReverseProxy(w, r, rprxAddr) return @@ -207,7 +206,7 @@ return } if rootDir != "" { - serveStatic(w, r, rootDir, rootShow) + serveStatic(w, r, rootDir, rootIndex) return } @@ -216,7 +215,7 @@ // --- Static ----------------------------------------------------------------- -func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, showDir bool) { +func serveStatic(w http.ResponseWriter, r *http.Request, rootDir string, rootIndex []string) { fpath := filepath.Join(rootDir, filepath.FromSlash(path.Clean(r.URL.Path))) info, err := os.Stat(fpath) @@ -228,14 +227,23 @@ http.Error(w, "internal error", http.StatusInternalServerError) return } + if info.IsDir() { - if !showDir { + if rootIndex == nil { http.Error(w, "forbidden", http.StatusForbidden) return } + for _, idx := range rootIndex { + idxPath := filepath.Join(fpath, idx) + if _, err := os.Stat(idxPath); err == nil { + http.ServeFile(w, r, idxPath) + return + } + } listDir(w, r, fpath, r.URL.Path) return } + http.ServeFile(w, r, fpath) }
