Enhance Dockerfile to build and include a new HTTP API service (mtproxy_checkerd) alongside the existing CLI tool (mtproxy_checker). Update README and documentation to reflect the new service and its usage, including environment variables and Docker run instructions.
This commit is contained in:
+4
-1
@@ -5,10 +5,13 @@ RUN apk add --no-cache ca-certificates git
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /mtproxy_checker ./cmd/mtproxy_checker
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /mtproxy_checker ./cmd/mtproxy_checker \
|
||||
&& CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /mtproxy_checkerd ./cmd/mtproxy_checkerd
|
||||
|
||||
# Runtime
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache ca-certificates
|
||||
COPY --from=build /mtproxy_checker /usr/local/bin/mtproxy_checker
|
||||
COPY --from=build /mtproxy_checkerd /usr/local/bin/mtproxy_checkerd
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/usr/local/bin/mtproxy_checker"]
|
||||
|
||||
@@ -24,7 +24,7 @@ go build -o mtproxy_checker.exe ./cmd/mtproxy_checker
|
||||
|
||||
Код выхода: `0` — OK, `1` — ошибка, `2` — неверные аргументы, `3` — прокси закрыл соединение после проверки, `4` — таймаут.
|
||||
|
||||
**Docker (описание образа, Linux, несколько прокси подряд):** [docs/docker.ru.md](docs/docker.ru.md).
|
||||
**Docker:** [docs/docker.ru.md](docs/docker.ru.md) — образ CLI, несколько прокси из файла, а также контейнер с **HTTP API** (`mtproxy_checkerd`, файл со списком `tg://`, интервал проверки, опциональный whitelist IP).
|
||||
|
||||
### Если в Telegram прокси «Available», а утилита падает на `read server hello`
|
||||
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"mtproxy_checker/internal/allowlist"
|
||||
"mtproxy_checker/internal/checker"
|
||||
"mtproxy_checker/internal/checkresult"
|
||||
"mtproxy_checker/internal/parseurl"
|
||||
"mtproxy_checker/internal/secret"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.LUTC)
|
||||
if err := run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type config struct {
|
||||
listFile string
|
||||
checkInterval time.Duration
|
||||
httpAddr string
|
||||
checkTimeout time.Duration
|
||||
dcID int16
|
||||
allowedPrefixes []netip.Prefix
|
||||
}
|
||||
|
||||
func loadConfig() (*config, error) {
|
||||
listFile := strings.TrimSpace(os.Getenv("MTPROXY_LIST_FILE"))
|
||||
if listFile == "" {
|
||||
listFile = "/data/proxies.txt"
|
||||
}
|
||||
intervalStr := strings.TrimSpace(os.Getenv("MTPROXY_CHECK_INTERVAL"))
|
||||
if intervalStr == "" {
|
||||
intervalStr = "5m"
|
||||
}
|
||||
interval, err := time.ParseDuration(intervalStr)
|
||||
if err != nil || interval <= 0 {
|
||||
return nil, fmt.Errorf("MTPROXY_CHECK_INTERVAL: invalid duration %q", intervalStr)
|
||||
}
|
||||
httpAddr := strings.TrimSpace(os.Getenv("MTPROXY_HTTP_ADDR"))
|
||||
if httpAddr == "" {
|
||||
httpAddr = ":8080"
|
||||
}
|
||||
timeoutStr := strings.TrimSpace(os.Getenv("MTPROXY_CHECK_TIMEOUT"))
|
||||
if timeoutStr == "" {
|
||||
timeoutStr = "15s"
|
||||
}
|
||||
checkTimeout, err := time.ParseDuration(timeoutStr)
|
||||
if err != nil || checkTimeout <= 0 {
|
||||
return nil, fmt.Errorf("MTPROXY_CHECK_TIMEOUT: invalid duration %q", timeoutStr)
|
||||
}
|
||||
dcStr := strings.TrimSpace(os.Getenv("MTPROXY_DC_ID"))
|
||||
if dcStr == "" {
|
||||
dcStr = "2"
|
||||
}
|
||||
var dcParsed int64
|
||||
_, err = fmt.Sscanf(dcStr, "%d", &dcParsed)
|
||||
if err != nil || dcParsed < -32768 || dcParsed > 32767 {
|
||||
return nil, fmt.Errorf("MTPROXY_DC_ID: invalid int16 %q", dcStr)
|
||||
}
|
||||
allowedRaw := os.Getenv("MTPROXY_ALLOWED_IPS")
|
||||
prefixes, err := allowlist.ParseCommaList(allowedRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config{
|
||||
listFile: listFile,
|
||||
checkInterval: interval,
|
||||
httpAddr: httpAddr,
|
||||
checkTimeout: checkTimeout,
|
||||
dcID: int16(dcParsed),
|
||||
allowedPrefixes: prefixes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type proxyEntry struct {
|
||||
RawLine string `json:"raw_line"`
|
||||
URL string `json:"url,omitempty"`
|
||||
OK bool `json:"ok"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ParseError string `json:"parse_error,omitempty"`
|
||||
CheckedAt string `json:"checked_at,omitempty"`
|
||||
}
|
||||
|
||||
type snapshot struct {
|
||||
CycleFinishedAt string `json:"cycle_finished_at"`
|
||||
NextCheckAfter string `json:"next_check_after,omitempty"`
|
||||
Proxies []proxyEntry `json:"proxies"`
|
||||
}
|
||||
|
||||
type store struct {
|
||||
mu sync.RWMutex
|
||||
data snapshot
|
||||
}
|
||||
|
||||
func (s *store) get() snapshot {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := s.data
|
||||
out.Proxies = append([]proxyEntry(nil), s.data.Proxies...)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *store) setCycle(entries []proxyEntry, finished time.Time, next time.Time) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.data.Proxies = append([]proxyEntry(nil), entries...)
|
||||
s.data.CycleFinishedAt = finished.UTC().Format(time.RFC3339)
|
||||
if !next.IsZero() {
|
||||
s.data.NextCheckAfter = next.UTC().Format(time.RFC3339)
|
||||
} else {
|
||||
s.data.NextCheckAfter = ""
|
||||
}
|
||||
}
|
||||
|
||||
func readProxyLines(path string) ([]string, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var lines []string
|
||||
for _, line := range strings.Split(string(b), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
func checkOneLine(ctx context.Context, line string, dcID int16) proxyEntry {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
ent := proxyEntry{RawLine: line, CheckedAt: now}
|
||||
t, err := parseurl.ParseTGProxy(line)
|
||||
if err != nil {
|
||||
ent.ExitCode = 2
|
||||
ent.ParseError = err.Error()
|
||||
ent.Error = ent.ParseError
|
||||
return ent
|
||||
}
|
||||
ent.URL = line
|
||||
parsed, err := secret.Parse(t.Secret)
|
||||
if err != nil {
|
||||
ent.ExitCode = 2
|
||||
ent.ParseError = err.Error()
|
||||
ent.Error = ent.ParseError
|
||||
return ent
|
||||
}
|
||||
err = checker.Check(ctx, t.Host, t.Port, parsed, dcID)
|
||||
code, msg := checkresult.Classify(err)
|
||||
ent.ExitCode = code
|
||||
ent.OK = err == nil
|
||||
if msg != "" {
|
||||
ent.Error = msg
|
||||
}
|
||||
return ent
|
||||
}
|
||||
|
||||
func runCycle(cfg *config, st *store) {
|
||||
lines, err := readProxyLines(cfg.listFile)
|
||||
if err != nil {
|
||||
finished := time.Now().UTC()
|
||||
st.setCycle([]proxyEntry{{
|
||||
RawLine: cfg.listFile,
|
||||
ExitCode: 2,
|
||||
ParseError: err.Error(),
|
||||
Error: err.Error(),
|
||||
CheckedAt: finished.Format(time.RFC3339),
|
||||
}}, finished, time.Now().Add(cfg.checkInterval))
|
||||
log.Printf("read list file: %v", err)
|
||||
return
|
||||
}
|
||||
entries := make([]proxyEntry, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.checkTimeout)
|
||||
ent := checkOneLine(ctx, line, cfg.dcID)
|
||||
cancel()
|
||||
entries = append(entries, ent)
|
||||
}
|
||||
finished := time.Now().UTC()
|
||||
next := time.Now().Add(cfg.checkInterval)
|
||||
st.setCycle(entries, finished, next)
|
||||
}
|
||||
|
||||
func whitelistMiddleware(prefixes []netip.Prefix, next http.Handler) http.Handler {
|
||||
if len(prefixes) == 0 {
|
||||
return next
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
host = r.RemoteAddr
|
||||
}
|
||||
if len(host) >= 2 && host[0] == '[' && host[len(host)-1] == ']' {
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
addr, err := netip.ParseAddr(host)
|
||||
if err != nil || !allowlist.Contains(prefixes, addr) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func run() error {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st := &store{}
|
||||
|
||||
go func() {
|
||||
runCycle(cfg, st)
|
||||
t := time.NewTicker(cfg.checkInterval)
|
||||
defer t.Stop()
|
||||
for range t.C {
|
||||
runCycle(cfg, st)
|
||||
}
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
mux.HandleFunc("/api/v1/proxies", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
snap := st.get()
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(snap); err != nil {
|
||||
log.Printf("encode json: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
handler := whitelistMiddleware(cfg.allowedPrefixes, mux)
|
||||
srv := &http.Server{
|
||||
Addr: cfg.httpAddr,
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
log.Printf("listening on %s, list=%s interval=%s", cfg.httpAddr, cfg.listFile, cfg.checkInterval)
|
||||
errCh <- srv.ListenAndServe()
|
||||
}()
|
||||
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
select {
|
||||
case <-sig:
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
return srv.Shutdown(ctx)
|
||||
case err := <-errCh:
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# HTTP API: список tg:// в файле, периодическая проверка, JSON по GET.
|
||||
# Запуск: docker compose -f docker-compose.api.yaml up --build
|
||||
services:
|
||||
mtproxy-checkerd:
|
||||
build: .
|
||||
image: mtproxy_checker:local
|
||||
entrypoint: ["/usr/local/bin/mtproxy_checkerd"]
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./proxies.txt:/data/proxies.txt:ro
|
||||
environment:
|
||||
MTPROXY_LIST_FILE: /data/proxies.txt
|
||||
MTPROXY_CHECK_INTERVAL: 5m
|
||||
MTPROXY_HTTP_ADDR: ":8080"
|
||||
MTPROXY_CHECK_TIMEOUT: 15s
|
||||
MTPROXY_DC_ID: "2"
|
||||
# Опционально: только перечисленные IP/CIDR (иначе не задавайте переменную)
|
||||
# MTPROXY_ALLOWED_IPS: "172.17.0.0/16,127.0.0.1"
|
||||
+54
-4
@@ -5,12 +5,62 @@
|
||||
| Компонент | Описание |
|
||||
|-----------|----------|
|
||||
| **База runtime** | Alpine Linux 3.20 |
|
||||
| **Бинарник** | Статическая сборка Go (`CGO_ENABLED=0`), путь в контейнере: `/usr/local/bin/mtproxy_checker` |
|
||||
| **Бинарники** | CLI: `/usr/local/bin/mtproxy_checker`. HTTP-сервис списка: `/usr/local/bin/mtproxy_checkerd` (см. ниже) |
|
||||
| **Сертификаты** | Пакет `ca-certificates` (для TLS к реальным хостам при необходимости) |
|
||||
| **ENTRYPOINT** | Тот же бинарник — все аргументы `docker run …` передаются в утилиту |
|
||||
| **Порты** | Ничего не слушает: только **исходящие** TCP до хоста и порта из ссылки |
|
||||
| **ENTRYPOINT** | По умолчанию **CLI** — аргументы `docker run …` идут в `mtproxy_checker` |
|
||||
| **Порты** | В режиме CLI ничего не слушает (только исходящий TCP). Образ объявляет **EXPOSE 8080** для режима API |
|
||||
|
||||
Одна команда `docker run` = **одна** проверка одного прокси; процесс завершается с **кодом выхода** (`0`…`4`), что удобно в CI и скриптах.
|
||||
Одна команда `docker run` с entrypoint по умолчанию = **одна** проверка одного прокси; процесс завершается с **кодом выхода** (`0`…`4`), что удобно в CI и скриптах.
|
||||
|
||||
## HTTP API: `mtproxy_checkerd`
|
||||
|
||||
Лёгкий сервер на стандартной библиотеке Go: читает файл со списком `tg://` (как в разделе «файл — одна ссылка на строку»), **сразу после старта** выполняет первый цикл проверок, затем повторяет с интервалом. Результаты последнего цикла отдаются по HTTP в JSON.
|
||||
|
||||
### Запуск контейнера (смена entrypoint)
|
||||
|
||||
**Linux / macOS:**
|
||||
|
||||
```bash
|
||||
docker run --rm -p 8080:8080 \
|
||||
-v /path/to/proxies.txt:/data/proxies.txt:ro \
|
||||
--entrypoint /usr/local/bin/mtproxy_checkerd \
|
||||
mtproxy_checker:local
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
docker run --rm -p 8080:8080 `
|
||||
-v "${PWD}\proxies.txt:/data/proxies.txt:ro" `
|
||||
--entrypoint /usr/local/bin/mtproxy_checkerd `
|
||||
mtproxy_checker:local
|
||||
```
|
||||
|
||||
Готовый пример с переменными окружения: репозиторий **`docker-compose.api.yaml`** — `docker compose -f docker-compose.api.yaml up --build`.
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
| Переменная | По умолчанию | Назначение |
|
||||
|------------|--------------|------------|
|
||||
| `MTPROXY_LIST_FILE` | `/data/proxies.txt` | Путь к файлу: одна `tg://` ссылка на строку; пустые строки и строки с `#` в начале пропускаются |
|
||||
| `MTPROXY_CHECK_INTERVAL` | `5m` | Интервал между циклами (`time.ParseDuration`, например `5m`, `1h`) |
|
||||
| `MTPROXY_HTTP_ADDR` | `:8080` | Адрес прослушивания HTTP |
|
||||
| `MTPROXY_CHECK_TIMEOUT` | `15s` | Таймаут одной проверки (аналог `-timeout` CLI) |
|
||||
| `MTPROXY_DC_ID` | `2` | DC id (аналог `-dc-id` CLI) |
|
||||
| `MTPROXY_ALLOWED_IPS` | *(не задана)* | Если задана непустая строка — доступ к **всем** маршрутам только с перечисленных IP/CIDR; остальные получают **403** и JSON `{"error":"forbidden"}`. Формат: через запятую, пробелы допускаются: `192.168.1.10`, `10.0.0.0/8`, IPv6 и CIDR вида `2001:db8::/32`. Учитывается только **`RemoteAddr`** TCP-соединения; заголовок `X-Forwarded-For` **не** используется |
|
||||
|
||||
### HTTP
|
||||
|
||||
| Метод | Путь | Ответ |
|
||||
|--------|------|--------|
|
||||
| GET | `/health` | `200`, `{"status":"ok"}` |
|
||||
| GET | `/api/v1/proxies` | `200`, JSON с полями `cycle_finished_at`, `next_check_after`, массив `proxies` |
|
||||
|
||||
Элемент `proxies[]`: `raw_line`, при успешном разборе — `url`, `ok`, `exit_code` (`0` OK, `1` ошибка проверки, `2` ошибка разбора URL/секрета, `3` прокси закрыл соединение, `4` таймаут), `error`, при необходимости `parse_error`, `checked_at`.
|
||||
|
||||
### Whitelist IP и Docker
|
||||
|
||||
Процесс видит **IP источника TCP** таким, каким его передал стек в контейнер. При пробросе порта с хоста (`-p 8080:8080`) на Linux часто это адрес **шлюза bridge** или **userland-proxy**, а не «настоящий» IP клиента с хоста. Имеет смысл задавать **CIDR подсети Docker** (например `172.17.0.0/16`), слушать только внутреннюю сеть, использовать **`--network host`** (на Linux) или ограничивать доступ на **обратном прокси** (nginx `allow` и т.п.).
|
||||
|
||||
## Требования
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package allowlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseCommaList parses MTPROXY_ALLOWED_IPS: comma-separated IPv4/IPv6 addresses or CIDR prefixes.
|
||||
// Empty or whitespace-only input returns (nil, nil) meaning no restriction.
|
||||
func ParseCommaList(s string) ([]netip.Prefix, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var out []netip.Prefix
|
||||
for _, part := range strings.Split(s, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
pfx, err := parseEntry(part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("allowed_ips entry %q: %w", part, err)
|
||||
}
|
||||
out = append(out, pfx)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseEntry(s string) (netip.Prefix, error) {
|
||||
if strings.Contains(s, "/") {
|
||||
return netip.ParsePrefix(s)
|
||||
}
|
||||
addr, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
bits := 32
|
||||
if addr.Is6() {
|
||||
bits = 128
|
||||
}
|
||||
return addr.Prefix(bits)
|
||||
}
|
||||
|
||||
// Contains reports whether addr matches any prefix in list. Empty list means allow all.
|
||||
func Contains(list []netip.Prefix, addr netip.Addr) bool {
|
||||
if len(list) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, p := range list {
|
||||
if p.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package allowlist
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCommaList(t *testing.T) {
|
||||
list, err := ParseCommaList(" ")
|
||||
if err != nil || list != nil {
|
||||
t.Fatalf("empty: list=%v err=%v", list, err)
|
||||
}
|
||||
list, err = ParseCommaList("192.168.1.1, 10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(list) != 2 {
|
||||
t.Fatalf("want 2 prefixes, got %d", len(list))
|
||||
}
|
||||
a := netip.MustParseAddr("10.5.5.5")
|
||||
if !Contains(list, a) {
|
||||
t.Fatal("10.5.5.5 should match 10.0.0.0/8")
|
||||
}
|
||||
b := netip.MustParseAddr("192.168.1.1")
|
||||
if !Contains(list, b) {
|
||||
t.Fatal("192.168.1.1 should match host /32")
|
||||
}
|
||||
c := netip.MustParseAddr("8.8.8.8")
|
||||
if Contains(list, c) {
|
||||
t.Fatal("8.8.8.8 should not match")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package checkresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"mtproxy_checker/internal/checker"
|
||||
)
|
||||
|
||||
// Classify maps a checker error to CLI-style exit codes: 0 OK, 1 generic, 3 proxy closed, 4 timeout.
|
||||
func Classify(err error) (exitCode int, message string) {
|
||||
if err == nil {
|
||||
return 0, ""
|
||||
}
|
||||
if errors.Is(err, checker.ErrProxyClosed) {
|
||||
return 3, err.Error()
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return 4, "timeout"
|
||||
}
|
||||
return 1, err.Error()
|
||||
}
|
||||
Reference in New Issue
Block a user