test
This commit is contained in:
44
app/config.go
Normal file
44
app/config.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
JWTSecret string
|
||||
Host string
|
||||
SNI string
|
||||
NameFormat string
|
||||
}
|
||||
|
||||
var cfg *Config
|
||||
var hysteriaCfg *HysteriaConfig
|
||||
|
||||
func LoadConfig() {
|
||||
cfg = &Config{
|
||||
JWTSecret: os.Getenv("JWT_SECRET"),
|
||||
Host: LoadIP(),
|
||||
SNI: os.Getenv("SNI"),
|
||||
NameFormat: os.Getenv("NAME_FORMAT"),
|
||||
}
|
||||
hysteriaCfg = &HysteriaConfig{}
|
||||
|
||||
f, err := os.Open("config.yaml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = yaml.Unmarshal(data, hysteriaCfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Println(cfg, hysteriaCfg)
|
||||
}
|
||||
38
app/database.go
Normal file
38
app/database.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/vinovest/sqlx"
|
||||
)
|
||||
import _ "github.com/lib/pq"
|
||||
|
||||
var db *sqlx.DB = nil
|
||||
|
||||
func init() {
|
||||
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()
|
||||
}
|
||||
108
app/provider.go
108
app/provider.go
@@ -1,9 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"github.com/vinovest/sqlx"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@@ -11,86 +9,46 @@ type User struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
type Provider struct {
|
||||
Users []User `json:"users"`
|
||||
LastUserID int `json:"last_user_id"`
|
||||
path string
|
||||
type UserUsage struct {
|
||||
UserID int `json:"user_id" db:"user_id"`
|
||||
Recv int64 `json:"recv" db:"recv"`
|
||||
Sent int64 `json:"sent" db:"sent"`
|
||||
}
|
||||
|
||||
// NewProvider создаёт новый провайдер с пустыми данными (файл не трогает)
|
||||
func NewProvider() *Provider {
|
||||
return &Provider{
|
||||
Users: make([]User, 0),
|
||||
LastUserID: 0,
|
||||
path: "./provider.json",
|
||||
}
|
||||
type UserRepository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
// 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
|
||||
func NewUserRepository(db *sqlx.DB) *UserRepository {
|
||||
return &UserRepository{db}
|
||||
}
|
||||
|
||||
// Save сохраняет текущее состояние в файл
|
||||
func (p *Provider) Save() error {
|
||||
data, err := json.MarshalIndent(p, "", " ") // для читаемости
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p.path, data, 0644)
|
||||
func (rep UserRepository) AddUser(username, password string) (User, error) {
|
||||
sql := "INSERT INTO users (username, password) VALUES (?, ?) RETURNING *;"
|
||||
sql = rep.db.Rebind(sql)
|
||||
var user User
|
||||
err := db.Get(&user, sql, username, password)
|
||||
return user, err
|
||||
}
|
||||
|
||||
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 (rep UserRepository) GetById(id int) (User, error) {
|
||||
var user User
|
||||
sql := "SELECT * FROM users WHERE id=?;"
|
||||
sql = rep.db.Rebind(sql)
|
||||
err := rep.db.Get(&user, sql, id)
|
||||
return user, err
|
||||
}
|
||||
func (rep UserRepository) GetUsers() ([]User, error) {
|
||||
users := make([]User, 0)
|
||||
sql := "SELECT * FROM users;"
|
||||
sql = rep.db.Rebind(sql)
|
||||
err := rep.db.Select(&users, sql)
|
||||
return users, err
|
||||
}
|
||||
|
||||
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()
|
||||
func (rep UserRepository) DeleteUser(id int) error {
|
||||
sql := "DELETE FROM users WHERE id=?;"
|
||||
sql = rep.db.Rebind(sql)
|
||||
_, err := rep.db.Exec(sql, id)
|
||||
return err
|
||||
}
|
||||
|
||||
107
app/routes.go
107
app/routes.go
@@ -1,12 +1,17 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type AddUserReq struct {
|
||||
@@ -31,22 +36,8 @@ func AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
rep := NewUserRepository(db)
|
||||
user, err := rep.AddUser(req.Username, req.Password)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
@@ -69,17 +60,8 @@ func DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
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()
|
||||
rep := NewUserRepository(db)
|
||||
err = rep.DeleteUser(req.ID)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
@@ -94,12 +76,13 @@ func AllUsers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := LoadProvider()
|
||||
rep := NewUserRepository(db)
|
||||
users, err := rep.GetUsers()
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
WriteResponse(w, provider)
|
||||
WriteResponse(w, users)
|
||||
}
|
||||
|
||||
type GetConnectURLReq struct {
|
||||
@@ -114,25 +97,58 @@ func GetUserURL(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := LoadProvider()
|
||||
rep := NewUserRepository(db)
|
||||
user, err := rep.GetById(req.ID)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
user, err := provider.GetById(req.ID)
|
||||
//if user.Password != req.Pass {
|
||||
// WriteError(w, errors.New("invalid password"))
|
||||
// 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,
|
||||
hysteriaCfg.Listen,
|
||||
hysteriaCfg.Obfs.Salamander.Password,
|
||||
cfg.SNI,
|
||||
formatConfigName(cfg.NameFormat, user),
|
||||
)
|
||||
WriteResponse(w, u)
|
||||
}
|
||||
|
||||
func Sub(w http.ResponseWriter, r *http.Request) {
|
||||
idS := chi.URLParam(r, "id")
|
||||
id, err := strconv.Atoi(idS)
|
||||
if err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if user.Password != req.Pass {
|
||||
WriteError(w, errors.New("invalid password"))
|
||||
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"
|
||||
rep := NewUserRepository(db)
|
||||
user, err := rep.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)
|
||||
u := fmt.Sprintf(
|
||||
urlTemplate,
|
||||
authString,
|
||||
cfg.Host,
|
||||
hysteriaCfg.Listen,
|
||||
hysteriaCfg.Obfs.Salamander.Password,
|
||||
cfg.SNI,
|
||||
formatConfigName(cfg.NameFormat, user),
|
||||
)
|
||||
w.Write([]byte(base64.StdEncoding.EncodeToString([]byte(u))))
|
||||
}
|
||||
|
||||
type AuthReq struct {
|
||||
@@ -154,7 +170,7 @@ func DoAuth(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
req := new(AuthReq)
|
||||
if err := json.Unmarshal(data, req); err != nil {
|
||||
if err = json.Unmarshal(data, req); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Println(err)
|
||||
return
|
||||
@@ -164,20 +180,21 @@ func DoAuth(w http.ResponseWriter, r *http.Request) {
|
||||
reqUser, err := decodeURL(req.Auth)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := LoadProvider()
|
||||
rep := NewUserRepository(db)
|
||||
user, err := rep.GetById(reqUser.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
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)
|
||||
|
||||
114
app/types.go
Normal file
114
app/types.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package app
|
||||
|
||||
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"`
|
||||
}
|
||||
35
app/utils.go
35
app/utils.go
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -47,31 +46,9 @@ func decodeURL(tokenString string) (User, error) {
|
||||
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() {
|
||||
func LoadIP() string {
|
||||
res, err := http.Get("https://api.ipify.org")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -82,10 +59,18 @@ func LoadIP() {
|
||||
panic(err)
|
||||
}
|
||||
ip = string(data)
|
||||
log.Println(ip)
|
||||
return ip
|
||||
}
|
||||
func formatConfigName(format string, user User) string {
|
||||
s := strings.ReplaceAll(format, "{username}", user.Username)
|
||||
s = strings.ReplaceAll(s, "{host}", ip)
|
||||
return s
|
||||
}
|
||||
|
||||
func Ptr[T any](t T) *T { return &t }
|
||||
func Val[T any](t *T, def T) T {
|
||||
if t == nil {
|
||||
return def
|
||||
}
|
||||
return *t
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user