initial commit
This commit is contained in:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.idea/
|
||||||
|
.env*
|
||||||
|
*.json
|
||||||
|
Makefile
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
6
.env
Normal file
6
.env
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
JWT_SECRET=CHANGEME
|
||||||
|
HOST=127.0.0.1
|
||||||
|
PORT=443
|
||||||
|
OBFS_PASSWORD=test
|
||||||
|
SNI=vk.com
|
||||||
|
NAME_FORMAT={username}
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.idea/
|
||||||
|
provider.json
|
||||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM golang:1.26-alpine AS builder
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY ./app ./app/
|
||||||
|
COPY main.go .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg/mod go build -v -o api .
|
||||||
|
|
||||||
|
FROM alpine:3.23 AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /src/api /app/api
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["/app/api"]
|
||||||
3
Makefile
Normal file
3
Makefile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
build:
|
||||||
|
docker build -t git.nix13.pw/scuroneko/h2auth:latest .
|
||||||
|
docker push git.nix13.pw/scuroneko/h2auth:latest
|
||||||
59
app/api.go
Normal file
59
app/api.go
Normal 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
96
app/provider.go
Normal 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
199
app/routes.go
Normal 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
91
app/utils.go
Normal 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
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module Hyst2Auth
|
||||||
|
|
||||||
|
go 1.26
|
||||||
|
|
||||||
|
require github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
30
main.go
Normal file
30
main.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Hyst2Auth/app"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app.LoadIP()
|
||||||
|
app.LoadConfig()
|
||||||
|
_, err := app.LoadProvider()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := http.NewServeMux()
|
||||||
|
r.HandleFunc("/add", app.AddUser)
|
||||||
|
r.HandleFunc("/delete", app.DeleteUser)
|
||||||
|
r.HandleFunc("/users", app.AllUsers)
|
||||||
|
|
||||||
|
r.HandleFunc("/connect_url", app.GetUserURL)
|
||||||
|
|
||||||
|
r.HandleFunc("/auth", app.DoAuth)
|
||||||
|
|
||||||
|
log.Println("Listening on :8080")
|
||||||
|
if err := http.ListenAndServe(":8080", r); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user