Initial commit to Proxyfier
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
config.yaml
|
||||||
|
*.log
|
||||||
|
bin/
|
||||||
|
dist/
|
||||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"go.toolsManagement.autoUpdate": true,
|
||||||
|
"go.useLanguageServer": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"files.eol": "\n"
|
||||||
|
}
|
||||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -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"]
|
||||||
51
README.md
Normal file
51
README.md
Normal file
@@ -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 или внутри сервиса.
|
||||||
13
config.yaml.example
Normal file
13
config.yaml.example
Normal file
@@ -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"
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module proxyfier
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
3
go.sum
Normal file
3
go.sum
Normal file
@@ -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=
|
||||||
132
main.go
Normal file
132
main.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
67
main_test.go
Normal file
67
main_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user