From 3f80dab132fb3af4aaabdfc144b51668a64562c8 Mon Sep 17 00:00:00 2001 From: Alexander Sergeevich <7339834@gmail.com> Date: Sat, 7 Feb 2026 01:22:35 +0300 Subject: [PATCH] Initial commit to Proxyfier --- .gitignore | 5 ++ .vscode/launch.json | 15 +++++ .vscode/settings.json | 6 ++ Dockerfile | 13 +++++ README.md | 51 ++++++++++++++++ config.yaml.example | 13 +++++ go.mod | 5 ++ go.sum | 3 + main.go | 132 ++++++++++++++++++++++++++++++++++++++++++ main_test.go | 67 +++++++++++++++++++++ 10 files changed, 310 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 config.yaml.example create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 main_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d396fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +config.yaml +*.log +bin/ +dist/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9bebe55 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "proxyfier (config.yaml)", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "env": { + "PROXYFIER_CONFIG": "${workspaceFolder}/config.yaml" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f4d59fc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "go.toolsManagement.autoUpdate": true, + "go.useLanguageServer": true, + "editor.formatOnSave": true, + "files.eol": "\n" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..456ecc4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.22-alpine AS build +WORKDIR /app +COPY go.mod ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/proxyfier ./main.go + +FROM alpine:3.20 +WORKDIR /app +COPY --from=build /out/proxyfier /usr/local/bin/proxyfier +COPY config.yaml.example /app/config.yaml.example +EXPOSE 9000 +CMD ["/usr/local/bin/proxyfier"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8f08fb --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# proxyfier + +Мини‑сервис для выдачи прокси‑кредов по запросу. + +## Идея +- Аутентификация выполняется **в самом Go‑сервисе** (Basic Auth). +- В конфиге храним пары «имя сервиса → логин/пароль». +- По запросу `/creds?service=telegram` возвращаем креды JSON‑ом. + +## Быстрый старт (локально) +1) Скопировать конфиг: +``` +cp config.yaml.example config.yaml +``` +2) Запустить: +``` +go run . +``` +3) Проверка: +``` +curl -u "admin:change-me" "http://localhost:9000/creds?service=telegram" +``` + +## Настройка (config.yaml) +``` +listen: "0.0.0.0:9000" +auth: + user: "admin" + pass: "change-me" +credentials: + telegram: + username: "tg-user" + password: "tg-pass" + note: "proxy for telegram" +``` + +## Docker +``` +docker build -t proxyfier:latest . +docker run --rm -p 9000:9000 \ + -v "$PWD/config.yaml:/app/config.yaml:ro" \ + -e PROXYFIER_CONFIG=/app/config.yaml \ + proxyfier:latest +``` + +## VSCode +Файлы уже в `.vscode/`. Для запуска нужен `config.yaml`. + +## Дальше +- Добавить раздел «деплой на my‑vpn». +- Решить, оставляем ли HTTPS на стороне Nginx или внутри сервиса. diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..153c486 --- /dev/null +++ b/config.yaml.example @@ -0,0 +1,13 @@ +listen: "0.0.0.0:9000" +auth: + user: "admin" + pass: "change-me" +credentials: + telegram: + username: "tg-user" + password: "tg-pass" + note: "proxy for telegram" + jellyfin: + username: "jf-user" + password: "jf-pass" + note: "tmdb proxy" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e8da797 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module proxyfier + +go 1.22 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4bc0337 --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1902ff9 --- /dev/null +++ b/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "encoding/json" + "errors" + + // "fmt" + "log" + "net/http" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Listen string `yaml:"listen"` + Auth AuthConfig `yaml:"auth"` + Credentials map[string]Credential `yaml:"credentials"` +} + +type AuthConfig struct { + User string `yaml:"user"` + Pass string `yaml:"pass"` +} + +type Credential struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + Note string `yaml:"note"` +} + +type Response struct { + Service string `json:"service"` + Username string `json:"username"` + Password string `json:"password"` + Note string `json:"note,omitempty"` + IssuedAt string `json:"issued_at"` +} + +func basicAuthOK(r *http.Request, cfg *Config) bool { + user, pass, ok := r.BasicAuth() + if !ok { + return false + } + return user == cfg.Auth.User && pass == cfg.Auth.Pass +} + +func newMux(cfg *Config) *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) + + mux.HandleFunc("/creds", func(w http.ResponseWriter, r *http.Request) { + if !basicAuthOK(r, cfg) { + w.Header().Set("WWW-Authenticate", `Basic realm="proxyfier"`) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + service := r.URL.Query().Get("service") + if service == "" { + http.Error(w, "service is required", http.StatusBadRequest) + return + } + + cred, ok := cfg.Credentials[service] + if !ok { + http.Error(w, "service not found", http.StatusNotFound) + return + } + + resp := Response{ + Service: service, + Username: cred.Username, + Password: cred.Password, + Note: cred.Note, + IssuedAt: time.Now().UTC().Format(time.RFC3339), + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(resp); err != nil { + http.Error(w, "encode error", http.StatusInternalServerError) + return + } + }) + + return mux +} + +func loadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + if cfg.Listen == "" { + cfg.Listen = "0.0.0.0:9000" + } + if cfg.Auth.User == "" || cfg.Auth.Pass == "" { + return nil, errors.New("auth.user/auth.pass must be set") + } + return &cfg, nil +} + +func main() { + cfgPath := os.Getenv("PROXYFIER_CONFIG") + if cfgPath == "" { + cfgPath = "config.yaml" + } + + cfg, err := loadConfig(cfgPath) + if err != nil { + log.Fatalf("config error: %v", err) + } + + srv := &http.Server{ + Addr: cfg.Listen, + Handler: newMux(cfg), + ReadHeaderTimeout: 5 * time.Second, + } + + log.Printf("proxyfier listening on %s", cfg.Listen) + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("server error: %v", err) + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..471a089 --- /dev/null +++ b/main_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func testConfig() *Config { + return &Config{ + Listen: "127.0.0.1:0", + Auth: AuthConfig{ + User: "user", + Pass: "pass", + }, + Credentials: map[string]Credential{ + "telegram": { + Username: "tg-user", + Password: "tg-pass", + Note: "note", + }, + }, + } +} + +func TestHealthOK(t *testing.T) { + mux := newMux(testConfig()) + req := httptest.NewRequest(http.MethodGet, "/health", nil) + rr := httptest.NewRecorder() + mux.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } +} + +func TestCredsUnauthorized(t *testing.T) { + mux := newMux(testConfig()) + req := httptest.NewRequest(http.MethodGet, "/creds?service=telegram", nil) + rr := httptest.NewRecorder() + mux.ServeHTTP(rr, req) + + if rr.Code != http.StatusUnauthorized { + t.Fatalf("expected 401, got %d", rr.Code) + } +} + +func TestCredsSuccess(t *testing.T) { + mux := newMux(testConfig()) + req := httptest.NewRequest(http.MethodGet, "/creds?service=telegram", nil) + req.SetBasicAuth("user", "pass") + rr := httptest.NewRecorder() + mux.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var resp Response + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatalf("invalid json: %v", err) + } + if resp.Service != "telegram" || resp.Username != "tg-user" || resp.Password != "tg-pass" { + t.Fatalf("unexpected response: %+v", resp) + } +}