separate frontend repository
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
"s-ui/service"
|
||||
"s-ui/util"
|
||||
)
|
||||
|
||||
const defaultJson = `
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"address": [
|
||||
"172.19.0.1/30",
|
||||
"fdfe:dcba:9876::1/126"
|
||||
],
|
||||
"mtu": 9000,
|
||||
"auto_route": true,
|
||||
"strict_route": false,
|
||||
"endpoint_independent_nat": false,
|
||||
"stack": "system",
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": true,
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 2080
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 2080,
|
||||
"users": []
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
type JsonService struct {
|
||||
service.SettingService
|
||||
LinkService
|
||||
}
|
||||
|
||||
func (j *JsonService) GetJson(subId string, format string) (*string, error) {
|
||||
var jsonConfig map[string]interface{}
|
||||
|
||||
client, inDatas, err := j.getData(subId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outbounds, outTags, err := j.getOutbounds(client.Config, inDatas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
links := j.LinkService.GetLinks(&client.Links, "external", "")
|
||||
for index, link := range links {
|
||||
json, tag, err := util.GetOutbound(link, index)
|
||||
if err == nil && len(tag) > 0 {
|
||||
*outbounds = append(*outbounds, *json)
|
||||
*outTags = append(*outTags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
j.addDefaultOutbounds(outbounds, outTags)
|
||||
|
||||
err = json.Unmarshal([]byte(defaultJson), &jsonConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonConfig["outbounds"] = outbounds
|
||||
|
||||
// Add other objects from settings
|
||||
j.addOthers(&jsonConfig)
|
||||
|
||||
result, _ := json.MarshalIndent(jsonConfig, "", " ")
|
||||
resultStr := string(result)
|
||||
return &resultStr, nil
|
||||
}
|
||||
|
||||
func (j *JsonService) getData(subId string) (*model.Client, []*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
client := &model.Client{}
|
||||
err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var clientInbounds []uint
|
||||
err = json.Unmarshal(client.Inbounds, &clientInbounds)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var inbounds []*model.Inbound
|
||||
err = db.Model(model.Inbound{}).Where("id in ?", clientInbounds).Find(&inbounds).Error
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return client, inbounds, nil
|
||||
}
|
||||
|
||||
func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inbounds []*model.Inbound) (*[]map[string]interface{}, *[]string, error) {
|
||||
var outbounds []map[string]interface{}
|
||||
var configs map[string]interface{}
|
||||
var outTags []string
|
||||
|
||||
err := json.Unmarshal(clientConfig, &configs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, inData := range inbounds {
|
||||
if len(inData.OutJson) < 5 {
|
||||
continue
|
||||
}
|
||||
var outbound map[string]interface{}
|
||||
err = json.Unmarshal(inData.OutJson, &outbound)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
protocol, _ := outbound["type"].(string)
|
||||
config, _ := configs[protocol].(map[string]interface{})
|
||||
for key, value := range config {
|
||||
if key != "alterId" && key != "name" {
|
||||
outbound[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
var addrs []map[string]interface{}
|
||||
err = json.Unmarshal(inData.Addrs, &addrs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
tag, _ := outbound["tag"].(string)
|
||||
if len(addrs) == 0 {
|
||||
// For mixed protocol, use separated socks and http
|
||||
if protocol == "mixed" {
|
||||
outbound["tag"] = tag
|
||||
j.pushMixed(&outbounds, &outTags, outbound)
|
||||
} else {
|
||||
outTags = append(outTags, tag)
|
||||
outbounds = append(outbounds, outbound)
|
||||
}
|
||||
} else {
|
||||
for index, addr := range addrs {
|
||||
// Copy original config
|
||||
newOut := make(map[string]interface{}, len(outbound))
|
||||
for key, value := range outbound {
|
||||
newOut[key] = value
|
||||
}
|
||||
// Change and push copied config
|
||||
newOut["server"], _ = addr["server"].(string)
|
||||
port, _ := addr["server_port"].(float64)
|
||||
newOut["server_port"] = int(port)
|
||||
|
||||
// Override TLS
|
||||
outTls, _ := newOut["tls"].(map[string]interface{})
|
||||
if addrTls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||
for key, value := range addrTls {
|
||||
outTls[key] = value
|
||||
}
|
||||
}
|
||||
newOut["tls"] = outTls
|
||||
|
||||
remark, _ := addr["remark"].(string)
|
||||
newTag := fmt.Sprintf("%d.%s%s", index+1, tag, remark)
|
||||
newOut["tag"] = newTag
|
||||
// For mixed protocol, use separated socks and http
|
||||
if protocol == "mixed" {
|
||||
j.pushMixed(&outbounds, &outTags, newOut)
|
||||
} else {
|
||||
outTags = append(outTags, newTag)
|
||||
outbounds = append(outbounds, newOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &outbounds, &outTags, nil
|
||||
}
|
||||
|
||||
func (j *JsonService) addDefaultOutbounds(outbounds *[]map[string]interface{}, outTags *[]string) {
|
||||
outbound := []map[string]interface{}{
|
||||
{
|
||||
"outbounds": append([]string{"auto", "direct"}, *outTags...),
|
||||
"tag": "proxy",
|
||||
"type": "selector",
|
||||
},
|
||||
{
|
||||
"tag": "auto",
|
||||
"type": "urltest",
|
||||
"outbounds": outTags,
|
||||
"url": "http://www.gstatic.com/generate_204",
|
||||
"interval": "10m",
|
||||
"tolerance": 50,
|
||||
},
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct",
|
||||
},
|
||||
}
|
||||
*outbounds = append(outbound, *outbounds...)
|
||||
}
|
||||
|
||||
func (j *JsonService) addOthers(jsonConfig *map[string]interface{}) error {
|
||||
rules := []interface{}{
|
||||
map[string]interface{}{
|
||||
"clash_mode": "Direct",
|
||||
"outbound": "direct",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"clash_mode": "Global",
|
||||
"outbound": "proxy",
|
||||
},
|
||||
}
|
||||
route := map[string]interface{}{
|
||||
"auto_detect_interface": true,
|
||||
"final": "proxy",
|
||||
"rules": rules,
|
||||
}
|
||||
|
||||
othersStr, err := j.SettingService.GetSubJsonExt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(othersStr) == 0 {
|
||||
(*jsonConfig)["route"] = route
|
||||
return nil
|
||||
}
|
||||
var othersJson map[string]interface{}
|
||||
err = json.Unmarshal([]byte(othersStr), &othersJson)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := othersJson["log"]; ok {
|
||||
(*jsonConfig)["log"] = othersJson["log"]
|
||||
}
|
||||
if _, ok := othersJson["dns"]; ok {
|
||||
(*jsonConfig)["dns"] = othersJson["dns"]
|
||||
}
|
||||
if _, ok := othersJson["inbounds"]; ok {
|
||||
(*jsonConfig)["inbounds"] = othersJson["inbounds"]
|
||||
}
|
||||
if _, ok := othersJson["experimental"]; ok {
|
||||
(*jsonConfig)["experimental"] = othersJson["experimental"]
|
||||
}
|
||||
if _, ok := othersJson["rule_set"]; ok {
|
||||
route["rule_set"] = othersJson["rule_set"]
|
||||
}
|
||||
if settingRules, ok := othersJson["rules"].([]interface{}); ok {
|
||||
route["rules"] = append(rules, settingRules...)
|
||||
}
|
||||
(*jsonConfig)["route"] = route
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JsonService) pushMixed(outbounds *[]map[string]interface{}, outTags *[]string, out map[string]interface{}) {
|
||||
socksOut := make(map[string]interface{}, 1)
|
||||
httpOut := make(map[string]interface{}, 1)
|
||||
for key, value := range out {
|
||||
socksOut[key] = value
|
||||
httpOut[key] = value
|
||||
}
|
||||
socksTag := fmt.Sprintf("%s-socks", out["tag"])
|
||||
httpTag := fmt.Sprintf("%s-http", out["tag"])
|
||||
socksOut["type"] = "socks"
|
||||
httpOut["type"] = "http"
|
||||
socksOut["tag"] = socksTag
|
||||
httpOut["tag"] = httpTag
|
||||
*outbounds = append(*outbounds, socksOut, httpOut)
|
||||
*outTags = append(*outTags, socksTag, httpTag)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"s-ui/logger"
|
||||
"s-ui/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
Type string `json:"type"`
|
||||
Remark string `json:"remark"`
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
type LinkService struct {
|
||||
}
|
||||
|
||||
func (s *LinkService) GetLinks(linkJson *json.RawMessage, types string, clientInfo string) []string {
|
||||
links := []Link{}
|
||||
var result []string
|
||||
err := json.Unmarshal(*linkJson, &links)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, link := range links {
|
||||
switch link.Type {
|
||||
case "external":
|
||||
result = append(result, link.Uri)
|
||||
case "sub":
|
||||
result = append(result, s.getExternalSub(link.Uri)...)
|
||||
case "local":
|
||||
if types == "all" {
|
||||
result = append(result, s.addClientInfo(link.Uri, clientInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *LinkService) addClientInfo(uri string, clientInfo string) string {
|
||||
if len(clientInfo) == 0 {
|
||||
return uri
|
||||
}
|
||||
protocol := strings.Split(uri, "://")
|
||||
if len(protocol) < 2 {
|
||||
return uri
|
||||
}
|
||||
switch protocol[0] {
|
||||
case "vmess":
|
||||
var vmessJson map[string]interface{}
|
||||
config, err := util.B64StrToByte(protocol[1])
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding vmess content:", err)
|
||||
return uri
|
||||
}
|
||||
err = json.Unmarshal(config, &vmessJson)
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding vmess content:", err)
|
||||
return uri
|
||||
}
|
||||
vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo
|
||||
result, err := json.MarshalIndent(vmessJson, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding vmess + clientInfo content:", err)
|
||||
return uri
|
||||
}
|
||||
return "vmess://" + util.ByteToB64Str(result)
|
||||
default:
|
||||
return uri + clientInfo
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LinkService) getExternalSub(url string) []string {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
// Make the HTTP request
|
||||
response, err := client.Get(url)
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error making HTTP request:", err)
|
||||
return nil
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// Read the response body
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error reading response body:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert if the content is Base64 encoded
|
||||
links := util.StrOrBase64Encoded(string(body))
|
||||
return strings.Split(links, "\n")
|
||||
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"s-ui/config"
|
||||
"s-ui/logger"
|
||||
"s-ui/middleware"
|
||||
"s-ui/network"
|
||||
"s-ui/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
service.SettingService
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
subPath, err := s.SettingService.GetSubPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subDomain, err := s.SettingService.GetSubDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if subDomain != "" {
|
||||
engine.Use(middleware.DomainValidator(subDomain))
|
||||
}
|
||||
|
||||
g := engine.Group(subPath)
|
||||
NewSubHandler(g)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
//This is an anonymous function, no function name
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
engine, err := s.initRouter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certFile, err := s.SettingService.GetSubCertFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyFile, err := s.SettingService.GetSubKeyFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listen, err := s.SettingService.GetSubListen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port, err := s.SettingService.GetSubPort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return err
|
||||
}
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info("Sub server run https on", listener.Addr())
|
||||
} else {
|
||||
logger.Info("Sub server run http on", listener.Addr())
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
Handler: engine,
|
||||
}
|
||||
|
||||
go func() {
|
||||
s.httpServer.Serve(listener)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
s.cancel()
|
||||
var err error
|
||||
if s.httpServer != nil {
|
||||
err = s.httpServer.Shutdown(s.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if s.listener != nil {
|
||||
err = s.listener.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) GetCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"s-ui/logger"
|
||||
"s-ui/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SubHandler struct {
|
||||
service.SettingService
|
||||
SubService
|
||||
JsonService
|
||||
}
|
||||
|
||||
func NewSubHandler(g *gin.RouterGroup) {
|
||||
a := &SubHandler{}
|
||||
a.initRouter(g)
|
||||
}
|
||||
|
||||
func (s *SubHandler) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/:subid", s.subs)
|
||||
}
|
||||
|
||||
func (s *SubHandler) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
format, isFormat := c.GetQuery("format")
|
||||
if isFormat {
|
||||
result, err := s.JsonService.GetJson(subId, format)
|
||||
if err != nil || result == nil {
|
||||
logger.Error(err)
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
c.String(200, *result)
|
||||
}
|
||||
} else {
|
||||
result, headers, err := s.SubService.GetSubs(subId)
|
||||
if err != nil || result == nil {
|
||||
logger.Error(err)
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
|
||||
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
|
||||
c.Writer.Header().Set("Profile-Title", headers[2])
|
||||
|
||||
c.String(200, *result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
"s-ui/service"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SubService struct {
|
||||
service.SettingService
|
||||
LinkService
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string) (*string, []string, error) {
|
||||
var err error
|
||||
|
||||
db := database.GetDB()
|
||||
client := &model.Client{}
|
||||
err = db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
clientInfo := ""
|
||||
subShowInfo, _ := s.SettingService.GetSubShowInfo()
|
||||
if subShowInfo {
|
||||
clientInfo = s.getClientInfo(client)
|
||||
}
|
||||
|
||||
linksArray := s.LinkService.GetLinks(&client.Links, "all", clientInfo)
|
||||
result := strings.Join(linksArray, "\n")
|
||||
|
||||
var headers []string
|
||||
updateInterval, _ := s.SettingService.GetSubUpdates()
|
||||
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", client.Up, client.Down, client.Volume, client.Expiry))
|
||||
headers = append(headers, fmt.Sprintf("%d", updateInterval))
|
||||
headers = append(headers, subId)
|
||||
|
||||
subEncode, _ := s.SettingService.GetSubEncode()
|
||||
if subEncode {
|
||||
result = base64.StdEncoding.EncodeToString([]byte(result))
|
||||
}
|
||||
|
||||
return &result, headers, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getClientInfo(c *model.Client) string {
|
||||
now := time.Now().Unix()
|
||||
|
||||
var result []string
|
||||
if vol := c.Volume - (c.Up + c.Down); vol > 0 {
|
||||
result = append(result, fmt.Sprintf("%s%s", s.formatTraffic(vol), "📊"))
|
||||
}
|
||||
if c.Expiry > 0 {
|
||||
result = append(result, fmt.Sprintf("%d%s⏳", (c.Expiry-now)/86400, "Days"))
|
||||
}
|
||||
if len(result) > 0 {
|
||||
return " " + strings.Join(result, " ")
|
||||
} else {
|
||||
return " ♾"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubService) formatTraffic(trafficBytes int64) string {
|
||||
if trafficBytes < 1024 {
|
||||
return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
|
||||
} else if trafficBytes < (1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024))
|
||||
} else if trafficBytes < (1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024))
|
||||
} else if trafficBytes < (1024 * 1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024))
|
||||
} else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024))
|
||||
} else {
|
||||
return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user