initial commit
This commit is contained in:
5
.env
Normal file
5
.env
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
PSQL_HOST=127.0.0.1
|
||||||
|
PSQL_NAME=hyst2
|
||||||
|
PSQL_USER=hyst2
|
||||||
|
PSQL_PASS=123456
|
||||||
|
JWT_SECRET=CHANGEME
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.idea/
|
||||||
|
provider.json
|
||||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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"]
|
||||||
|
USER nobody
|
||||||
3
Makefile
Normal file
3
Makefile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
build:
|
||||||
|
docker build -t git.nix13.pw/scuroneko/h2master:latest .
|
||||||
|
docker push git.nix13.pw/scuroneko/h2master:latest
|
||||||
79
app/api.go
Normal file
79
app/api.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"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 io.ReadCloser) (T, error) {
|
||||||
|
dst := new(T)
|
||||||
|
data, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return *dst, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, dst)
|
||||||
|
return *dst, err
|
||||||
|
}
|
||||||
|
func ReadResponse[T any](r io.ReadCloser) (T, error) {
|
||||||
|
var zero T
|
||||||
|
resp, err := ReadBody[Response[T]](r)
|
||||||
|
if err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
return resp.Data, nil
|
||||||
|
}
|
||||||
|
func CheckToken(r *http.Request) bool {
|
||||||
|
auth := r.Header.Get("Authorization")
|
||||||
|
if auth != cfg.JWTSecret {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
13
app/config.go
Normal file
13
app/config.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
JWTSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg *Config
|
||||||
|
|
||||||
|
func LoadConfig() {
|
||||||
|
cfg = &Config{JWTSecret: os.Getenv("JWT_SECRET")}
|
||||||
|
}
|
||||||
44
app/database.go
Normal file
44
app/database.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"github.com/vinovest/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var db *sqlx.DB = nil
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Println("Error loading .env file. If you use docker, then you can ignore this.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if db == nil {
|
||||||
|
connectPsql()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDSN() string {
|
||||||
|
user := os.Getenv("PSQL_USER")
|
||||||
|
password := os.Getenv("PSQL_PASS")
|
||||||
|
database := os.Getenv("PSQL_NAME")
|
||||||
|
host, exists := os.LookupEnv("PSQL_HOST")
|
||||||
|
if !exists {
|
||||||
|
host = "localhost"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("postgresql://%s:%s@%s/%s?sslmode=disable", user, password, host, database)
|
||||||
|
}
|
||||||
|
func connectPsql() {
|
||||||
|
var err error
|
||||||
|
db, err = sqlx.Connect("postgres", getDSN())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func ClosePsql() error {
|
||||||
|
return db.Close()
|
||||||
|
}
|
||||||
81
app/manager.go
Normal file
81
app/manager.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NodeManager struct {
|
||||||
|
client *http.Client
|
||||||
|
token string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNodeManager(node Node) NodeManager {
|
||||||
|
client := &http.Client{Timeout: time.Second * 15}
|
||||||
|
return NodeManager{
|
||||||
|
client, node.Token, node.URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *NodeManager) NewRequest(method, path string, body io.Reader) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", m.url, path), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", m.token)
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
func (m *NodeManager) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
return m.client.Do(req)
|
||||||
|
}
|
||||||
|
func (m *NodeManager) Request(method, path string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := m.NewRequest(method, path, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m.Do(req)
|
||||||
|
}
|
||||||
|
func (m *NodeManager) LoadConfig() (NodeConfig, HysteriaConfig, error) {
|
||||||
|
var _cfg NodeConfig
|
||||||
|
var hysteriaCfg HysteriaConfig
|
||||||
|
|
||||||
|
resp, err := m.Request("GET", "/config", nil)
|
||||||
|
if err != nil {
|
||||||
|
return _cfg, hysteriaCfg, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
data, err := ReadResponse[NodeConfigRsp](resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return _cfg, hysteriaCfg, err
|
||||||
|
}
|
||||||
|
return data.Config, data.HysteriaConfig, nil
|
||||||
|
}
|
||||||
|
func (m *NodeManager) AddUser(username, password string) (User, error) {
|
||||||
|
body := strings.NewReader(fmt.Sprintf(`{"username":"%s","password":"%s"}`, username, password))
|
||||||
|
resp, err := m.Request("POST", "/users", body)
|
||||||
|
if err != nil {
|
||||||
|
return User{}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return ReadResponse[User](resp.Body)
|
||||||
|
}
|
||||||
|
func (m *NodeManager) GetUser(id uint64) (User, error) {
|
||||||
|
var user User
|
||||||
|
resp, err := m.Request("GET", fmt.Sprintf("/users/%d", id), nil)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return ReadResponse[User](resp.Body)
|
||||||
|
}
|
||||||
|
func (m *NodeManager) GetUserKey(id uint64) (string, error) {
|
||||||
|
resp, err := m.Request("GET", fmt.Sprintf("/users/%d/key", id), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return ReadResponse[string](resp.Body)
|
||||||
|
}
|
||||||
32
app/nodes.go
Normal file
32
app/nodes.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vinovest/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NodeRepository struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNodeRepository(db *sqlx.DB) *NodeRepository { return &NodeRepository{db} }
|
||||||
|
|
||||||
|
func (r *NodeRepository) All() ([]Node, error) {
|
||||||
|
nodes := make([]Node, 0)
|
||||||
|
sql := "SELECT * FROM nodes;"
|
||||||
|
err := r.db.Select(&nodes, sql)
|
||||||
|
return nodes, err
|
||||||
|
}
|
||||||
|
func (r *NodeRepository) Find(id int) (Node, error) {
|
||||||
|
var node Node
|
||||||
|
sql := "SELECT * FROM nodes WHERE id=?;"
|
||||||
|
sql = r.db.Rebind(sql)
|
||||||
|
err := r.db.Select(&node, sql, id)
|
||||||
|
return node, err
|
||||||
|
}
|
||||||
|
func (r *NodeRepository) Create(token, url string) (Node, error) {
|
||||||
|
sql := "INSERT INTO nodes (token, url) VALUES (?, ?) RETURNING *;"
|
||||||
|
sql = r.db.Rebind(sql)
|
||||||
|
var node Node
|
||||||
|
err := r.db.Get(&node, sql, token, url)
|
||||||
|
return node, err
|
||||||
|
}
|
||||||
125
app/routes.go
Normal file
125
app/routes.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hosts = []string{
|
||||||
|
"185.231.245.25",
|
||||||
|
"46.243.6.125",
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddNode(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type AddNodeReq struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
req, err := ReadBody[AddNodeReq](r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
rep := NewNodeRepository(db)
|
||||||
|
node, err := rep.Create(req.Token, req.URL)
|
||||||
|
WriteResponseOrError(w, node, err)
|
||||||
|
}
|
||||||
|
func GetNodes(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rep := NewNodeRepository(db)
|
||||||
|
nodes, err := rep.All()
|
||||||
|
WriteResponseOrError(w, nodes, err)
|
||||||
|
}
|
||||||
|
func GetNodeById(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
nodeId, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rep := NewNodeRepository(db)
|
||||||
|
node, err := rep.Find(nodeId)
|
||||||
|
WriteResponseOrError(w, node, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rep := NewNodeRepository(db)
|
||||||
|
nodes, err := rep.All()
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddUserReq struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
req, err := ReadBody[AddUserReq](r.Body)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
for _, node := range nodes {
|
||||||
|
manager := NewNodeManager(node)
|
||||||
|
user, err = manager.AddUser(req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WriteResponseOrError(w, user, err)
|
||||||
|
}
|
||||||
|
func Sub(w http.ResponseWriter, r *http.Request) {
|
||||||
|
idS := chi.URLParam(r, "id")
|
||||||
|
userId, err := strconv.ParseUint(idS, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rep := NewNodeRepository(db)
|
||||||
|
nodes, err := rep.All()
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subs := make([]string, 0)
|
||||||
|
for _, node := range nodes {
|
||||||
|
manager := NewNodeManager(node)
|
||||||
|
conf, hystConf, err := manager.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, err := manager.GetUserKey(userId)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := manager.GetUser(userId)
|
||||||
|
if err != nil {
|
||||||
|
WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hystUrl := buildSubUrl(conf, hystConf, key, user)
|
||||||
|
subs = append(subs, hystUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := []byte(base64.StdEncoding.EncodeToString([]byte(strings.Join(subs, "\n"))))
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(sub)))
|
||||||
|
w.Header().Set("Content-Disposition", `attachment; filename="ScuroNeko"`)
|
||||||
|
w.Header().Set("profile-web-page-url", "http://185.231.245.25:9997/sub/1")
|
||||||
|
w.Header().Set("support-url", "https://t.me/scuroneko")
|
||||||
|
w.Header().Set("profile-title", "base64:YmFuLm5peDEzLnB3")
|
||||||
|
w.Header().Set("profile-update-interval", "12")
|
||||||
|
w.Header().Set("subscription-userinfo", "upload=0; download=197782510450; total=0; expire=0")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write(sub)
|
||||||
|
}
|
||||||
133
app/types.go
Normal file
133
app/types.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeConfig struct {
|
||||||
|
JWTSecret string
|
||||||
|
Host string
|
||||||
|
SNI string
|
||||||
|
NameFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaTLSConfig struct {
|
||||||
|
Cert string `yaml:"cert"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaObfsConfig struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Salamander struct {
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
} `yaml:"salamander"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaQuicConfig struct {
|
||||||
|
InitStreamReceiveWindow *int64 `yaml:"initStreamReceiveWindow"`
|
||||||
|
MaxStreamReceiveWindow *int64 `yaml:"maxStreamReceiveWindow"`
|
||||||
|
InitConnReceiveWindow *int64 `yaml:"initConnReceiveWindow"`
|
||||||
|
MaxConnReceiveWindow *int64 `yaml:"maxConnReceiveWindow"`
|
||||||
|
MaxIdleTimeout *string `yaml:"maxIdleTimeout"`
|
||||||
|
MaxIncomingStream *int64 `yaml:"maxIncomingStream"`
|
||||||
|
DisablePathMTUDiscovery *bool `yaml:"disablePathMTUDiscovery"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaBandwidthConfig struct {
|
||||||
|
UP *string `yaml:"up"`
|
||||||
|
Down *string `yaml:"down"`
|
||||||
|
IgnoreClientBandwidth *bool `yaml:"ignoreClientBandwidth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaAuthType string
|
||||||
|
type HysteriaAuthConfig struct {
|
||||||
|
Type HysteriaAuthType `yaml:"type"`
|
||||||
|
Password string `yaml:"password,omitempty"`
|
||||||
|
Userpass map[string]string `yaml:"userpass,omitempty"`
|
||||||
|
Http struct {
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
Insecure bool `yaml:"insecure"`
|
||||||
|
} `yaml:"http,omitempty"`
|
||||||
|
Command string `yaml:"command,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaTCPUDPResolverConfig struct {
|
||||||
|
Address string `yaml:"addr"`
|
||||||
|
Timeout string `yaml:"timeout"`
|
||||||
|
}
|
||||||
|
type HysteriaTLSResolverConfig struct {
|
||||||
|
Address string `yaml:"addr"`
|
||||||
|
Timeout string `yaml:"timeout"`
|
||||||
|
SNI string `yaml:"sni"`
|
||||||
|
Insecure bool `yaml:"insecure"`
|
||||||
|
}
|
||||||
|
type HysteriaResolverType string
|
||||||
|
type HysteriaResolverConfig struct {
|
||||||
|
Type HysteriaResolverType `yaml:"type"`
|
||||||
|
TCP *HysteriaTCPUDPResolverConfig `yaml:"tcp,omitempty"`
|
||||||
|
UDP *HysteriaTCPUDPResolverConfig `yaml:"udp,omitempty"`
|
||||||
|
TLS *HysteriaTLSResolverConfig `yaml:"tls,omitempty"`
|
||||||
|
HTTPS *HysteriaTLSResolverConfig `yaml:"https,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaSniffConfig struct {
|
||||||
|
Enable bool `yaml:"enable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaACLConfig struct{}
|
||||||
|
type HysteriaOutboundConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaTrafficStatsConfig struct {
|
||||||
|
Listen string `yaml:"listen"`
|
||||||
|
Secret string `yaml:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaMasqueradeType string
|
||||||
|
type HysteriaMasqueradeFile struct {
|
||||||
|
Dir string `yaml:"dir"`
|
||||||
|
}
|
||||||
|
type HysteriaMasqueradeProxy struct {
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
RewriteHost *bool `yaml:"rewriteHost,omitempty"`
|
||||||
|
Insecure *bool `yaml:"insecure,omitempty"`
|
||||||
|
}
|
||||||
|
type HysteriaMasqueradeString struct {
|
||||||
|
Content string `yaml:"content"`
|
||||||
|
Headers map[string]string `yaml:"headers,omitempty"`
|
||||||
|
StatusCode int `yaml:"statusCode"`
|
||||||
|
}
|
||||||
|
type HysteriaMasqueradeConfig struct {
|
||||||
|
Type HysteriaMasqueradeType `yaml:"type"`
|
||||||
|
File *HysteriaMasqueradeFile `yaml:"file,omitempty"`
|
||||||
|
Proxy *HysteriaMasqueradeProxy `yaml:"proxy,omitempty"`
|
||||||
|
String *HysteriaMasqueradeString `yaml:"string,omitempty"`
|
||||||
|
|
||||||
|
ListenHTTP *string `yaml:"listenHTTP,omitempty"`
|
||||||
|
ListenHTTPS *string `yaml:"listenHTTPS,omitempty"`
|
||||||
|
ForceHTTPS *bool `yaml:"forceHTTPS,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HysteriaConfig struct {
|
||||||
|
Listen string `yaml:"listen"`
|
||||||
|
TLS HysteriaTLSConfig `yaml:"tls"`
|
||||||
|
Obfs *HysteriaObfsConfig `yaml:"obfs,omitempty"`
|
||||||
|
Quic *HysteriaQuicConfig `yaml:"quic,omitempty"`
|
||||||
|
SpeedTest *bool `yaml:"speedTest,omitempty"`
|
||||||
|
Auth HysteriaAuthConfig `yaml:"auth"`
|
||||||
|
Resolver *HysteriaResolverConfig `yaml:"resolver,omitempty"`
|
||||||
|
Sniff *HysteriaSniffConfig `yaml:"sniff,omitempty"`
|
||||||
|
ACL *HysteriaACLConfig `yaml:"acl,omitempty"`
|
||||||
|
Outbounds []HysteriaOutboundConfig `yaml:"outbounds,omitempty"`
|
||||||
|
TrafficStats *HysteriaTrafficStatsConfig `yaml:"trafficStats,omitempty"`
|
||||||
|
}
|
||||||
31
app/utils.go
Normal file
31
app/utils.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NodeConfigRsp struct {
|
||||||
|
Config NodeConfig `json:"config"`
|
||||||
|
HysteriaConfig HysteriaConfig `json:"hysteria"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSubUrl(conf NodeConfig, hystConf HysteriaConfig, key string, user User) string {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Add("type", "hysteria")
|
||||||
|
values.Add("security", "tls")
|
||||||
|
values.Add("alpn", "h3,h2")
|
||||||
|
values.Add("fp", "chrome")
|
||||||
|
values.Add("allowInsecure", "0")
|
||||||
|
values.Add("sni", conf.SNI)
|
||||||
|
if hystConf.Obfs != nil {
|
||||||
|
values.Add("obfs", hystConf.Obfs.Type)
|
||||||
|
values.Add("obfs-password", hystConf.Obfs.Salamander.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
host := fmt.Sprintf("%s@%s%s", key, conf.Host, hystConf.Listen)
|
||||||
|
subName := strings.ReplaceAll(conf.NameFormat, "{username}", user.Username)
|
||||||
|
return fmt.Sprintf("hysteria2://%s?%s#%s", host, values.Encode(), subName)
|
||||||
|
// hysteria2://pass@185.231.245.25:51052?&type=hysteria&mport&security=tls&sni=nix13.pw&alpn=h3&fp=chrome&allowInsecure=0#🇷🇺ssia (user1)
|
||||||
|
}
|
||||||
12
go.mod
Normal file
12
go.mod
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module Hyst2API
|
||||||
|
|
||||||
|
go 1.26
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/lib/pq v1.11.2
|
||||||
|
github.com/vinovest/sqlx v1.7.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require github.com/muir/sqltoken v0.1.0 // indirect
|
||||||
24
go.sum
Normal file
24
go.sum
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||||
|
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||||
|
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/muir/sqltoken v0.1.0 h1:edosEGsOClOZNfgGQNQSgxR9O6LiVefm2rDRqp2InuI=
|
||||||
|
github.com/muir/sqltoken v0.1.0/go.mod h1:lgOIORnKekMsuc/ZwdPOfwz/PtWLPCke43cEbT3uDuY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/vinovest/sqlx v1.7.2 h1:t/IahCJqO71GJYnhOcACiUXlMiiMomMHtxtUthdcBfo=
|
||||||
|
github.com/vinovest/sqlx v1.7.2/go.mod h1:o49uG4W/ZYZompljKx5GZ7qx6OFklPjSHXP63nSmND8=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
19
main.go
Normal file
19
main.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Hyst2API/app"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Post("/nodes", app.AddNode)
|
||||||
|
r.Get("/nodes", app.GetNodes)
|
||||||
|
r.Get("/nodes/{id}", app.GetNodeById)
|
||||||
|
r.Post("/users", app.AddUser)
|
||||||
|
r.Get("/sub/{id}", app.Sub)
|
||||||
|
log.Fatal(http.ListenAndServe(":9997", r))
|
||||||
|
}
|
||||||
6
scripts/00-init.sql
Normal file
6
scripts/00-init.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE nodes(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
token TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX nodes_id ON nodes(id);
|
||||||
Reference in New Issue
Block a user