a9b1073877
Сервер сам загружает заголовок, описание и картинку страницы по ссылке из сообщения и отдаёт клиенту готовую карточку. Безопасность (главное здесь): - только http/https и без userinfo; запрет петли, частных сетей, link-local (169.254.169.254), CGNAT, multicast и IPv4-mapped вариантов; - проверка идёт по адресу, к которому реально открывается TCP (`net.Dialer.Control`), поэтому подмена DNS между проверкой и соединением (DNS rebinding) ничего не даёт; - не больше 3 редиректов, каждый хоп проверяется заново; таймаут 5 с, тело ≤ 512 КБ, только `text/html`; прокси из окружения игнорируются, cookie и авторизация не отправляются; в логи попадают только хост и код причины; - картинка по ссылке не скачивается — проверяется лишь её URL: экономия CPU на 1 vCPU и минус класс атак через декодирование. Кэш: таблица `link_previews` (миграция 00022, ключ — sha256 нормализованного URL), TTL по статусу (ok — сутки, empty/blocked — час, error — 10 минут). Ручка `GET /api/v1/link-previews?url=…` отвечает статусом (ok/empty/blocked/error) и карточкой только при ok; 20 новых загрузок в минуту на пользователя, кэшированные ответы лимит не тратят. `UNFURL_ENABLED=false` выключает функцию целиком, `features.unfurl_enabled` виден в `/meta`. Retention убирает истёкшие записи кэша. Тесты: 21 в `internal/unfurl` (включая DNS rebinding через локальный DNS-сервер, редирект во внутреннюю сеть, таймаут, лимиты размера и типа), ручки, store и миграция.
61 lines
2.1 KiB
Go
61 lines
2.1 KiB
Go
package meta
|
|
|
|
import "glchat/internal/config"
|
|
|
|
const APIVersion = "v1"
|
|
|
|
type Features struct {
|
|
RegistrationEnabled bool `json:"registration_enabled"`
|
|
AntiBotEnabled bool `json:"anti_bot_enabled"`
|
|
VoiceEnabled bool `json:"voice_enabled"`
|
|
// VoiceURL — адрес LiveKit для клиента (ws/wss), пусто при выключенном голосе.
|
|
VoiceURL string `json:"voice_url,omitempty"`
|
|
WebPushEnabled bool `json:"web_push_enabled"`
|
|
UnfurlEnabled bool `json:"unfurl_enabled"`
|
|
OAuthEnabled bool `json:"oauth_enabled"`
|
|
PasskeysEnabled bool `json:"passkeys_enabled"`
|
|
// OAuthProviders — включённые провайдеры входа (id): клиент рисует по
|
|
// списку кнопки, выключенные не показывает (Фаза 7).
|
|
OAuthProviders []string `json:"oauth_providers"`
|
|
}
|
|
|
|
type Response struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Commit string `json:"commit"`
|
|
BuildDate string `json:"build_date"`
|
|
APIVersion string `json:"api_version"`
|
|
BaseURL string `json:"base_url"`
|
|
FilesURL string `json:"files_url"`
|
|
GatewayURL string `json:"gateway_url"`
|
|
RTCPath string `json:"rtc_path"`
|
|
MaxUploadSize int64 `json:"max_upload_size"`
|
|
Features Features `json:"features"`
|
|
}
|
|
|
|
func New(cfg config.Config) Response {
|
|
return Response{
|
|
Name: cfg.InstanceName,
|
|
Version: cfg.Version,
|
|
Commit: cfg.Commit,
|
|
BuildDate: cfg.BuildDate,
|
|
APIVersion: APIVersion,
|
|
BaseURL: cfg.BaseURL(),
|
|
FilesURL: cfg.FilesURL(),
|
|
GatewayURL: cfg.GatewayURL(),
|
|
RTCPath: "/rtc",
|
|
MaxUploadSize: cfg.MaxUploadSize,
|
|
Features: Features{
|
|
RegistrationEnabled: true,
|
|
AntiBotEnabled: false,
|
|
VoiceEnabled: cfg.VoiceEnabled(),
|
|
VoiceURL: cfg.LiveKitURL,
|
|
WebPushEnabled: cfg.WebPushEnabled(),
|
|
UnfurlEnabled: cfg.UnfurlEnabled,
|
|
OAuthEnabled: cfg.OAuthEnabled(),
|
|
PasskeysEnabled: cfg.WebAuthnEnabled(),
|
|
OAuthProviders: cfg.OAuthProviders(),
|
|
},
|
|
}
|
|
}
|