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