initial commit

This commit is contained in:
2026-02-27 13:23:16 +03:00
commit 3701af1fd7
12 changed files with 512 additions and 0 deletions

59
app/api.go Normal file
View File

@@ -0,0 +1,59 @@
package app
import (
"encoding/json"
"log"
"net/http"
)
type Error struct {
Ok bool `json:"ok"`
Error string `json:"error"`
}
func WriteError(w http.ResponseWriter, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
e := json.NewEncoder(w).Encode(Error{
Ok: false,
Error: err.Error(),
})
if e != nil {
log.Println(e)
}
}
type Response[T any] struct {
Ok bool `json:"ok"`
Data T `json:"data"`
}
func WriteResponse[T any](w http.ResponseWriter, data T) {
WriteResponseCode(w, data, 200)
}
func WriteResponseCode[T any](w http.ResponseWriter, data T, code int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
e := enc.Encode(Response[T]{
Ok: true,
Data: data,
})
if e != nil {
log.Println(e)
}
}
func WriteResponseOrError[T any](w http.ResponseWriter, data T, err error) {
if err != nil {
WriteError(w, err)
} else {
WriteResponse(w, data)
}
}
func ReadBody[T any](r *http.Request) (T, error) {
dst := new(T)
err := json.NewDecoder(r.Body).Decode(dst)
return *dst, err
}

96
app/provider.go Normal file
View File

@@ -0,0 +1,96 @@
package app
import (
"encoding/json"
"errors"
"os"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
type Provider struct {
Users []User `json:"users"`
LastUserID int `json:"last_user_id"`
path string
}
// NewProvider создаёт новый провайдер с пустыми данными (файл не трогает)
func NewProvider() *Provider {
return &Provider{
Users: make([]User, 0),
LastUserID: 0,
path: "./provider.json",
}
}
// LoadProvider загружает данные из файла, если файла нет — возвращает новый провайдер
func LoadProvider() (*Provider, error) {
data, err := os.ReadFile("./provider.json")
if err != nil {
if os.IsNotExist(err) {
// файл не существует, возвращаем новый провайдер
return NewProvider(), nil
}
return nil, err
}
p := &Provider{path: "./provider.json"}
if err := json.Unmarshal(data, p); err != nil {
return nil, err
}
return p, nil
}
// Save сохраняет текущее состояние в файл
func (p *Provider) Save() error {
data, err := json.MarshalIndent(p, "", " ") // для читаемости
if err != nil {
return err
}
return os.WriteFile(p.path, data, 0644)
}
func (p *Provider) AddUser(username, password string) error {
p.LastUserID++
p.Users = append(p.Users, User{
ID: p.LastUserID,
Username: username,
Password: password,
})
return p.Save()
}
func (p *Provider) GetById(id int) (User, error) {
for _, user := range p.Users {
if user.ID == id {
return user, nil
}
}
return User{}, errors.New("user not found")
}
func (p *Provider) GetUser(password string) (User, error) {
for _, user := range p.Users {
if user.Password == password {
return user, nil
}
}
return User{}, errors.New("user not found")
}
func (p *Provider) DeleteUser(id int) error {
index := -1
for i, user := range p.Users {
if user.ID == id {
index = i
break
}
}
if index == -1 {
return errors.New("user not found")
}
p.Users = append(p.Users[:index], p.Users[index+1:]...)
return p.Save()
}

199
app/routes.go Normal file
View File

@@ -0,0 +1,199 @@
package app
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
)
type AddUserReq struct {
Token string `json:"token"`
Username string `json:"username"`
Password string `json:"password"`
}
type AddUserRsp struct {
ID int `json:"id"`
Username string `json:"username"`
}
func AddUser(w http.ResponseWriter, r *http.Request) {
log.Println("AddUser called")
req, err := ReadBody[AddUserReq](r)
if err != nil {
WriteError(w, err)
return
}
if req.Token != cfg.JWTSecret {
WriteError(w, errors.New("token required"))
return
}
provider, err := LoadProvider()
if err != nil {
WriteError(w, err)
return
}
err = provider.AddUser(req.Username, req.Password)
if err != nil {
WriteError(w, err)
return
}
err = provider.Save()
if err != nil {
WriteError(w, err)
}
user, err := provider.GetUser(req.Password)
if err != nil {
WriteError(w, err)
return
}
WriteResponseCode(w, user, http.StatusCreated)
}
type DeleteUserReq struct {
Token string `json:"token"`
ID int `json:"id"`
}
func DeleteUser(w http.ResponseWriter, r *http.Request) {
req, err := ReadBody[DeleteUserReq](r)
if err != nil {
WriteError(w, err)
return
}
if req.Token != cfg.JWTSecret {
WriteError(w, errors.New("invalid token"))
return
}
provider, err := LoadProvider()
if err != nil {
WriteError(w, err)
return
}
err = provider.DeleteUser(req.ID)
if err != nil {
WriteError(w, err)
return
}
err = provider.Save()
if err != nil {
WriteError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
type AllUserReq struct {
Token string `json:"token"`
}
func AllUsers(w http.ResponseWriter, r *http.Request) {
fmt.Println("AllUsers called")
req, err := ReadBody[AllUserReq](r)
if err != nil {
WriteError(w, err)
return
}
if req.Token != cfg.JWTSecret {
WriteError(w, errors.New("invalid token"))
return
}
provider, err := LoadProvider()
if err != nil {
WriteError(w, err)
return
}
WriteResponse(w, provider)
}
type GetConnectURLReq struct {
ID int `json:"id"`
Pass string `json:"pass"`
}
func GetUserURL(w http.ResponseWriter, r *http.Request) {
vars := r.URL.Query()
idS := vars.Get("id")
id, err := strconv.Atoi(idS)
if err != nil {
WriteError(w, err)
return
}
provider, err := LoadProvider()
if err != nil {
WriteError(w, err)
return
}
user, err := provider.GetById(id)
if err != nil {
WriteError(w, err)
return
}
urlTemplate := "hysteria2://%s@%s:%s?obfs=salamander&obfs-password=%s&type=hysteria&mport&security=tls&sni=%s&alpn=h3&fp=chrome&allowInsecure=0#%s"
authString := encodeURL(user)
u := fmt.Sprintf(urlTemplate, authString, cfg.Host, cfg.Port, cfg.ObfsPassword, cfg.SNI, formatConfigName(cfg.NameFormat, user))
WriteResponse(w, u)
}
type AuthReq struct {
Addr string `json:"addr"`
Auth string `json:"auth"`
Tx int `json:"tx"`
}
type AuthRsp struct {
Ok bool `json:"ok"`
ID string `json:"id"`
}
func DoAuth(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Println(err)
return
}
req := new(AuthReq)
if err := json.Unmarshal(data, req); err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Println(err)
return
}
log.Printf("New auth request from %s, data %s", req.Addr, string(data))
reqUser, err := decodeURL(req.Auth)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
provider, err := LoadProvider()
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
user, err := provider.GetUser(reqUser.Password)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
res := &AuthRsp{true, user.Username}
err = json.NewEncoder(w).Encode(res)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
} else {
w.WriteHeader(http.StatusOK)
}
}

91
app/utils.go Normal file
View File

@@ -0,0 +1,91 @@
package app
import (
"encoding/base64"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"github.com/golang-jwt/jwt/v5"
)
func encodeURL(user User) string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": user.ID, "name": user.Username, "pass": user.Password,
})
tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
return ""
}
tokenString = base64.RawURLEncoding.EncodeToString([]byte(tokenString))
return tokenString
}
func decodeURL(tokenString string) (User, error) {
var zero User
tokenBytes, err := base64.RawURLEncoding.DecodeString(tokenString)
if err != nil {
return zero, err
}
token, err := jwt.Parse(string(tokenBytes), func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil {
return zero, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
id := int(claims["id"].(float64))
name := claims["name"].(string)
pass := claims["pass"].(string)
return User{ID: id, Username: name, Password: pass}, nil
}
return zero, nil
}
type Config struct {
JWTSecret string
Host string
Port string
ObfsPassword string
SNI string
NameFormat string
}
var cfg *Config
func LoadConfig() {
cfg = &Config{
JWTSecret: os.Getenv("JWT_SECRET"),
Host: os.Getenv("HOST"),
Port: os.Getenv("PORT"),
ObfsPassword: os.Getenv("OBFS_PASSWORD"),
SNI: os.Getenv("SNI"),
NameFormat: os.Getenv("NAME_FORMAT"),
}
}
var ip string
func LoadIP() {
res, err := http.Get("https://api.ipify.org")
if err != nil {
panic(err)
}
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
ip = string(data)
log.Println(ip)
}
func formatConfigName(format string, user User) string {
s := strings.ReplaceAll(format, "{username}", user.Username)
s = strings.ReplaceAll(s, "{host}", ip)
return s
}