initial commit
This commit is contained in:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user