refactor: 更新项目引用路径从perfect-panel/ppanel-server到perfect-panel/server
Build docker and publish / build (20.15.1) (push) Failing after 6m27s

feat: 添加版本和构建时间变量
fix: 修正短信队列类型注释错误
style: 清理未使用的代码和测试文件
docs: 更新安装文档中的下载链接
chore: 迁移数据库脚本添加日志和订阅配置
This commit is contained in:
2025-10-13 01:33:03 -07:00
parent 393b42f35a
commit c582087c0f
974 changed files with 23609 additions and 23398 deletions
-71
View File
@@ -1,71 +0,0 @@
package adapter
import (
"github.com/perfect-panel/ppanel-server/internal/model/server"
"github.com/perfect-panel/ppanel-server/pkg/adapter/clash"
"github.com/perfect-panel/ppanel-server/pkg/adapter/general"
"github.com/perfect-panel/ppanel-server/pkg/adapter/loon"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/adapter/quantumultx"
"github.com/perfect-panel/ppanel-server/pkg/adapter/shadowrocket"
"github.com/perfect-panel/ppanel-server/pkg/adapter/singbox"
"github.com/perfect-panel/ppanel-server/pkg/adapter/surfboard"
)
type Adapter struct {
proxy.Adapter
}
func NewAdapter(nodes []*server.Server, rules []*server.RuleGroup) *Adapter {
// 转换服务器列表
proxies := adapterProxies(nodes)
// 生成代理组
proxyGroup, region := generateProxyGroup(proxies)
// 转换规则组
g, r := adapterRules(rules)
// 加入兜底节点
for i, group := range g {
if len(group.Proxies) == 0 {
g[i].Proxies = append([]string{"DIRECT"}, region...)
}
}
// 合并代理组
proxyGroup = append(proxyGroup, g...)
return &Adapter{
Adapter: proxy.Adapter{
Proxies: proxies,
Group: proxyGroup,
Rules: r,
Region: region,
},
}
}
func (m *Adapter) BuildClash(uuid string) ([]byte, error) {
client := clash.NewClash(m.Adapter)
return client.Build(uuid)
}
func (m *Adapter) BuildGeneral(uuid string) []byte {
return general.GenerateBase64General(m.Proxies, uuid)
}
func (m *Adapter) BuildLoon(uuid string) []byte {
return loon.BuildLoon(m.Proxies, uuid)
}
func (m *Adapter) BuildQuantumultX(uuid string) string {
return quantumultx.BuildQuantumultX(m.Proxies, uuid)
}
func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) {
return singbox.BuildSingbox(m.Adapter, uuid)
}
func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte {
return shadowrocket.BuildShadowrocket(m.Proxies, uuid, userInfo)
}
func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte {
return surfboard.BuildSurfboard(m.Adapter, siteName, user)
}
-138
View File
@@ -1,138 +0,0 @@
package adapter
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/perfect-panel/ppanel-server/internal/model/server"
"github.com/perfect-panel/ppanel-server/pkg/adapter/surfboard"
)
func createTestServer() []*server.Server {
c := server.Shadowsocks{
Method: "aes-256-gcm",
Port: 10301,
ServerKey: "",
}
data, _ := json.Marshal(c)
relays := creatRelayNode()
relay, _ := json.Marshal(relays)
enable := true
// 创建一个测试用的服务器列表
return []*server.Server{
{
Id: 1,
Name: "Test Server 1",
Tags: "",
Country: "CN",
City: "",
Latitude: "",
Longitude: "",
ServerAddr: "test1.example.com",
RelayMode: "random",
RelayNode: string(relay),
SpeedLimit: 0,
TrafficRatio: 0,
GroupId: 0,
Protocol: "shadowsocks",
Config: string(data),
Enable: &enable,
Sort: 0,
},
}
}
func creatRelayNode() []*server.NodeRelay {
var nodes []*server.NodeRelay
for i := 0; i < 10; i++ {
port := 10301 + i
c := server.NodeRelay{
Host: fmt.Sprintf("192.168.1.%d", i),
Port: port,
Prefix: fmt.Sprintf("relay-%d", i),
}
nodes = append(nodes, &c)
}
return nodes
}
func TestNewAdapter(t *testing.T) {
nodes := createTestServer()
rules := []*server.RuleGroup{
{
Name: "Test Rule Group 1",
Tags: "",
Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1",
},
}
adapter := NewAdapter(nodes, rules)
bytes, err := adapter.BuildClash("some-uuid")
if err != nil {
t.Errorf("Failed to build adapter: %v", err)
return
}
t.Logf("Adapter built successfully: %s", string(bytes))
}
func TestAdapter_BuildSingbox(t *testing.T) {
nodes := createTestServer()
rules := []*server.RuleGroup{
{
Name: "Test Rule Group 1",
Tags: "",
Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1",
},
}
adapter := NewAdapter(nodes, rules)
bytes, err := adapter.BuildSingbox("some-uuid")
if err != nil {
t.Errorf("Failed to build adapter: %v", err)
return
}
var pretty map[string]interface{}
_ = json.Unmarshal(bytes, &pretty)
if pretty == nil {
t.Errorf("Failed to parse Singbox config")
return
}
prettyStr, err := json.MarshalIndent(pretty, "", " ")
if err != nil {
t.Errorf("Failed to format Singbox config: %v", err)
return
}
t.Logf("Adapter built successfully: \n %s", string(prettyStr))
}
func TestAdapter_BuildSurfboard(t *testing.T) {
nodes := createTestServer()
rules := []*server.RuleGroup{
{
Name: "Test Rule Group 1",
Tags: "",
Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1",
},
}
adapter := NewAdapter(nodes, rules)
user := surfboard.UserInfo{
UUID: "some-uuid",
Upload: 200,
Download: 13012,
TotalTraffic: 1024000,
ExpiredDate: time.Now().Add(24 * time.Hour),
SubscribeURL: "",
}
bytes := adapter.BuildSurfboard("test-site", user)
if bytes == nil {
t.Errorf("Failed to build adapter")
return
}
t.Logf("Adapter built successfully: %s", string(bytes))
}
-68
View File
@@ -1,68 +0,0 @@
package clash
import (
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"gopkg.in/yaml.v3"
)
type Clash struct {
proxy.Adapter
}
func NewClash(adapter proxy.Adapter) *Clash {
return &Clash{
Adapter: adapter,
}
}
func (c *Clash) Build(uuid string) ([]byte, error) {
var proxies []Proxy
for _, v := range c.Proxies {
p, err := c.parseProxy(v, uuid)
if err != nil {
logger.Errorf("Failed to parse proxy for %s: %s", v.Name, err.Error())
continue
}
proxies = append(proxies, *p)
}
var rawConfig RawConfig
if err := yaml.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil {
return nil, fmt.Errorf("failed to unmarshal template: %w", err)
}
rawConfig.Proxies = proxies
// generate proxy groups
var groups []ProxyGroup
for _, group := range c.Group {
groups = append(groups, ProxyGroup{
Name: group.Name,
Type: string(group.Type),
Proxies: group.Proxies,
Url: group.URL,
Interval: group.Interval,
})
}
rawConfig.ProxyGroups = groups
rawConfig.Rules = append(c.Rules, "# 最终规则", "MATCH,手动选择")
return yaml.Marshal(&rawConfig)
}
func (c *Clash) parseProxy(p proxy.Proxy, uuid string) (*Proxy, error) {
parseFuncs := map[string]func(proxy.Proxy, string) (*Proxy, error){
"shadowsocks": parseShadowsocks,
"trojan": parseTrojan,
"vless": parseVless,
"vmess": parseVmess,
"hysteria2": parseHysteria2,
"tuic": parseTuic,
}
if parseFunc, exists := parseFuncs[p.Protocol]; exists {
return parseFunc(p, uuid)
}
logger.Errorw("Unknown protocol", logger.Field("protocol", p.Protocol), logger.Field("server", p.Name))
return nil, fmt.Errorf("unknown protocol: %s", p.Protocol)
}
-41
View File
@@ -1,41 +0,0 @@
package clash
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/stretchr/testify/assert"
)
func TestClash_Build(t *testing.T) {
adapter := proxy.Adapter{
Proxies: []proxy.Proxy{
{
Name: "test-proxy",
Protocol: "shadowsocks",
Server: "1.2.3.4",
Port: 8388,
Option: proxy.Shadowsocks{
Method: "aes-256-gcm",
},
},
},
Group: []proxy.Group{
{
Name: "test-group",
Type: "select",
Proxies: []string{"test-proxy"},
},
},
Rules: []string{
"DOMAIN-SUFFIX,example.com,DIRECT",
"GEOIP,CN,DIRECT",
"MATCH,DIRECT",
},
}
clash := NewClash(adapter)
result, err := clash.Build("test-uuid")
assert.NoError(t, err)
assert.NotNil(t, result)
}
-35
View File
@@ -1,35 +0,0 @@
package clash
const DefaultTemplate = `
mixed-port: 7890
allow-lan: true
bind-address: "*"
mode: rule
log-level: info
external-controller: 127.0.0.1:9090
global-client-fingerprint: chrome
unified-delay: true
geox-url:
mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb"
dns:
enable: true
ipv6: true
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
use-hosts: true
default-nameserver:
- 120.53.53.53
- 1.12.12.12
nameserver:
- https://120.53.53.53/dns-query#skip-cert-verify=true
- tls://1.12.12.12#skip-cert-verify=true
proxy-server-nameserver:
- https://120.53.53.53/dns-query#skip-cert-verify=true
- tls://1.12.12.12#skip-cert-verify=true
proxies:
proxy-groups:
rules:
`
-131
View File
@@ -1,131 +0,0 @@
package clash
type RawConfig struct {
Port int `yaml:"port" json:"port"`
SocksPort int `yaml:"socks-port" json:"socks-port"`
RedirPort int `yaml:"redir-port" json:"redir-port"`
TProxyPort int `yaml:"tproxy-port" json:"tproxy-port"`
MixedPort int `yaml:"mixed-port" json:"mixed-port"`
AllowLan bool `yaml:"allow-lan" json:"allow-lan"`
Mode string `yaml:"mode" json:"mode"`
LogLevel string `yaml:"log-level" json:"log-level"`
ExternalController string `yaml:"external-controller" json:"external-controller"`
Secret string `yaml:"secret" json:"secret"`
Proxies []Proxy `yaml:"proxies" json:"proxies"`
ProxyGroups []ProxyGroup `yaml:"proxy-groups" json:"proxy-groups"`
Rules []string `yaml:"rules" json:"rule"`
}
type Proxy struct {
// 基础数据
Name string `yaml:"name"`
Type string `yaml:"type"`
Server string `yaml:"server"`
Port int `yaml:"port,omitempty"`
// Shadowsocks
Password string `yaml:"password,omitempty"`
Cipher string `yaml:"cipher,omitempty"`
UDP bool `yaml:"udp,omitempty"`
Plugin string `yaml:"plugin,omitempty"`
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
// Vmess
UUID string `yaml:"uuid,omitempty"`
AlterID *int `yaml:"alterId,omitempty"`
Network string `yaml:"network,omitempty"`
TLS bool `yaml:"tls,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ServerName string `yaml:"servername,omitempty"`
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
PacketAddr bool `yaml:"packet-addr,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
GlobalPadding bool `yaml:"global-padding,omitempty"`
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
// Vless
Flow string `yaml:"flow,omitempty"`
WSPath string `yaml:"ws-path,omitempty"`
WSHeaders map[string]string `yaml:"ws-headers,omitempty"`
// Trojan
SNI string `yaml:"sni,omitempty"`
SSOpts TrojanSSOption `yaml:"ss-opts,omitempty"`
// Hysteria2
Ports string `yaml:"ports,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ObfsPassword string `yaml:"obfs-password,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
CWND int `yaml:"cwnd,omitempty"`
UdpMTU int `yaml:"udp-mtu,omitempty"`
// Tuic
Token string `yaml:"token,omitempty"`
Ip string `yaml:"ip,omitempty"`
HeartbeatInterval int `yaml:"heartbeat-interval,omitempty"`
ReduceRtt bool `yaml:"reduce-rtt,omitempty"`
RequestTimeout int `yaml:"request-timeout,omitempty"`
UdpRelayMode string `yaml:"udp-relay-mode,omitempty"`
CongestionController string `yaml:"congestion-controller,omitempty"`
DisableSni bool `yaml:"disable-sni,omitempty"`
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
MaxOpenStreams int `yaml:"max-open-streams,omitempty"`
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
ReceiveWindow int `yaml:"recv-window,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `yaml:"max-datagram-frame-size,omitempty"`
UDPOverStream bool `yaml:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `yaml:"udp-over-stream-version,omitempty"`
}
type ProxyGroup struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Proxies []string `yaml:"proxies"`
Url string `yaml:"url,omitempty"`
Interval int `yaml:"interval,omitempty"`
}
type TrojanSSOption struct {
Enabled bool `yaml:"enabled,omitempty"`
Method string `yaml:"method,omitempty"`
Password string `yaml:"password,omitempty"`
}
type RealityOptions struct {
PublicKey string `yaml:"public-key"`
ShortID string `yaml:"short-id"`
}
type HTTPOptions struct {
Method string `yaml:"method,omitempty"`
Path []string `yaml:"path,omitempty"`
Headers map[string][]string `yaml:"headers,omitempty"`
}
type HTTP2Options struct {
Host []string `yaml:"host,omitempty"`
Path string `yaml:"path,omitempty"`
}
type GrpcOptions struct {
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
}
type WSOptions struct {
Path string `yaml:"path,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
MaxEarlyData int `yaml:"max-early-data,omitempty"`
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
V2rayHttpUpgrade bool `yaml:"v2ray-http-upgrade,omitempty"`
V2rayHttpUpgradeFastOpen bool `yaml:"v2ray-http-upgrade-fast-open,omitempty"`
}
-165
View File
@@ -1,165 +0,0 @@
package clash
import (
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func parseShadowsocks(s proxy.Proxy, uuid string) (*Proxy, error) {
config, ok := s.Option.(proxy.Shadowsocks)
if !ok {
return nil, fmt.Errorf("invalid type for Shadowsocks")
}
p := &Proxy{
Name: s.Name,
Type: "ss",
Server: s.Server,
Port: s.Port,
Cipher: config.Method,
Password: uuid,
UDP: true,
}
return p, nil
}
func parseTrojan(data proxy.Proxy, password string) (*Proxy, error) {
trojan, ok := data.Option.(proxy.Trojan)
if !ok {
return nil, fmt.Errorf("invalid type for Trojan")
}
p := &Proxy{
Name: data.Name,
Type: "trojan",
Server: data.Server,
Port: data.Port,
Password: password,
SNI: trojan.SecurityConfig.SNI,
SkipCertVerify: trojan.SecurityConfig.AllowInsecure,
}
setTransportOptions(p, trojan.Transport, trojan.TransportConfig)
return p, nil
}
func parseVless(data proxy.Proxy, uuid string) (*Proxy, error) {
vless, ok := data.Option.(proxy.Vless)
if !ok {
return nil, fmt.Errorf("invalid type for Vless")
}
p := &Proxy{
Name: data.Name,
Type: "vless",
Server: data.Server,
Port: data.Port,
UUID: uuid,
Flow: vless.Flow,
}
setSecurityOptions(p, vless.Security, vless.SecurityConfig)
clashTransport(p, vless.Transport, vless.TransportConfig)
return p, nil
}
func parseVmess(data proxy.Proxy, uuid string) (*Proxy, error) {
vmess, ok := data.Option.(proxy.Vmess)
if !ok {
return nil, fmt.Errorf("invalid type for Vmess")
}
alterID := 0
p := &Proxy{
Name: data.Name,
Type: "vmess",
Server: data.Server,
Port: data.Port,
UUID: uuid,
AlterID: &alterID,
Cipher: "auto",
}
setSecurityOptions(p, vmess.Security, vmess.SecurityConfig)
clashTransport(p, vmess.Transport, vmess.TransportConfig)
return p, nil
}
func parseHysteria2(data proxy.Proxy, uuid string) (*Proxy, error) {
hysteria2, ok := data.Option.(proxy.Hysteria2)
if !ok {
return nil, fmt.Errorf("invalid type for Hysteria2")
}
p := &Proxy{
Name: data.Name,
Type: "hysteria2",
Server: data.Server,
Port: data.Port,
Ports: hysteria2.HopPorts,
Password: uuid,
HeartbeatInterval: hysteria2.HopInterval,
SkipCertVerify: hysteria2.SecurityConfig.AllowInsecure,
SNI: hysteria2.SecurityConfig.SNI,
}
if hysteria2.ObfsPassword != "" {
p.Obfs = "salamander"
p.ObfsPassword = hysteria2.ObfsPassword
}
return p, nil
}
func parseTuic(data proxy.Proxy, uuid string) (*Proxy, error) {
tuic, ok := data.Option.(proxy.Tuic)
if !ok {
return nil, fmt.Errorf("invalid type for Tuic")
}
p := &Proxy{
Name: data.Name,
Type: "tuic",
Server: data.Server,
Port: data.Port,
UUID: uuid,
Password: uuid,
SNI: tuic.SecurityConfig.SNI,
SkipCertVerify: tuic.SecurityConfig.AllowInsecure,
}
return p, nil
}
func setSecurityOptions(p *Proxy, security string, config proxy.SecurityConfig) {
switch security {
case "tls":
p.TLS = true
p.ServerName = config.SNI
p.ClientFingerprint = config.Fingerprint
p.SkipCertVerify = config.AllowInsecure
case "reality":
p.TLS = true
p.ServerName = config.SNI
p.ClientFingerprint = config.Fingerprint
p.RealityOpts = RealityOptions{
PublicKey: config.RealityPublicKey,
ShortID: config.RealityShortId,
}
p.SkipCertVerify = config.AllowInsecure
default:
p.TLS = false
}
}
func setTransportOptions(p *Proxy, transport string, config proxy.TransportConfig) {
switch transport {
case "websocket":
p.Network = "ws"
p.WSOpts = WSOptions{
Path: config.Path,
Headers: map[string]string{
"Host": config.Host,
},
}
case "grpc":
p.Network = "grpc"
p.GrpcOpts = GrpcOptions{
GrpcServiceName: config.ServiceName,
}
default:
p.Network = "tcp"
}
}
-33
View File
@@ -1,33 +0,0 @@
package clash
import "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
func clashTransport(c *Proxy, transportType string, transportConfig proxy.TransportConfig) {
switch transportType {
case "websocket", "httpupgrade":
if transportType == "websocket" {
c.Network = "ws"
} else {
c.Network = transportType
}
c.WSOpts = WSOptions{
Path: transportConfig.Path,
Headers: map[string]string{},
}
if transportConfig.Host != "" {
c.WSOpts.Headers["host"] = transportConfig.Host
}
if transportType == "httpupgrade" {
c.WSOpts.V2rayHttpUpgrade = true
}
case "grpc":
c.Network = "grpc"
c.GrpcOpts = GrpcOptions{
GrpcServiceName: transportConfig.ServiceName,
}
case "tcp":
c.Network = "tcp"
}
}
-245
View File
@@ -1,245 +0,0 @@
package general
import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
type v2rayShareLink struct {
Ps string `json:"ps"`
Add string `json:"add"`
Port string `json:"port"`
ID string `json:"id"`
Aid string `json:"aid"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
SNI string `json:"sni"`
Path string `json:"path"`
TLS string `json:"tls"`
Flow string `json:"flow,omitempty"`
Alpn string `json:"alpn,omitempty"`
AllowInsecure bool `json:"allowInsecure"`
Fingerprint string `json:"fp,omitempty"`
PublicKey string `json:"pbk,omitempty"`
ShortId string `json:"sid,omitempty"`
SpiderX string `json:"spx,omitempty"`
V string `json:"v"`
}
// GenerateBase64General will output node URLs split by '\n' and then encode into base64
func GenerateBase64General(data []proxy.Proxy, uuid string) []byte {
var links []string
for _, v := range data {
p := buildProxy(v, uuid)
if p == "" {
continue
}
links = append(links, p)
}
var rsp []byte
rsp = base64.RawStdEncoding.AppendEncode(rsp, []byte(strings.Join(links, "\n")))
return rsp
}
func buildProxy(data proxy.Proxy, uuid string) string {
switch data.Protocol {
case "shadowsocks":
return ShadowsocksUri(data, uuid)
case "vmess":
return VmessUri(data, uuid)
case "vless":
return VlessUri(data, uuid)
case "trojan":
return TrojanUri(data, uuid)
case "hysteria2":
return Hysteria2Uri(data, uuid)
case "tuic":
return TuicUri(data, uuid)
default:
return ""
}
}
func ShadowsocksUri(data proxy.Proxy, uuid string) string {
ss := data.Option.(proxy.Shadowsocks)
// sip002
u := &url.URL{
Scheme: "ss",
// 还没有写 2022 的
User: url.User(strings.TrimSuffix(base64.URLEncoding.EncodeToString([]byte(ss.Method+":"+uuid)), "=")),
Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)),
Fragment: data.Name,
}
return u.String()
}
func VmessUri(data proxy.Proxy, uuid string) string {
vmess := data.Option.(proxy.Vmess)
transport := vmess.TransportConfig
securityConfig := vmess.SecurityConfig
var s = v2rayShareLink{
V: "2",
Add: data.Server,
Port: fmt.Sprint(data.Port),
ID: uuid,
Aid: "0",
Net: vmess.Transport,
// Type: "?",
Host: transport.Host,
Path: transport.Path,
}
if vmess.Security == "tls" {
s.TLS = "tls"
s.SNI = securityConfig.SNI
s.AllowInsecure = securityConfig.AllowInsecure
s.Fingerprint = securityConfig.Fingerprint
}
b, _ := json.Marshal(s)
return "vmess://" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(b), "=")
}
func VlessUri(data proxy.Proxy, uuid string) string {
vless := data.Option.(proxy.Vless)
transportConfig := vless.TransportConfig
securityConfig := vless.SecurityConfig
var query = make(url.Values)
setQuery(&query, "flow", vless.Flow)
setQuery(&query, "type", vless.Transport)
setQuery(&query, "security", vless.Security)
switch vless.Transport {
case "ws", "http", "httpupgrade":
setQuery(&query, "path", transportConfig.Path)
setQuery(&query, "host", transportConfig.Host)
case "grpc":
setQuery(&query, "serviceName", transportConfig.ServiceName)
case "meek":
setQuery(&query, "url", transportConfig.Host)
}
setQuery(&query, "sni", securityConfig.SNI)
setQuery(&query, "fp", securityConfig.Fingerprint)
setQuery(&query, "pbk", securityConfig.RealityPublicKey)
setQuery(&query, "sid", securityConfig.RealityShortId)
u := url.URL{
Scheme: "vless",
User: url.User(uuid),
Host: net.JoinHostPort(data.Server, fmt.Sprint(data.Port)),
RawQuery: query.Encode(),
Fragment: data.Name,
}
return u.String()
}
func TrojanUri(data proxy.Proxy, uuid string) string {
trojan := data.Option.(proxy.Trojan)
transportConfig := trojan.TransportConfig
securityConfig := trojan.SecurityConfig
var query = make(url.Values)
setQuery(&query, "type", trojan.Transport)
setQuery(&query, "security", trojan.Security)
switch trojan.Transport {
case "ws", "http", "httpupgrade":
setQuery(&query, "path", transportConfig.Path)
setQuery(&query, "host", transportConfig.Host)
case "grpc":
setQuery(&query, "serviceName", transportConfig.ServiceName)
case "meek":
setQuery(&query, "url", transportConfig.Host)
}
setQuery(&query, "sni", securityConfig.SNI)
setQuery(&query, "fp", securityConfig.Fingerprint)
setQuery(&query, "pbk", securityConfig.RealityPublicKey)
setQuery(&query, "sid", securityConfig.RealityShortId)
if securityConfig.AllowInsecure {
setQuery(&query, "allowInsecure", "1")
}
u := &url.URL{
Scheme: "trojan",
User: url.User(uuid),
Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)),
RawQuery: query.Encode(),
Fragment: data.Name,
}
return u.String()
}
func Hysteria2Uri(data proxy.Proxy, uuid string) string {
hysteria2 := data.Option.(proxy.Hysteria2)
var query = make(url.Values)
setQuery(&query, "sni", hysteria2.SecurityConfig.SNI)
if hysteria2.SecurityConfig.AllowInsecure {
setQuery(&query, "insecure", "1")
}
if hp := strings.TrimSpace(hysteria2.HopPorts); hp != "" {
setQuery(&query, "mport", hp)
}
if hysteria2.ObfsPassword != "" {
setQuery(&query, "obfs", "salamander")
setQuery(&query, "obfs-password", hysteria2.ObfsPassword)
}
u := &url.URL{
Scheme: "hysteria2",
User: url.User(uuid),
Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)),
RawQuery: query.Encode(),
Fragment: data.Name,
}
return u.String()
}
func TuicUri(data proxy.Proxy, uuid string) string {
tuic := data.Option.(proxy.Tuic)
var query = make(url.Values)
setQuery(&query, "congestion_control", "bbr")
if tuic.SecurityConfig.SNI == "" {
setQuery(&query, "sni", tuic.SecurityConfig.SNI)
} else {
setQuery(&query, "disable_sni", "1")
}
if tuic.SecurityConfig.AllowInsecure {
setQuery(&query, "allow_insecure", "1")
}
u := &url.URL{
Scheme: "tuic",
User: url.User(uuid + ":" + uuid),
Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)),
RawQuery: query.Encode(),
Fragment: data.Name,
}
return u.String()
}
func setQuery(q *url.Values, k, v string) {
if v != "" {
q.Set(k, v)
}
}
-26
View File
@@ -1,26 +0,0 @@
package general
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func createServer() proxy.Proxy {
return proxy.Proxy{
Name: "Meta",
Server: "127.0.0.1",
Port: 13092,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Method: "aes-256-gcm",
ServerKey: "",
},
}
}
func TestGenerateBase64General(t *testing.T) {
s := createServer()
p := buildProxy(s, "935b33c7-e128-49f2-816b-71070469cac2")
t.Log(p)
}
-27
View File
@@ -1,27 +0,0 @@
package loon
import (
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func BuildLoon(servers []proxy.Proxy, uuid string) []byte {
uri := ""
for _, s := range servers {
switch s.Protocol {
case "vmess":
uri += buildVMess(s, uuid)
case "shadowsocks":
uri += buildShadowsocks(s, uuid)
case "trojan":
uri += buildTrojan(s, uuid)
case "vless":
uri += buildVless(s, uuid)
case "hysteria2":
uri += buildHysteria2(s, uuid)
default:
continue
}
}
return []byte(uri)
}
-34
View File
@@ -1,34 +0,0 @@
package loon
import (
"fmt"
"strconv"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildHysteria2(data proxy.Proxy, password string) string {
hysteria2 := data.Option.(proxy.Hysteria2)
configs := []string{
fmt.Sprintf("%s=Hysteria2", data.Name),
data.Server,
strconv.Itoa(data.Port),
password,
"udp=true",
}
if hysteria2.ObfsPassword != "" {
configs = append(configs, "obfs=salamander", fmt.Sprintf("salamander-password=%s", hysteria2.ObfsPassword))
}
if hysteria2.SecurityConfig.SNI != "" {
configs = append(configs, fmt.Sprintf("sni=%s", hysteria2.SecurityConfig.SNI))
if hysteria2.SecurityConfig.AllowInsecure {
configs = append(configs, "skip-cert-verify=true")
} else {
configs = append(configs, "skip-cert-verify=false")
}
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}
-29
View File
@@ -1,29 +0,0 @@
package loon
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func createSS() proxy.Proxy {
return proxy.Proxy{
Name: "Shadowsocks",
Server: "127.0.0.1",
Port: 10301,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Method: "aes-256-gcm",
ServerKey: "",
},
}
}
func TestBuildSS(t *testing.T) {
s := createSS()
password := "f0d0237d-193a-4cf5-99dd-b02207beaea6"
uri := buildShadowsocks(s, password)
t.Log(uri)
}
-49
View File
@@ -1,49 +0,0 @@
package loon
import (
"fmt"
"strconv"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/ppanel-server/pkg/uuidx"
)
func buildShadowsocks(data proxy.Proxy, password string) string {
shadowsocks := data.Option.(proxy.Shadowsocks)
// If the method is 2022-blake3-chacha20-poly1305, it means that the server is a relay server
if shadowsocks.Method == "2022-blake3-chacha20-poly1305" {
return ""
}
if strings.Contains(shadowsocks.Method, "2022") {
serverKey, userKey := generateShadowsocks2022Password(shadowsocks, password)
password = fmt.Sprintf("%s:%s", serverKey, userKey)
}
configs := []string{
fmt.Sprintf("%s=Shadowsocks", data.Name),
data.Server,
strconv.Itoa(data.Port),
shadowsocks.Method,
password,
"fast-open=false",
"udp=true",
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}
func generateShadowsocks2022Password(ss proxy.Shadowsocks, password string) (string, string) {
// server key
var serverKey string
if ss.Method == "2022-blake3-aes-128-gcm" {
serverKey = tool.GenerateCipher(ss.ServerKey, 16)
password = uuidx.UUIDToBase64(password, 16)
} else {
serverKey = tool.GenerateCipher(ss.ServerKey, 32)
password = uuidx.UUIDToBase64(password, 32)
}
return serverKey, password
}
-44
View File
@@ -1,44 +0,0 @@
package loon
import (
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildTrojan(data proxy.Proxy, password string) string {
trojan := data.Option.(proxy.Trojan)
configs := []string{
fmt.Sprintf("%s=trojan", data.Name),
data.Server,
fmt.Sprintf("%d", data.Port),
"auto",
password,
"fast-open=false",
"udp=true",
}
if trojan.SecurityConfig.SNI != "" {
configs = append(configs, fmt.Sprintf("sni=%s", trojan.SecurityConfig.SNI))
}
if trojan.SecurityConfig.AllowInsecure {
configs = append(configs, "skip-cert-verify=true")
} else {
configs = append(configs, "skip-cert-verify=false")
}
if trojan.Transport == "websocket" {
configs = append(configs, "transport=ws")
if trojan.TransportConfig.Path != "" {
configs = append(configs, fmt.Sprintf("path=%s", trojan.TransportConfig.Path))
}
if trojan.TransportConfig.Host != "" {
configs = append(configs, fmt.Sprintf("host=%s", trojan.TransportConfig.Host))
}
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}
-62
View File
@@ -1,62 +0,0 @@
package loon
import (
"fmt"
"strconv"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/logger"
)
func buildVless(data proxy.Proxy, password string) string {
vless := data.Option.(proxy.Vless)
// If flow is not empty, it means that the server is a relay server
if vless.Flow != "" {
return ""
}
configs := []string{
fmt.Sprintf("%s=vless", data.Name),
data.Server,
strconv.Itoa(data.Port),
"auto",
password,
"fast-open=false",
"udp=true",
"alterId=0",
}
switch vless.Transport {
case "tcp":
configs = append(configs, "transport=tcp")
case "websocket":
configs = append(configs, "transport=ws")
if vless.TransportConfig.Path != "" {
configs = append(configs, fmt.Sprintf("path=%s", vless.TransportConfig.Path))
}
if vless.TransportConfig.Host != "" {
configs = append(configs, fmt.Sprintf("host=%s", vless.TransportConfig.Host))
}
default:
logger.Info("Loon Unknown transport type: ", logger.Field("transport", vless.Transport))
return ""
}
if vless.Security == "tls" {
configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vless.SecurityConfig.SNI))
if vless.SecurityConfig.AllowInsecure {
configs = append(configs, "skip-cert-verify=true")
} else {
configs = append(configs, "skip-cert-verify=false")
}
} else if vless.Security == "reality" {
// Loon does not support reality security
logger.Info("Loon Unknown security type: ", logger.Field("security", vless.Security))
return ""
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}
-53
View File
@@ -1,53 +0,0 @@
package loon
import (
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/logger"
)
func buildVMess(data proxy.Proxy, password string) string {
vmess := data.Option.(proxy.Vmess)
configs := []string{
fmt.Sprintf("%s=vmess", data.Name),
data.Server,
fmt.Sprintf("%d", data.Port),
"auto",
password,
"fast-open=false",
"udp=true",
"alterId=0",
}
switch vmess.Transport {
case "tcp":
configs = append(configs, "transport=tcp")
case "websocket":
configs = append(configs, "transport=ws")
if vmess.TransportConfig.Path != "" {
configs = append(configs, fmt.Sprintf("path=%s", vmess.TransportConfig.Path))
}
if vmess.TransportConfig.Host != "" {
configs = append(configs, fmt.Sprintf("host=%s", vmess.TransportConfig.Host))
}
default:
logger.Info("Loon Unknown transport type: ", logger.Field("transport", vmess.Transport))
return ""
}
if vmess.Security == "tls" {
configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vmess.SecurityConfig.SNI))
if vmess.SecurityConfig.AllowInsecure {
configs = append(configs, "skip-cert-verify=true")
} else {
configs = append(configs, "skip-cert-verify=false")
}
}
uri := strings.Join(configs, ",")
return uri + "\r\n"
}
-114
View File
@@ -1,114 +0,0 @@
package proxy
// Adapter represents a proxy adapter
type Adapter struct {
Proxies []Proxy
Group []Group
Rules []string
Region []string
}
// Proxy represents a proxy server
type Proxy struct {
Name string
Server string
Port int
Protocol string
Country string
Option any
}
// Group represents a group of proxies
type Group struct {
Name string
Type GroupType
Proxies []string
URL string
Interval int
}
type GroupType string
const (
GroupTypeSelect GroupType = "select"
GroupTypeURLTest GroupType = "url-test"
GroupTypeFallback GroupType = "fallback"
)
// Shadowsocks represents a Shadowsocks proxy configuration
type Shadowsocks struct {
Port int `json:"port"`
Method string `json:"method"`
ServerKey string `json:"server_key"`
}
// Vless represents a Vless proxy configuration
type Vless struct {
Port int `json:"port"`
Flow string `json:"flow"`
Transport string `json:"transport"`
TransportConfig TransportConfig `json:"transport_config"`
Security string `json:"security"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// Vmess represents a Vmess proxy configuration
type Vmess struct {
Port int `json:"port"`
Flow string `json:"flow"`
Transport string `json:"transport"`
TransportConfig TransportConfig `json:"transport_config"`
Security string `json:"security"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// Trojan represents a Trojan proxy configuration
type Trojan struct {
Port int `json:"port"`
Flow string `json:"flow"`
Transport string `json:"transport"`
TransportConfig TransportConfig `json:"transport_config"`
Security string `json:"security"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// Hysteria2 represents a Hysteria2 proxy configuration
type Hysteria2 struct {
Port int `json:"port"`
HopPorts string `json:"hop_ports"`
HopInterval int `json:"hop_interval"`
ObfsPassword string `json:"obfs_password"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// Tuic represents a Tuic proxy configuration
type Tuic struct {
Port int `json:"port"`
SecurityConfig SecurityConfig `json:"security_config"`
}
// TransportConfig represents the transport configuration for a proxy
type TransportConfig struct {
Path string `json:"path,omitempty"` // ws/httpupgrade
Host string `json:"host,omitempty"`
ServiceName string `json:"service_name"` // grpc
}
// SecurityConfig represents the security configuration for a proxy
type SecurityConfig struct {
SNI string `json:"sni"`
AllowInsecure bool `json:"allow_insecure"`
Fingerprint string `json:"fingerprint"`
RealityServerAddr string `json:"reality_server_addr"`
RealityServerPort int `json:"reality_server_port"`
RealityPrivateKey string `json:"reality_private_key"`
RealityPublicKey string `json:"reality_public_key"`
RealityShortId string `json:"reality_short_id"`
}
// Relay represents a relay configuration
type Relay struct {
RelayHost string
DispatchMode string
Prefix string
}
-22
View File
@@ -1,22 +0,0 @@
package quantumultx
import (
"encoding/base64"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func BuildQuantumultX(servers []proxy.Proxy, uuid string) string {
var uri string
for _, s := range servers {
switch s.Protocol {
case "vmess":
uri += buildVmess(s, uuid)
case "shadowsocks":
uri += buildShadowsocks(s, uuid)
case "trojan":
uri += buildTrojan(s, uuid)
}
}
return base64.StdEncoding.EncodeToString([]byte(uri))
}
-94
View File
@@ -1,94 +0,0 @@
package quantumultx
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func createVMess() proxy.Proxy {
return proxy.Proxy{
Name: "Vmess",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "vmess",
Option: proxy.Vmess{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "test.xx.com",
},
Security: "none",
},
}
}
func createSS() proxy.Proxy {
return proxy.Proxy{
Name: "Shadowsocks",
Server: "test.xxxx.com",
Port: 10301,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Port: 10301,
Method: "aes-256-gcm",
ServerKey: "123456",
},
}
}
func createTrojan() proxy.Proxy {
return proxy.Proxy{
Name: "Trojan",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "trojan",
Option: proxy.Trojan{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "baidu.com",
},
SecurityConfig: proxy.SecurityConfig{
SNI: "baidu.com",
AllowInsecure: true,
},
},
}
}
func TestVmess(t *testing.T) {
s := createVMess()
vmess := buildVmess(s, "uuid")
t.Log(vmess)
// output:
// vmess=127.0.0.1:13002,method=chacha20-poly1305,password=uuid,fast-open=true,udp-relay=true,tag=Vmess,tls-verification=true,obfs-uri=/ws,obfs-host=baidu.com
}
func TestShadowsocks(t *testing.T) {
s := createSS()
shadowsocks := buildShadowsocks(s, "uuid")
t.Log(shadowsocks)
// output:
// shadowsocks=127.0.0.1:10301,method=aes-256-gcm,password=uuid,fast-open=true,udp-relay=true,tag=Shadowsocks
}
func TestTrojan(t *testing.T) {
s := createTrojan()
trojan := buildTrojan(s, "password")
t.Log(trojan)
// output:
// trojan=192.168.0.1:13002,password=password,fast-open=true,udp-relay=true,tag=Trojan,obfs=wss,obfs-uri=ws,obfs-host=baidu.com
}
func TestBuildQuantumultX(t *testing.T) {
var servers []proxy.Proxy
uri := BuildQuantumultX(servers, "uuid")
t.Log(uri)
// output:
// c2hhZG93c29ja3M9MTI3LjAuMC4xOjEwMzAxLG1ldGhvZD1hZXMtMjU2LWdjbSxwYXNzd29yZD11dWlkLGZhc3Qtb3Blbj10cnVlLHVkcC1yZWxheT10cnVlLHRhZz1TaGFkb3dzb2Nrcw0KdHJvamFuPTE5Mi4xNjguMC4xOjEzMDAyLHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVRyb2phbixvYmZzPXdzcyxvYmZzLXVyaT13cyxvYmZzLWhvc3Q9YmFpZHUuY29tDQp2bWVzcz0xMjcuMC4wLjE6MTMwMDIsbWV0aG9kPWNoYWNoYTIwLXBvbHkxMzA1LHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVZtZXNzLHRscy12ZXJpZmljYXRpb249dHJ1ZSxvYmZzLXVyaT0vd3Msb2Jmcy1ob3N0PWJhaWR1LmNvbQ0K
}
-23
View File
@@ -1,23 +0,0 @@
package quantumultx
import (
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildShadowsocks(data proxy.Proxy, uuid string) string {
ss := data.Option.(proxy.Shadowsocks)
addr := fmt.Sprintf("%s:%d", data.Server, data.Port)
config := []string{
addr,
fmt.Sprintf("method=%s", ss.Method),
fmt.Sprintf("password=%s", uuid),
"fast-open=true",
"udp-relay=true",
fmt.Sprintf("tag=%s", data.Name),
}
return strings.Join(config, ",") + "\r\n"
}
-39
View File
@@ -1,39 +0,0 @@
package quantumultx
import (
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
// 生成 Trojan 配置
func buildTrojan(data proxy.Proxy, password string) string {
trojan := data.Option.(proxy.Trojan)
addr := fmt.Sprintf("trojan=%s:%d", data.Server, data.Port)
config := []string{
addr,
fmt.Sprintf("password=%s", password),
"fast-open=true",
"udp-relay=true",
fmt.Sprintf("tag=%s", data.Name),
}
if trojan.Transport == "websocket" {
config = append(config, "obfs=wss")
if trojan.TransportConfig.Path != "" {
config = append(config, fmt.Sprintf("obfs-uri=%s", trojan.TransportConfig.Path))
}
if trojan.TransportConfig.Host != "" {
config = append(config, fmt.Sprintf("obfs-host=%s", trojan.TransportConfig.Host))
}
} else {
config = append(config, "over-tls=true")
if trojan.SecurityConfig.SNI != "" {
config = append(config, fmt.Sprintf("tls-host=%s", trojan.SecurityConfig.SNI))
}
}
return strings.Join(config, ",") + "\r\n"
}
-45
View File
@@ -1,45 +0,0 @@
package quantumultx
import (
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildVmess(data proxy.Proxy, uuid string) string {
vmess := data.Option.(proxy.Vmess)
addr := fmt.Sprintf("vmess=%s:%d", data.Server, data.Port)
var host string
uriConfig := []string{
addr,
"method=chacha20-poly1305",
fmt.Sprintf("password=%s", uuid),
"fast-open=true",
"udp-relay=true",
fmt.Sprintf("tag=%s", data.Name),
}
if vmess.Security == "tls" {
if vmess.Transport == "tcp" {
uriConfig = append(uriConfig, "obfs=over-tls")
}
if vmess.SecurityConfig.AllowInsecure {
uriConfig = append(uriConfig, "tls-verification=true")
} else {
uriConfig = append(uriConfig, "tls-verification=false")
}
if vmess.SecurityConfig.SNI != "" {
host = vmess.SecurityConfig.SNI
}
}
if vmess.Transport == "websocket" {
uriConfig = append(uriConfig, fmt.Sprintf("obfs-uri=%s", vmess.TransportConfig.Path))
host = vmess.TransportConfig.Host
}
if host != "" {
uriConfig = append(uriConfig, fmt.Sprintf("obfs-host=%s", host))
}
return strings.Join(uriConfig, ",") + "\r\n"
}
-48
View File
@@ -1,48 +0,0 @@
package shadowrocket
import (
"fmt"
"time"
"github.com/perfect-panel/ppanel-server/pkg/adapter/general"
"encoding/base64"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/traffic"
)
type UserInfo struct {
Upload int64
Download int64
TotalTraffic int64
ExpiredDate time.Time
}
func BuildShadowrocket(servers []proxy.Proxy, uuid string, userinfo UserInfo) []byte {
upload := traffic.AutoConvert(userinfo.Upload, false)
download := traffic.AutoConvert(userinfo.Download, false)
total := traffic.AutoConvert(userinfo.TotalTraffic, false)
expiredAt := userinfo.ExpiredDate.Format("2006-01-02 15:04:05")
uri := fmt.Sprintf("STATUS=🚀↑:%s,↓:%s,TOT:%s💡Expires:%s\r\n", upload, download, total, expiredAt)
for _, s := range servers {
switch s.Protocol {
case "vmess":
uri += buildVmess(s, uuid)
case "shadowsocks":
uri += general.ShadowsocksUri(s, uuid) + "\r\n"
case "trojan":
uri += general.TrojanUri(s, uuid) + "\r\n"
case "vless":
uri += general.VlessUri(s, uuid) + "\r\n"
case "hysteria2":
uri += general.Hysteria2Uri(s, uuid) + "\r\n"
case "tuic":
uri += general.TuicUri(s, uuid) + "\r\n"
default:
continue
}
}
return []byte(base64.StdEncoding.EncodeToString([]byte(uri)))
}
@@ -1,76 +0,0 @@
package shadowrocket
import (
"testing"
"time"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func createVMess() proxy.Proxy {
return proxy.Proxy{
Name: "Vmess",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "vmess",
Option: proxy.Vmess{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "test.xx.com",
},
Security: "none",
},
}
}
func createSS() proxy.Proxy {
return proxy.Proxy{
Name: "Shadowsocks",
Server: "test.xxxx.com",
Port: 10301,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Port: 10301,
Method: "aes-256-gcm",
ServerKey: "123456",
},
}
}
func createTrojan() proxy.Proxy {
return proxy.Proxy{
Name: "Trojan",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "trojan",
Option: proxy.Trojan{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "baidu.com",
},
SecurityConfig: proxy.SecurityConfig{
SNI: "baidu.com",
AllowInsecure: true,
},
},
}
}
func TestBuildShadowrocket(t *testing.T) {
s := []proxy.Proxy{
createVMess(),
createSS(),
createTrojan(),
}
uri := BuildShadowrocket(s, "uuid", UserInfo{
Upload: 1024,
Download: 1024,
TotalTraffic: 2048,
ExpiredDate: time.Now().AddDate(0, 0, 1),
})
t.Log(string(uri))
}
-57
View File
@@ -1,57 +0,0 @@
package shadowrocket
import (
"fmt"
"strings"
"encoding/base64"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildVmess(data proxy.Proxy, uuid string) string {
vmess := data.Option.(proxy.Vmess)
userinfo := fmt.Sprintf("auto:%s@%s:%d", uuid, data.Server, data.Port)
// 准备 config,使用默认值
config := map[string]interface{}{
"tfo": 1,
"remark": data.Name,
"alterId": 0,
}
// tls 配置
if vmess.Security == "tls" {
config["tls"] = 1
if vmess.SecurityConfig.AllowInsecure {
config["allowInsecure"] = 1
}
if vmess.SecurityConfig.SNI != "" {
config["peer"] = vmess.SecurityConfig.SNI
}
}
// transport 配置
switch vmess.Transport {
case "websocket":
config["obfs"] = "websocket"
if vmess.TransportConfig.Path != "" {
config["path"] = vmess.TransportConfig.Path
}
if vmess.TransportConfig.Host != "" {
config["obfsParam"] = vmess.TransportConfig.Host
}
case "grpc":
config["obfs"] = "grpc"
if vmess.TransportConfig.ServiceName != "" {
config["path"] = vmess.TransportConfig.ServiceName
}
}
query := make([]string, 0)
for k, v := range config {
query = append(query, fmt.Sprintf("%s=%v", k, v))
}
queryStr := strings.Join(query, "&")
uri := fmt.Sprintf("vmess://%s?%s\r\n", base64.StdEncoding.EncodeToString([]byte(userinfo)), queryStr)
return uri
}
-201
View File
@@ -1,201 +0,0 @@
package singbox
import (
"encoding/json"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/logger"
)
func BuildSingbox(adapter proxy.Adapter, uuid string) ([]byte, error) {
// build outbounds type is Proxy
var proxies []Proxy
// build outbound group
for _, group := range adapter.Group {
if group.Type == proxy.GroupTypeSelect {
selector := Proxy{
Type: Selector,
Tag: group.Name,
SelectorOptions: &SelectorOutboundOptions{
OutboundOptions: OutboundOptions{
Tag: group.Name,
Type: Selector,
},
Outbounds: group.Proxies,
Default: group.Proxies[0],
InterruptExistConnections: false,
},
}
proxies = append(proxies, selector)
} else if group.Type == proxy.GroupTypeURLTest {
selector := Proxy{
Type: URLTest,
Tag: group.Name,
URLTestOptions: &URLTestOutboundOptions{
OutboundOptions: OutboundOptions{
Tag: group.Name,
Type: URLTest,
},
Outbounds: group.Proxies,
URL: group.URL,
},
}
proxies = append(proxies, selector)
} else {
logger.Errorf("[sing-box] Unknown group type: %s, group name: %s", group.Type, group.Name)
}
}
// build outbounds
for _, data := range adapter.Proxies {
p := buildProxy(data, uuid)
if p == nil {
continue
}
proxies = append(proxies, *p)
}
// add direct outbound
direct := Proxy{
Type: Direct,
Tag: "DIRECT",
}
// add block outbound
block := Proxy{
Type: Block,
Tag: "block",
}
// add dns outbound
dns := Proxy{
Type: DNS,
Tag: "dns-out",
}
proxies = append(proxies, direct, block, dns)
var rawConfig map[string]any
if err := json.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil {
return nil, err
}
rawConfig["outbounds"] = proxies
route := RouteOptions{
Final: "手动选择",
Rules: []Rule{
{
Inbound: []string{
"tun-in",
"mixed-in",
},
Action: "sniff",
},
{
Type: "logical",
Mode: "or",
Rules: []Rule{
{
Port: []uint16{53},
},
{
Protocol: []string{"dns"},
},
},
Action: "hijack-dns",
},
{
RuleSet: []string{
"geosite-category-ads-all",
},
ClashMode: "rule",
Action: "reject",
},
{
ClashMode: "direct",
Outbound: "DIRECT",
},
{
ClashMode: "global",
Outbound: "手动选择",
},
{
IPIsPrivate: true,
Outbound: "DIRECT",
},
{
RuleSet: []string{
"geosite-private",
},
Outbound: "DIRECT",
},
},
RuleSet: []RuleSet{
{
Tag: "geoip-cn",
Type: "remote",
Format: "binary",
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs",
DownloadDetour: "DIRECT",
},
{
Tag: "geosite-cn",
Type: "remote",
Format: "binary",
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs",
DownloadDetour: "DIRECT",
},
{
Tag: "geosite-private",
Type: "remote",
Format: "binary",
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs",
DownloadDetour: "DIRECT",
},
{
Tag: "geosite-category-ads-all",
Type: "remote",
Format: "binary",
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs",
DownloadDetour: "DIRECT",
},
{
Tag: "geosite-geolocation-!cn",
Type: "remote",
Format: "binary",
URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs",
DownloadDetour: "DIRECT",
},
},
AutoDetectInterface: true,
}
route.Rules = append(route.Rules, adapterToSingboxRule(adapter.Rules)...)
rawConfig["route"] = route
return json.Marshal(rawConfig)
}
func buildProxy(data proxy.Proxy, uuid string) *Proxy {
var p *Proxy
var err error
switch data.Protocol {
case VLESS:
p, err = ParseVless(data, uuid)
case Shadowsocks:
p, err = ParseShadowsocks(data, uuid)
case Trojan:
p, err = ParseTrojan(data, uuid)
case VMess:
p, err = ParseVMess(data, uuid)
case Hysteria2:
p, err = ParseHysteria2(data, uuid)
case TUIC:
p, err = ParseTUIC(data, uuid)
default:
logger.Error("Unknown protocol", logger.Field("protocol", data.Protocol), logger.Field("server", data.Name))
}
if err != nil {
logger.Error("ParseVless", logger.Field("error", err.Error()), logger.Field("server", data.Name), logger.Field("protocol", data.Protocol))
return nil
}
return p
}
-100
View File
@@ -1,100 +0,0 @@
package singbox
const DefaultTemplate = `
{
"log": {
"level": "info",
"timestamp": true
},
"experimental": {
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "ui",
"secret": "",
"external_ui_download_url": "https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
"external_ui_download_detour": "direct",
"default_mode": "rule"
},
"cache_file": {
"enabled": true,
"store_fakeip": false
}
},
"dns": {
"servers": [
{
"tag": "dns_proxy",
"address": "tls://8.8.8.8",
"detour": "手动选择"
},
{
"tag": "dns_direct",
"address": "https://223.5.5.5/dns-query",
"detour": "DIRECT"
}
],
"rules": [
{
"outbound": "any",
"server": "dns_direct",
"disable_cache": true
},
{
"rule_set": "geosite-cn",
"server": "dns_direct"
},
{
"clash_mode": "direct",
"server": "dns_direct"
},
{
"clash_mode": "global",
"server": "dns_proxy"
},
{
"rule_set": "geosite-geolocation-!cn",
"server": "dns_proxy"
}
],
"final": "dns_direct",
"strategy": "ipv4_only"
},
"route": {
"rules": [
{
"action": "sniff"
},
{
"protocol": "dns",
"action": "hijack-dns"
}
]
},
"inbounds": [
{
"tag": "tun-in",
"type": "tun",
"address": [
"172.18.0.1/30",
"fdfe:dcba:9876::1/126"
],
"auto_route": true,
"strict_route": true,
"stack": "system",
"platform": {
"http_proxy": {
"enabled": true,
"server": "127.0.0.1",
"server_port": 7890
}
}
},
{
"tag": "mixed-in",
"type": "mixed",
"listen": "127.0.0.1",
"listen_port": 7890
}
]
}
`
-76
View File
@@ -1,76 +0,0 @@
package singbox
import (
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
type Hysteria2Obfs struct {
Type string `json:"type,omitempty"`
Password string `json:"password,omitempty"`
}
type Hysteria2OutboundOptions struct {
ServerOptions
ServerPorts []string `json:"server_ports,omitempty"`
HopInterval int `json:"hop_interval,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
Password string `json:"password,omitempty"`
Network string `json:"network,omitempty"`
OutboundTLSOptionsContainer
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`
}
func ParseHysteria2(data proxy.Proxy, password string) (*Proxy, error) {
hysteria2 := data.Option.(proxy.Hysteria2)
p := &Proxy{
Tag: data.Name,
Type: Hysteria2,
Hysteria2Options: &Hysteria2OutboundOptions{
ServerOptions: ServerOptions{
Tag: data.Name,
Type: Hysteria2,
Server: data.Server,
},
Password: password,
},
}
var ports []string
if hysteria2.HopPorts != "" {
ps := strings.Split(hysteria2.HopPorts, ",")
for _, port := range ps {
// 舍弃单个端口,只保留端口范围
if len(strings.Split(port, "-")) > 1 {
tmp := strings.Split(port, "-")
ports = append(ports, strings.Join(tmp, ":"))
}
}
}
if len(ports) > 0 {
p.Hysteria2Options.ServerPorts = ports
p.Hysteria2Options.HopInterval = hysteria2.HopInterval
} else {
p.Hysteria2Options.ServerPort = data.Port
}
if hysteria2.ObfsPassword != "" {
p.Hysteria2Options.Obfs = &Hysteria2Obfs{
Type: "salamander",
Password: hysteria2.ObfsPassword,
}
}
var tls *OutboundTLSOptions
if hysteria2.SecurityConfig.SNI != "" {
tls = NewOutboundTLSOptions("tls", hysteria2.SecurityConfig)
}
p.Hysteria2Options.TLS = tls
return p, nil
}
-17
View File
@@ -1,17 +0,0 @@
package singbox
type OutboundMultiplexOptions struct {
Enabled bool `json:"enabled,omitempty"`
Protocol string `json:"protocol,omitempty"`
MaxConnections int `json:"max_connections,omitempty"`
MinStreams int `json:"min_streams,omitempty"`
MaxStreams int `json:"max_streams,omitempty"`
Padding bool `json:"padding,omitempty"`
Brutal *BrutalOptions `json:"brutal,omitempty"`
}
type BrutalOptions struct {
Enabled bool `json:"enabled,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
}
-130
View File
@@ -1,130 +0,0 @@
package singbox
import (
"strconv"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/rules"
)
type Rule struct {
Outbound string `json:"outbound,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
RuleSet []string `json:"rule_set,omitempty"`
Domain []string `json:"domain,omitempty"`
DomainSuffix []string `json:"domain_suffix,omitempty"`
DomainKeyword []string `json:"domain_keyword,omitempty"`
DomainRegex []string `json:"domain_regex,omitempty"`
GeoIP []string `json:"geoip,omitempty"`
IPCIDR []string `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
SourceIPCIDR []string `json:"source_ip_cidr,omitempty"`
ProcessName []string `json:"process_name,omitempty"`
ProcessPath []string `json:"process_path,omitempty"`
SourcePort []uint16 `json:"source_port,omitempty"`
Protocol []string `json:"protocol,omitempty"`
Port []uint16 `json:"port,omitempty"`
Action string `json:"action,omitempty"`
Inbound []string `json:"inbound,omitempty"`
Rules []Rule `json:"rules,omitempty"`
Type string `json:"type,omitempty"`
Mode string `json:"mode,omitempty"`
}
type RuleSet struct {
Tag string `json:"tag,omitempty"`
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
URL string `json:"url,omitempty"`
DownloadDetour string `json:"download_detour,omitempty"`
}
func adapterToSingboxRule(texts []string) []Rule {
var rulesList []Rule
for _, rule := range texts {
r := rules.NewRule(rule, "")
if r == nil {
continue
}
rulesList = addRuleToItem(rulesList, r.Target, *r)
}
return rulesList
}
func addRuleToItem(group []Rule, outbound string, rule rules.Rule) []Rule {
for i := range group {
if group[i].Outbound == outbound {
switch rules.ParseRuleType(rule.Type) {
case rules.Domain:
group[i].Domain = append(group[i].Domain, rule.Payload)
return group
case rules.DomainSuffix:
group[i].DomainSuffix = append(group[i].DomainSuffix, rule.Payload)
return group
case rules.DomainKeyword:
group[i].DomainKeyword = append(group[i].DomainKeyword, rule.Payload)
return group
case rules.IPCIDR:
group[i].IPCIDR = append(group[i].IPCIDR, rule.Payload)
return group
case rules.SrcIPCIDR:
group[i].SourceIPCIDR = append(group[i].SourceIPCIDR, rule.Payload)
return group
case rules.SrcPort:
port, err := strconv.ParseUint(rule.Payload, 10, 16)
if err != nil {
logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload)
return group
}
group[i].SourcePort = append(group[i].SourcePort, uint16(port))
return group
case rules.GEOIP:
group[i].GeoIP = append(group[i].GeoIP, rule.Payload)
return group
case rules.Process:
group[i].ProcessName = append(group[i].ProcessName, rule.Payload)
return group
case rules.ProcessPath:
group[i].ProcessPath = append(group[i].ProcessPath, rule.Payload)
return group
default:
logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type)
return group
}
}
}
newRule := Rule{
Outbound: outbound,
}
switch rules.ParseRuleType(rule.Type) {
case rules.Domain:
newRule.Domain = []string{rule.Payload}
case rules.DomainSuffix:
newRule.DomainSuffix = []string{rule.Payload}
case rules.DomainKeyword:
newRule.DomainKeyword = []string{rule.Payload}
case rules.IPCIDR:
newRule.IPCIDR = []string{rule.Payload}
case rules.SrcIPCIDR:
newRule.SourceIPCIDR = []string{rule.Payload}
case rules.SrcPort:
port, err := strconv.ParseUint(rule.Payload, 10, 16)
if err != nil {
logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload)
return group
}
newRule.SourcePort = []uint16{uint16(port)}
case rules.GEOIP:
newRule.GeoIP = []string{rule.Payload}
case rules.Process:
newRule.ProcessName = []string{rule.Payload}
case rules.ProcessPath:
newRule.ProcessPath = []string{rule.Payload}
default:
logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type)
return group
}
group = append(group, newRule)
return group
}
-15
View File
@@ -1,15 +0,0 @@
package singbox
import (
"fmt"
"testing"
)
func TestAdapterToSingboxRule(t *testing.T) {
rules := []string{
"DOMAIN,example.com,DIRECT",
"DOMAIN-SUFFIX,google.com,智能线路",
}
result := adapterToSingboxRule(rules)
fmt.Printf("TestAdapterToSingboxRule: result: %+v\n", result)
}
-34
View File
@@ -1,34 +0,0 @@
package singbox
import (
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
type ShadowsocksOptions struct {
ServerOptions
Method string `json:"method,omitempty"`
Password string `json:"password,omitempty"`
Plugin string `json:"plugin,omitempty"`
PluginOptions string `json:"plugin_opts,omitempty"`
Network string `json:"network,omitempty"`
}
func ParseShadowsocks(data proxy.Proxy, uuid string) (*Proxy, error) {
config := data.Option.(proxy.Shadowsocks)
p := &Proxy{
Tag: data.Name,
Type: Shadowsocks,
ShadowsocksOptions: &ShadowsocksOptions{
ServerOptions: ServerOptions{
Tag: data.Name,
Type: Shadowsocks,
Server: data.Server,
ServerPort: data.Port,
},
Method: config.Method,
Password: uuid,
Network: "tcp",
},
}
return p, nil
}
-98
View File
@@ -1,98 +0,0 @@
package singbox
import (
"encoding/json"
"fmt"
)
const (
Trojan = "trojan"
VLESS = "vless"
VMess = "vmess"
TUIC = "tuic"
Hysteria2 = "hysteria2"
Shadowsocks = "shadowsocks"
Selector = "selector"
URLTest = "urltest"
Direct = "direct"
Block = "block"
DNS = "dns"
)
type Proxy struct {
Tag string `json:"tag,omitempty"`
Type string `json:"type"`
ShadowsocksOptions *ShadowsocksOptions `json:"-"`
TUICOptions *TUICOutboundOptions `json:"-"`
TrojanOptions *TrojanOutboundOptions `json:"-"`
VLESSOptions *VLESSOutboundOptions `json:"-"`
VMessOptions *VMessOutboundOptions `json:"-"`
Hysteria2Options *Hysteria2OutboundOptions `json:"-"`
SelectorOptions *SelectorOutboundOptions `json:"-"`
URLTestOptions *URLTestOutboundOptions `json:"-"`
}
type ServerOptions struct {
Tag string `json:"tag"`
Type string `json:"type"`
Server string `json:"server"`
ServerPort int `json:"server_port,omitempty"`
}
type OutboundOptions struct {
Tag string `json:"tag"`
Type string `json:"type"`
}
type SelectorOutboundOptions struct {
OutboundOptions
Outbounds []string `json:"outbounds"`
Default string `json:"default,omitempty"`
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
}
type URLTestOutboundOptions struct {
OutboundOptions
Outbounds []string `json:"outbounds"`
URL string `json:"url,omitempty"`
Interval Duration `json:"interval,omitempty"`
Tolerance uint16 `json:"tolerance,omitempty"`
IdleTimeout Duration `json:"idle_timeout,omitempty"`
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
}
type RouteOptions struct {
Rules []Rule `json:"rules,omitempty"`
Final string `json:"final,omitempty"`
RuleSet []RuleSet `json:"rule_set,omitempty"`
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
}
func (p Proxy) MarshalJSON() ([]byte, error) {
type Alias Proxy
aux := struct {
Alias
}{
Alias: (Alias)(p),
}
switch p.Type {
case Shadowsocks:
return json.Marshal(p.ShadowsocksOptions)
case TUIC:
return json.Marshal(p.TUICOptions)
case Trojan:
return json.Marshal(p.TrojanOptions)
case VLESS:
return json.Marshal(p.VLESSOptions)
case VMess:
return json.Marshal(p.VMessOptions)
case Hysteria2:
return json.Marshal(p.Hysteria2Options)
case Selector:
return json.Marshal(p.SelectorOptions)
case URLTest:
return json.Marshal(p.URLTestOptions)
case Direct, Block, DNS:
return json.Marshal(aux.Alias)
default:
return nil, fmt.Errorf("[sing-box] MarshalJSON unknown type: %s", p.Type)
}
}
-80
View File
@@ -1,80 +0,0 @@
package singbox
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/stretchr/testify/assert"
)
func createSS() proxy.Proxy {
c := proxy.Shadowsocks{
Method: "aes-256-gcm",
Port: 10301,
ServerKey: "",
}
return proxy.Proxy{
Name: "Shadowsocks",
Server: "127.0.0.1",
Port: 10301,
Protocol: "shadowsocks",
Option: c,
}
}
func createVLESS() proxy.Proxy {
c := proxy.Vless{
Port: 10301,
Flow: "xtls-rprx-direct",
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "baidu.com",
},
Security: "tls",
SecurityConfig: proxy.SecurityConfig{
SNI: "baidu.com",
Fingerprint: "chrome",
AllowInsecure: true,
},
}
s := proxy.Proxy{
Name: "VLESS",
Server: "test.xxx.com",
Port: 10301,
Protocol: "vless",
Option: c,
}
return s
}
func TestSingboxShadowsocks(t *testing.T) {
s := createSS()
p, err := ParseShadowsocks(s, "uuid")
if err != nil {
t.Fatal(err)
}
data, err := p.MarshalJSON()
if err != nil {
t.Fatal(err)
}
assert.NotEqual(t, 0, len(data))
// Output:
// proxy: proxy: {"tag":"Shadowsocks","type":"shadowsocks","server":"127.0.0.1","server_port":10301,"method":"aes-256-gcm","password":"uuid","network":"tcp"}
}
func TestSingboxVless(t *testing.T) {
s := createVLESS()
p, err := ParseVless(s, "uuid")
if err != nil {
t.Fatal(err)
}
data, err := p.MarshalJSON()
if err != nil {
t.Fatal(err)
}
assert.NotEqual(t, 0, len(data))
}
-87
View File
@@ -1,87 +0,0 @@
package singbox
import (
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
type OutboundTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
DisableSNI bool `json:"disable_sni,omitempty"`
ServerName string `json:"server_name,omitempty"`
Insecure bool `json:"insecure,omitempty"`
ALPN Listable[string] `json:"alpn,omitempty"`
MinVersion string `json:"min_version,omitempty"`
MaxVersion string `json:"max_version,omitempty"`
CipherSuites Listable[string] `json:"cipher_suites,omitempty"`
Certificate Listable[string] `json:"certificate,omitempty"`
CertificatePath string `json:"certificate_path,omitempty"`
ECH *OutboundECHOptions `json:"ech,omitempty"`
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
Reality *OutboundRealityOptions `json:"reality,omitempty"`
}
func NewOutboundTLSOptions(security string, cfg proxy.SecurityConfig) *OutboundTLSOptions {
var tls = &OutboundTLSOptions{}
switch security {
case "none":
return nil
case "tls":
tls.Enabled = true
if cfg.SNI != "" {
tls.ServerName = cfg.SNI
} else {
tls.DisableSNI = true
}
tls.Insecure = cfg.AllowInsecure
if cfg.Fingerprint != "" {
tls.UTLS = &OutboundUTLSOptions{
Enabled: true,
Fingerprint: cfg.Fingerprint,
}
}
case "reality":
tls.Enabled = true
if cfg.SNI != "" {
tls.ServerName = cfg.SNI
} else {
tls.DisableSNI = true
}
tls.Insecure = cfg.AllowInsecure
if cfg.Fingerprint != "" {
tls.UTLS = &OutboundUTLSOptions{
Enabled: true,
Fingerprint: cfg.Fingerprint,
}
}
tls.Reality = &OutboundRealityOptions{
Enabled: true,
PublicKey: cfg.RealityPublicKey,
ShortID: cfg.RealityShortId,
}
}
return tls
}
type OutboundECHOptions struct {
Enabled bool `json:"enabled,omitempty"`
PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"`
DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"`
Config Listable[string] `json:"config,omitempty"`
ConfigPath string `json:"config_path,omitempty"`
}
type OutboundRealityOptions struct {
Enabled bool `json:"enabled,omitempty"`
PublicKey string `json:"public_key,omitempty"`
ShortID string `json:"short_id,omitempty"`
}
type OutboundUTLSOptions struct {
Enabled bool `json:"enabled,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
}
type Listable[T any] []T
type OutboundTLSOptionsContainer struct {
TLS *OutboundTLSOptions `json:"tls,omitempty"`
}
-11
View File
@@ -1,11 +0,0 @@
package singbox
import "encoding/json"
func mergeOptions(target map[string]any, options any) error {
optionsJSON, err := json.Marshal(options)
if err != nil {
return err
}
return json.Unmarshal(optionsJSON, &target)
}
-39
View File
@@ -1,39 +0,0 @@
package singbox
import (
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
type TrojanOutboundOptions struct {
ServerOptions
Password string `json:"password"`
Network string `json:"network,omitempty"`
OutboundTLSOptionsContainer
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`
}
func ParseTrojan(data proxy.Proxy, uuid string) (*Proxy, error) {
trojan := data.Option.(proxy.Trojan)
p := &Proxy{
Tag: data.Name,
Type: Trojan,
TrojanOptions: &TrojanOutboundOptions{
ServerOptions: ServerOptions{
Tag: data.Name,
Type: Trojan,
Server: data.Server,
ServerPort: data.Port,
},
Password: uuid,
},
}
// Transport options
transport := NewV2RayTransportOptions(trojan.Transport, trojan.TransportConfig)
p.TrojanOptions.Transport = transport
// Security options
p.TrojanOptions.TLS = NewOutboundTLSOptions(trojan.Security, trojan.SecurityConfig)
return p, nil
}
-40
View File
@@ -1,40 +0,0 @@
package singbox
import (
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
type TUICOutboundOptions struct {
ServerOptions
UUID string `json:"uuid,omitempty"`
Password string `json:"password,omitempty"`
CongestionControl string `json:"congestion_control,omitempty"`
UDPRelayMode string `json:"udp_relay_mode,omitempty"`
UDPOverStream bool `json:"udp_over_stream,omitempty"`
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
Heartbeat string `json:"heartbeat,omitempty"`
Network string `json:"network,omitempty"`
OutboundTLSOptionsContainer
}
func ParseTUIC(data proxy.Proxy, uuid string) (*Proxy, error) {
tuic := data.Option.(proxy.Tuic)
p := &Proxy{
Tag: data.Name,
Type: TUIC,
TUICOptions: &TUICOutboundOptions{
ServerOptions: ServerOptions{
Tag: data.Name,
Type: TUIC,
Server: data.Server,
ServerPort: data.Port,
},
UUID: uuid,
Password: uuid,
CongestionControl: "bbr",
},
}
// Security options
p.TUICOptions.TLS = NewOutboundTLSOptions("tls", tuic.SecurityConfig)
return p, nil
}
-114
View File
@@ -1,114 +0,0 @@
package singbox
import (
"encoding/json"
"time"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
type V2RayTransportOptions struct {
Type string `json:"type"`
HTTPOptions V2RayHTTPOptions `json:"-"`
WebsocketOptions V2RayWebsocketOptions `json:"-"`
QUICOptions V2RayQUICOptions `json:"-"`
GRPCOptions V2RayGRPCOptions `json:"-"`
HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"`
}
func (v V2RayTransportOptions) MarshalJSON() ([]byte, error) {
var v2rayTransportOptions any
data := map[string]any{
"type": v.Type,
}
switch v.Type {
case "http":
v2rayTransportOptions = v.HTTPOptions
case "ws":
v2rayTransportOptions = v.WebsocketOptions
case "quic":
v2rayTransportOptions = v.QUICOptions
case "grpc":
v2rayTransportOptions = v.GRPCOptions
case "httpupgrade":
v2rayTransportOptions = v.HTTPUpgradeOptions
}
if err := mergeOptions(data, v2rayTransportOptions); err != nil {
return nil, err
}
return json.Marshal(data)
}
func NewV2RayTransportOptions(network string, transport proxy.TransportConfig) *V2RayTransportOptions {
var t *V2RayTransportOptions = nil
switch network {
case "websocket":
t = &V2RayTransportOptions{
Type: "ws",
WebsocketOptions: V2RayWebsocketOptions{
Path: transport.Path,
Headers: map[string]Listable[string]{
"Host": []string{transport.Host},
},
MaxEarlyData: 2048,
EarlyDataHeaderName: "Sec-WebSocket-Protocol",
},
}
case "httpupgrade":
t = &V2RayTransportOptions{
Type: "httpupgrade",
HTTPOptions: V2RayHTTPOptions{
Path: transport.Path,
Host: []string{transport.Host},
Headers: map[string]Listable[string]{
"Host": []string{transport.Host},
},
},
}
case "grpc":
t = &V2RayTransportOptions{
Type: "grpc",
GRPCOptions: V2RayGRPCOptions{
ServiceName: transport.ServiceName,
},
}
}
return t
}
type V2RayHTTPOptions struct {
Host Listable[string] `json:"host,omitempty"`
Path string `json:"path,omitempty"`
Method string `json:"method,omitempty"`
Headers HTTPHeader `json:"headers,omitempty"`
IdleTimeout Duration `json:"idle_timeout,omitempty"`
PingTimeout Duration `json:"ping_timeout,omitempty"`
}
type V2RayWebsocketOptions struct {
Path string `json:"path,omitempty"`
Headers HTTPHeader `json:"headers,omitempty"`
MaxEarlyData uint32 `json:"max_early_data,omitempty"`
EarlyDataHeaderName string `json:"early_data_header_name,omitempty"`
}
type V2RayQUICOptions struct{}
type V2RayGRPCOptions struct {
ServiceName string `json:"service_name,omitempty"`
IdleTimeout string `json:"idle_timeout,omitempty"`
PingTimeout string `json:"ping_timeout,omitempty"`
PermitWithoutStream bool `json:"permit_without_stream,omitempty"`
ForceLite bool `json:"-"` // for test
}
type V2RayHTTPUpgradeOptions struct {
Host string `json:"host,omitempty"`
Path string `json:"path,omitempty"`
Headers HTTPHeader `json:"headers,omitempty"`
}
type HTTPHeader map[string]Listable[string]
type Duration time.Duration
-44
View File
@@ -1,44 +0,0 @@
package singbox
import (
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
type VLESSOutboundOptions struct {
ServerOptions
OutboundTLSOptionsContainer
UUID string `json:"uuid"`
Flow string `json:"flow,omitempty"`
Network string `json:"network,omitempty"`
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`
PacketEncoding *string `json:"packet_encoding,omitempty"`
}
func ParseVless(data proxy.Proxy, uuid string) (*Proxy, error) {
vless := data.Option.(proxy.Vless)
packetEncoding := "xudp"
p := &Proxy{
Tag: data.Name,
Type: VLESS,
VLESSOptions: &VLESSOutboundOptions{
ServerOptions: ServerOptions{
Tag: data.Name,
Type: VLESS,
Server: data.Server,
ServerPort: data.Port,
},
UUID: uuid,
Flow: vless.Flow,
PacketEncoding: &packetEncoding,
},
}
// Transport options
transport := NewV2RayTransportOptions(vless.Transport, vless.TransportConfig)
p.VLESSOptions.Transport = transport
// Security options
p.VLESSOptions.TLS = NewOutboundTLSOptions(vless.Security, vless.SecurityConfig)
return p, nil
}
-43
View File
@@ -1,43 +0,0 @@
package singbox
import (
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
type VMessOutboundOptions struct {
ServerOptions
UUID string `json:"uuid"`
Security string `json:"security"`
AlterId int `json:"alter_id,omitempty"`
GlobalPadding bool `json:"global_padding,omitempty"`
AuthenticatedLength bool `json:"authenticated_length,omitempty"`
Network string `json:"network,omitempty"`
PacketEncoding string `json:"packet_encoding,omitempty"`
Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"`
Transport *V2RayTransportOptions `json:"transport,omitempty"`
OutboundTLSOptionsContainer
}
func ParseVMess(data proxy.Proxy, uuid string) (*Proxy, error) {
vmess := data.Option.(proxy.Vmess)
p := &Proxy{
Type: VMess,
VMessOptions: &VMessOutboundOptions{
ServerOptions: ServerOptions{
Tag: data.Name,
Type: VMess,
Server: data.Server,
ServerPort: data.Port,
},
UUID: uuid,
Security: "auto",
AlterId: 0,
},
}
// Transport options
p.VMessOptions.Transport = NewV2RayTransportOptions(vmess.Transport, vmess.TransportConfig)
// Security options
p.VMessOptions.TLS = NewOutboundTLSOptions(vmess.Security, vmess.SecurityConfig)
return p, nil
}
-111
View File
@@ -1,111 +0,0 @@
package surfboard
import (
"bytes"
"embed"
"fmt"
"net/url"
"strings"
"text/template"
"time"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/ppanel-server/pkg/traffic"
)
//go:embed *.tpl
var configFiles embed.FS
var shadowsocksSupportMethod = []string{"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305"}
func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byte {
var proxies, proxyGroup string
for _, node := range servers.Proxies {
if uri := buildProxy(node, user.UUID); uri != "" {
proxies += uri
}
}
for _, group := range servers.Group {
if group.Type == proxy.GroupTypeSelect {
proxyGroup += fmt.Sprintf("%s = select, %s", group.Name, strings.Join(group.Proxies, ", ")) + "\r\n"
} else if group.Type == proxy.GroupTypeURLTest {
proxyGroup += fmt.Sprintf("%s = url-test, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n"
} else if group.Type == proxy.GroupTypeFallback {
proxyGroup += fmt.Sprintf("%s = fallback, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n"
} else {
logger.Errorf("[BuildSurfboard] unknown group type: %s", group.Type)
}
}
var rules string
for _, rule := range servers.Rules {
if rule == "" {
continue
}
rules += rule + "\r\n"
}
//final rule
rules += "# 最终规则" + "\r\n" + "FINAL, 手动选择"
file, err := configFiles.ReadFile("default.tpl")
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
// replace template
tpl, err := template.New("default").Parse(string(file))
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
var buf bytes.Buffer
var expiredAt string
if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) {
expiredAt = "长期有效"
} else {
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
}
// convert traffic
upload := traffic.AutoConvert(user.Upload, false)
download := traffic.AutoConvert(user.Download, false)
total := traffic.AutoConvert(user.TotalTraffic, false)
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
// query Host
urlParse, err := url.Parse(user.SubscribeURL)
if err != nil {
return nil
}
if err := tpl.Execute(&buf, map[string]interface{}{
"Proxies": proxies,
"ProxyGroup": proxyGroup,
"SubscribeURL": user.SubscribeURL,
"SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt),
"SubscribeDomain": urlParse.Host,
"Rules": rules,
}); err != nil {
logger.Errorf("build surfboard config error: %v", err.Error())
return nil
}
return buf.Bytes()
}
func buildProxy(data proxy.Proxy, uuid string) string {
var p string
switch data.Protocol {
case "vmess":
p = buildVMess(data, uuid)
case "shadowsocks":
if !tool.Contains(shadowsocksSupportMethod, data.Option.(proxy.Shadowsocks).Method) {
return ""
}
p = buildShadowsocks(data, uuid)
case "trojan":
p = buildTrojan(data, uuid)
}
return p
}
-24
View File
@@ -1,24 +0,0 @@
package surfboard
import (
"testing"
"time"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/uuidx"
)
func TestBuildSurfboard(t *testing.T) {
siteName := "test"
user := UserInfo{
UUID: uuidx.NewUUID().String(),
Upload: 0,
Download: 0,
TotalTraffic: 0,
ExpiredDate: time.Now().AddDate(0, 1, 1),
SubscribeURL: "https://test.com",
}
conf := BuildSurfboard(proxy.Adapter{}, siteName, user)
t.Log(string(conf))
}
-29
View File
@@ -1,29 +0,0 @@
#!MANAGED-CONFIG {{ .SubscribeURL }} interval=43200 strict=true
[General]
loglevel = notify
ipv6 = false
skip-proxy = localhost, *.local, injections.adguard.org, local.adguard.org, 0.0.0.0/8, 10.0.0.0/8, 17.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32
tls-provider = default
show-error-page-for-reject = true
dns-server = 223.6.6.6, 119.29.29.29, 119.28.28.28
test-timeout = 5
internet-test-url = http://bing.com
proxy-test-url = http://bing.com
[Panel]
SubscribeInfo = {{ .SubscribeInfo }}, style=info
# Surfboard 配置文档:https://manual.getsurfboard.com/
[Proxy]
# 代理列表
{{ .Proxies }}
[Proxy Group]
# 代理组列表
{{ .ProxyGroup }}
[Rule]
# 规则列表
{{ .Rules }}
-12
View File
@@ -1,12 +0,0 @@
package surfboard
import "time"
type UserInfo struct {
UUID string
Upload int64
Download int64
TotalTraffic int64
ExpiredDate time.Time
SubscribeURL string
}
-24
View File
@@ -1,24 +0,0 @@
package surfboard
import (
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildShadowsocks(data proxy.Proxy, uuid string) string {
ss, ok := data.Option.(proxy.Shadowsocks)
if !ok {
return ""
}
addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port)
config := []string{
addr,
fmt.Sprintf("encrypt-method=%s", ss.Method),
fmt.Sprintf("password=%s", uuid),
"tfo=true",
"udp-relay=true",
}
return strings.Join(config, ",") + "\r\n"
}
-28
View File
@@ -1,28 +0,0 @@
package surfboard
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func createSS() proxy.Proxy {
return proxy.Proxy{
Name: "Shadowsocks",
Server: "test.xxxx.com",
Port: 10301,
Protocol: "shadowsocks",
Option: proxy.Shadowsocks{
Port: 10301,
Method: "aes-256-gcm",
ServerKey: "123456",
},
}
}
func TestShadowsocks(t *testing.T) {
node := createSS()
uuid := "123456"
shadowsocks := buildShadowsocks(node, uuid)
t.Log(shadowsocks)
}
-41
View File
@@ -1,41 +0,0 @@
package surfboard
import (
"strconv"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildTrojan(data proxy.Proxy, uuid string) string {
// $config = [
// "{$server['name']}=trojan",
// "{$server['host']}",
// "{$server['port']}",
// "password={$password}",
// $protocol_settings['server_name'] ? "sni={$protocol_settings['server_name']}" : "",
// 'tfo=true',
// 'udp-relay=true'
//];
trojan, ok := data.Option.(proxy.Trojan)
if !ok {
return ""
}
config := []string{
data.Name + "=trojan",
data.Server,
strconv.Itoa(data.Port),
"password=" + uuid,
"tfo=true",
"udp-relay=true",
}
if trojan.SecurityConfig.SNI != "" {
config = append(config, "sni="+trojan.SecurityConfig.SNI)
}
if trojan.SecurityConfig.AllowInsecure {
config = append(config, "skip-cert-verify=true")
} else {
config = append(config, "skip-cert-verify=false")
}
return strings.Join(config, ",") + "\r\n"
}
-36
View File
@@ -1,36 +0,0 @@
package surfboard
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func createTrojan() proxy.Proxy {
return proxy.Proxy{
Name: "Trojan",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "trojan",
Option: proxy.Trojan{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "baidu.com",
},
SecurityConfig: proxy.SecurityConfig{
SNI: "baidu.com",
AllowInsecure: true,
},
},
}
}
func TestTrojan(t *testing.T) {
node := createTrojan()
uuid := "123456"
trojan := buildTrojan(node, uuid)
t.Log(trojan)
}
-45
View File
@@ -1,45 +0,0 @@
package surfboard
import (
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildVMess(data proxy.Proxy, uuid string) string {
vmess, ok := data.Option.(proxy.Vmess)
if !ok {
return ""
}
addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port)
uriConfig := []string{
addr,
fmt.Sprintf("username=%s", uuid),
"vmess-aead=true",
"tfo=true",
"udp-relay=true",
}
if vmess.Security == "tls" {
uriConfig = append(uriConfig, "tls=true")
if vmess.SecurityConfig.AllowInsecure {
uriConfig = append(uriConfig, "skip-cert-verify=true")
} else {
uriConfig = append(uriConfig, "skip-cert-verify=false")
}
if vmess.SecurityConfig.SNI != "" {
uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI))
}
}
if vmess.Transport == "websocket" {
uriConfig = append(uriConfig, "ws=true")
if vmess.TransportConfig.Path != "" {
uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path))
}
if vmess.TransportConfig.Host != "" {
uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host))
}
}
return strings.Join(uriConfig, ",") + "\r\n"
}
-33
View File
@@ -1,33 +0,0 @@
package surfboard
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func createVMess() proxy.Proxy {
return proxy.Proxy{
Name: "Vmess",
Server: "test.xxxx.com",
Port: 13002,
Protocol: "vmess",
Option: proxy.Vmess{
Port: 13002,
Transport: "websocket",
TransportConfig: proxy.TransportConfig{
Path: "/ws",
Host: "test.xx.com",
},
Security: "none",
},
}
}
func TestVMess(t *testing.T) {
node := createVMess()
uuid := "123456"
p := buildVMess(node, uuid)
t.Log(p)
}
-61
View File
@@ -1,61 +0,0 @@
#!MANAGED-CONFIG {{ .SubscribeURL }} interval=43200 strict=true
# Surge 的规则配置手册: https://manual.nssurge.com/
[General]
loglevel = notify
# 从 Surge iOS 4 / Surge Mac 3.3.0 起,工具开始支持 DoH
doh-server = https://doh.pub/dns-query
# https://dns.alidns.com/dns-query, https://13800000000.rubyfish.cn/, https://dns.google/dns-query
dns-server = 223.5.5.5, 114.114.114.114
tun-excluded-routes = 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 255.255.255.255/32
skip-proxy = localhost, *.local, injections.adguard.org, local.adguard.org, captive.apple.com, guzzoni.apple.com, 0.0.0.0/8, 10.0.0.0/8, 17.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32
wifi-assist = true
allow-wifi-access = true
wifi-access-http-port = 6152
wifi-access-socks5-port = 6153
http-listen = 0.0.0.0:6152
socks5-listen = 0.0.0.0:6153
external-controller-access = surgepasswd@0.0.0.0:6170
replica = false
tls-provider = openssl
network-framework = false
exclude-simple-hostnames = true
ipv6 = true
test-timeout = 4
proxy-test-url = http://www.gstatic.com/generate_204
geoip-maxmind-url = https://unpkg.zhimg.com/rulestatic@1.0.1/Country.mmdb
[Replica]
hide-apple-request = true
hide-crashlytics-request = true
use-keyword-filter = false
hide-udp = false
[Panel]
SubscribeInfo = {{ .SubscribeInfo }}, style=info
# -----------------------------
# Surge 的几种策略配置规范,请参考 https://manual.nssurge.com/policy/proxy.html
# 不同的代理策略有*很多*可选参数,请参考上方连接的 Parameters 一段,根据需求自行添加参数。
#
# Surge 现已支持 UDP 转发功能,请参考: https://trello.com/c/ugOMxD3u/53-udp-%E8%BD%AC%E5%8F%91
# Surge 现已支持 TCP-Fast-Open 技术,请参考: https://trello.com/c/ij65BU6Q/48-tcp-fast-open-troubleshooting-guide
# Surge 现已支持 ss-libev 的全部加密方式和混淆,请参考: https://trello.com/c/BTr0vG1O/47-ss-libev-%E7%9A%84%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5
# -----------------------------
[Proxy]
{{ .Proxies }}
[Proxy Group]
# 代理组列表
{{ .ProxyGroup }}
[Rule]
{{ .Rules }}
[URL Rewrite]
^https?://(www.)?(g|google).cn https://www.google.com 302
-43
View File
@@ -1,43 +0,0 @@
package surge
import (
"fmt"
"strconv"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildHysteria2(data proxy.Proxy, uuid string) string {
hysteria2, ok := data.Option.(proxy.Hysteria2)
if !ok {
return ""
}
var port int
if hysteria2.HopPorts != "" {
ports := strings.Split(hysteria2.HopPorts, ",")
p := ports[0]
if len(strings.Split(p, "-")) > 1 {
p = strings.Split(p, "-")[0]
}
port, _ = strconv.Atoi(p)
} else {
port = data.Port
}
config := []string{
fmt.Sprintf("%s=hysteria2,%s,%d", data.Name, data.Server, port),
"password=" + uuid,
"udp-relay=true",
}
if hysteria2.SecurityConfig.SNI != "" {
config = append(config, "sni="+hysteria2.SecurityConfig.SNI)
}
if hysteria2.SecurityConfig.AllowInsecure {
config = append(config, "skip-cert-verify=true")
} else {
config = append(config, "skip-cert-verify=false")
}
return strings.Join(config, ",") + "\r\n"
}
-70
View File
@@ -1,70 +0,0 @@
package surge
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func TestBuildHysteria2(t *testing.T) {
tests := []struct {
name string
data proxy.Proxy
uuid string
expected string
}{
{
name: "Valid Hysteria2 with HopPorts",
data: proxy.Proxy{
Name: "test",
Server: "server.com",
Port: 443,
Option: proxy.Hysteria2{
HopPorts: "1000-2000",
SecurityConfig: proxy.SecurityConfig{
SNI: "example.com",
AllowInsecure: true,
},
},
},
uuid: "test-uuid",
expected: "test=hysteria2,server.com,1000,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=true\r\n",
},
{
name: "Valid Hysteria2 without HopPorts",
data: proxy.Proxy{
Name: "test",
Server: "server.com",
Port: 443,
Option: proxy.Hysteria2{
SecurityConfig: proxy.SecurityConfig{
SNI: "example.com",
AllowInsecure: false,
},
},
},
uuid: "test-uuid",
expected: "test=hysteria2,server.com,443,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=false\r\n",
},
{
name: "Invalid Hysteria2 Option",
data: proxy.Proxy{
Name: "test",
Server: "server.com",
Port: 443,
Option: nil,
},
uuid: "test-uuid",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildHysteria2(tt.data, tt.uuid)
if result != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, result)
}
})
}
}
-24
View File
@@ -1,24 +0,0 @@
package surge
import (
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildShadowsocks(data proxy.Proxy, uuid string) string {
ss, ok := data.Option.(proxy.Shadowsocks)
if !ok {
return ""
}
addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port)
config := []string{
addr,
fmt.Sprintf("encrypt-method=%s", ss.Method),
fmt.Sprintf("password=%s", uuid),
"tfo=true",
"udp-relay=true",
}
return strings.Join(config, ",") + "\r\n"
}
-117
View File
@@ -1,117 +0,0 @@
package surge
import (
"bytes"
"embed"
"fmt"
"net/url"
"strings"
"text/template"
"time"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/traffic"
)
//go:embed *.tpl
var configFiles embed.FS
type UserInfo struct {
UUID string
Upload int64
Download int64
TotalTraffic int64
ExpiredDate time.Time
SubscribeURL string
}
type Surge struct {
Adapter proxy.Adapter
UUID string
User UserInfo
}
func NewSurge(adapter proxy.Adapter) *Surge {
return &Surge{
Adapter: adapter,
}
}
func (m *Surge) Build(uuid, siteName string, user UserInfo) []byte {
var proxies, proxyGroup, rules string
for _, p := range m.Adapter.Proxies {
switch p.Protocol {
case "shadowsocks":
proxies += buildShadowsocks(p, uuid)
case "trojan":
proxies += buildTrojan(p, uuid)
case "hysteria2":
proxies += buildHysteria2(p, uuid)
case "vmess":
proxies += buildVMess(p, uuid)
}
}
for _, group := range m.Adapter.Group {
if group.Type == proxy.GroupTypeSelect {
proxyGroup += fmt.Sprintf("%s = select, %s", group.Name, strings.Join(group.Proxies, ", ")) + "\r\n"
} else if group.Type == proxy.GroupTypeURLTest {
proxyGroup += fmt.Sprintf("%s = url-test, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n"
} else if group.Type == proxy.GroupTypeFallback {
proxyGroup += fmt.Sprintf("%s = fallback, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n"
} else {
logger.Errorf("[BuildSurfboard] unknown group type: %s", group.Type)
}
}
for _, rule := range m.Adapter.Rules {
if rule == "" {
continue
}
rules += rule + "\r\n"
}
//final rule
rules += "# 最终规则" + "\r\n" + "FINAL,手动选择,dns-failed"
file, err := configFiles.ReadFile("default.tpl")
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
// replace template
tpl, err := template.New("default").Parse(string(file))
if err != nil {
logger.Errorf("read default surfboard config error: %v", err.Error())
return nil
}
var buf bytes.Buffer
var expiredAt string
if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) {
expiredAt = "长期有效"
} else {
expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05")
}
// convert traffic
upload := traffic.AutoConvert(user.Upload, false)
download := traffic.AutoConvert(user.Download, false)
total := traffic.AutoConvert(user.TotalTraffic, false)
unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false)
// query Host
urlParse, err := url.Parse(user.SubscribeURL)
if err != nil {
return nil
}
if err := tpl.Execute(&buf, map[string]interface{}{
"Proxies": proxies,
"ProxyGroup": proxyGroup,
"SubscribeURL": user.SubscribeURL,
"SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt),
"SubscribeDomain": urlParse.Host,
"Rules": rules,
}); err != nil {
logger.Errorf("build Surge config error: %v", err.Error())
return nil
}
return buf.Bytes()
}
-97
View File
@@ -1,97 +0,0 @@
package surge
import (
"strings"
"testing"
"time"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func TestSurgeBuild(t *testing.T) {
adapter := proxy.Adapter{
Proxies: []proxy.Proxy{
{
Name: "test-shadowsocks",
Protocol: "shadowsocks",
Server: "1.2.3.4",
Port: 8388,
Option: proxy.Shadowsocks{
Method: "aes-256-gcm",
},
},
{
Name: "test-trojan",
Protocol: "trojan",
Server: "5.6.7.8",
Port: 443,
Option: proxy.Trojan{
SecurityConfig: proxy.SecurityConfig{
SNI: "example.com",
AllowInsecure: true,
},
},
},
{
Name: "test-hysteria",
Protocol: "hysteria2",
Server: "1.1.1.1",
Port: 443,
Option: proxy.Hysteria2{
HopPorts: "8080-8090",
HopInterval: 320,
SecurityConfig: proxy.SecurityConfig{
SNI: "example.com",
AllowInsecure: true,
},
},
},
},
Group: []proxy.Group{
{
Name: "test-group",
Type: proxy.GroupTypeSelect,
Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"},
},
{
Name: "手动选择",
Type: proxy.GroupTypeSelect,
Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"},
},
},
Rules: []string{
"DOMAIN-SUFFIX,example.com,DIRECT",
},
}
user := UserInfo{
UUID: "test-uuid",
Upload: 1024,
Download: 2048,
TotalTraffic: 4096,
ExpiredDate: time.Now().Add(24 * time.Hour),
SubscribeURL: "http://example.com/subscribe",
}
surge := NewSurge(adapter)
config := surge.Build("test-uuid", "TestSite", user)
if config == nil {
t.Fatal("Expected non-nil config")
}
configStr := string(config)
t.Logf("configStr: %v", configStr)
if !strings.Contains(configStr, "test-shadowsocks=ss") {
t.Errorf("Expected config to contain test-shadowsocks proxy")
}
if !strings.Contains(configStr, "test-trojan=trojan") {
t.Errorf("Expected config to contain test-trojan proxy")
}
if !strings.Contains(configStr, "test-group = select") {
t.Errorf("Expected config to contain test-group proxy group")
}
if !strings.Contains(configStr, "DOMAIN-SUFFIX,example.com,DIRECT") {
t.Errorf("Expected config to contain rule for example.com")
}
}
-32
View File
@@ -1,32 +0,0 @@
package surge
import (
"strconv"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildTrojan(data proxy.Proxy, uuid string) string {
trojan, ok := data.Option.(proxy.Trojan)
if !ok {
return ""
}
config := []string{
data.Name + "=trojan",
data.Server,
strconv.Itoa(data.Port),
"password=" + uuid,
"tfo=true",
"udp-relay=true",
}
if trojan.SecurityConfig.SNI != "" {
config = append(config, "sni="+trojan.SecurityConfig.SNI)
}
if trojan.SecurityConfig.AllowInsecure {
config = append(config, "skip-cert-verify=true")
} else {
config = append(config, "skip-cert-verify=false")
}
return strings.Join(config, ",") + "\r\n"
}
-44
View File
@@ -1,44 +0,0 @@
package surge
import (
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
)
func buildVMess(data proxy.Proxy, uuid string) string {
vmess, ok := data.Option.(proxy.Vmess)
if !ok {
return ""
}
addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port)
uriConfig := []string{
addr,
fmt.Sprintf("username=%s", uuid),
"vmess-aead=true",
"tfo=true",
"udp-relay=true",
}
if vmess.Security == "tls" {
uriConfig = append(uriConfig, "tls=true")
if vmess.SecurityConfig.AllowInsecure {
uriConfig = append(uriConfig, "skip-cert-verify=true")
} else {
uriConfig = append(uriConfig, "skip-cert-verify=false")
}
if vmess.SecurityConfig.SNI != "" {
uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI))
}
}
if vmess.Transport == "websocket" {
uriConfig = append(uriConfig, "ws=true")
if vmess.TransportConfig.Path != "" {
uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path))
}
if vmess.TransportConfig.Host != "" {
uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host))
}
}
return strings.Join(uriConfig, ",") + "\r\n"
}
-197
View File
@@ -1,197 +0,0 @@
package adapter
import (
"encoding/json"
"strings"
"github.com/perfect-panel/ppanel-server/internal/model/server"
"github.com/perfect-panel/ppanel-server/pkg/adapter/proxy"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/random"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
func addNode(data *server.Server, host string, port int) *proxy.Proxy {
var option any
node := proxy.Proxy{
Name: data.Name,
Server: host,
Port: port,
Country: data.Country,
Protocol: data.Protocol,
}
switch data.Protocol {
case "shadowsocks":
var ss proxy.Shadowsocks
if err := json.Unmarshal([]byte(data.Config), &ss); err != nil {
return nil
}
if port == 0 {
node.Port = ss.Port
}
option = ss
case "vless":
var vless proxy.Vless
if err := json.Unmarshal([]byte(data.Config), &vless); err != nil {
return nil
}
if port == 0 {
node.Port = vless.Port
}
option = vless
case "vmess":
var vmess proxy.Vmess
if err := json.Unmarshal([]byte(data.Config), &vmess); err != nil {
return nil
}
if port == 0 {
node.Port = vmess.Port
}
option = vmess
case "trojan":
var trojan proxy.Trojan
if err := json.Unmarshal([]byte(data.Config), &trojan); err != nil {
return nil
}
if port == 0 {
node.Port = trojan.Port
}
option = trojan
case "hysteria2":
var hysteria2 proxy.Hysteria2
if err := json.Unmarshal([]byte(data.Config), &hysteria2); err != nil {
return nil
}
if port == 0 {
node.Port = hysteria2.Port
}
option = hysteria2
case "tuic":
var tuic proxy.Tuic
if err := json.Unmarshal([]byte(data.Config), &tuic); err != nil {
return nil
}
if port == 0 {
node.Port = tuic.Port
}
option = tuic
default:
return nil
}
node.Option = option
return &node
}
func addProxyToGroup(proxyName, groupName string, groups []proxy.Group) []proxy.Group {
for i, group := range groups {
if group.Name == groupName {
groups[i].Proxies = tool.RemoveDuplicateElements(append(group.Proxies, proxyName)...)
return groups
}
}
groups = append(groups, proxy.Group{
Name: groupName,
Type: "select",
Proxies: []string{proxyName},
})
return groups
}
func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string) {
for _, group := range groups {
proxyGroup = append(proxyGroup, proxy.Group{
Name: group.Name,
Type: "select",
Proxies: RemoveEmptyString(strings.Split(group.Tags, ",")),
})
rules = append(rules, strings.Split(group.Rules, "/n")...)
}
return
}
func generateProxyGroup(servers []proxy.Proxy) (proxyGroup []proxy.Group, region []string) {
// 设置手动选择分组
proxyGroup = append(proxyGroup, []proxy.Group{
{
Name: "智能线路",
Type: "url-test",
Proxies: make([]string, 0),
URL: "https://www.gstatic.com/generate_204",
Interval: 300,
},
{
Name: "手动选择",
Type: "select",
Proxies: []string{"智能线路"},
},
}...)
for _, node := range servers {
if node.Country != "" {
proxyGroup = addProxyToGroup(node.Name, node.Country, proxyGroup)
region = append(region, node.Country)
proxyGroup = addProxyToGroup(node.Country, "智能线路", proxyGroup)
}
proxyGroup = addProxyToGroup(node.Name, "手动选择", proxyGroup)
}
proxyGroup = addProxyToGroup("DIRECT", "手动选择", proxyGroup)
return proxyGroup, tool.RemoveDuplicateElements(region...)
}
func adapterProxies(servers []*server.Server) []proxy.Proxy {
var proxies []proxy.Proxy
for _, node := range servers {
switch node.RelayMode {
case server.RelayModeAll:
var relays []server.NodeRelay
if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil {
logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode))
continue
}
for _, relay := range relays {
n := addNode(node, relay.Host, relay.Port)
if n == nil {
continue
}
if relay.Prefix != "" {
n.Name = relay.Prefix + "-" + n.Name
}
proxies = append(proxies, *n)
}
case server.RelayModeRandom:
var relays []server.NodeRelay
if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil {
logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode))
continue
}
randNum := random.RandomInRange(0, len(relays)-1)
relay := relays[randNum]
n := addNode(node, relay.Host, relay.Port)
if n == nil {
continue
}
if relay.Prefix != "" {
n.Name = relay.Prefix + " - " + node.Name
}
proxies = append(proxies, *n)
default:
logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode))
n := addNode(node, node.ServerAddr, 0)
if n != nil {
proxies = append(proxies, *n)
}
}
}
return proxies
}
// RemoveEmptyString 切片去除空值
func RemoveEmptyString(arr []string) []string {
var result []string
for _, str := range arr {
if str != "" {
result = append(result, str)
}
}
return result
}
+49
View File
@@ -0,0 +1,49 @@
package cache
import "time"
const (
defaultExpiry = time.Hour * 24 * 7
defaultNotFoundExpiry = time.Minute
)
type (
// Options is used to store the cache options.
Options struct {
Expiry time.Duration
NotFoundExpiry time.Duration
}
// Option defines the method to customize an Options.
Option func(o *Options)
)
func newOptions(opts ...Option) Options {
var o Options
for _, opt := range opts {
opt(&o)
}
if o.Expiry <= 0 {
o.Expiry = defaultExpiry
}
if o.NotFoundExpiry <= 0 {
o.NotFoundExpiry = defaultNotFoundExpiry
}
return o
}
// WithExpiry returns a func to customize an Options with given expiry.
func WithExpiry(expiry time.Duration) Option {
return func(o *Options) {
o.Expiry = expiry
}
}
// WithNotFoundExpiry returns a func to customize an Options with given not found expiry.
func WithNotFoundExpiry(expiry time.Duration) Option {
return func(o *Options) {
o.NotFoundExpiry = expiry
}
}
+28
View File
@@ -0,0 +1,28 @@
package cache
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCacheOptions(t *testing.T) {
t.Run("default options", func(t *testing.T) {
o := newOptions()
assert.Equal(t, defaultExpiry, o.Expiry)
assert.Equal(t, defaultNotFoundExpiry, o.NotFoundExpiry)
})
t.Run("with expiry", func(t *testing.T) {
o := newOptions(WithExpiry(time.Second))
assert.Equal(t, time.Second, o.Expiry)
assert.Equal(t, defaultNotFoundExpiry, o.NotFoundExpiry)
})
t.Run("with not found expiry", func(t *testing.T) {
o := newOptions(WithNotFoundExpiry(time.Second))
assert.Equal(t, defaultExpiry, o.Expiry)
assert.Equal(t, time.Second, o.NotFoundExpiry)
})
}
+16 -7
View File
@@ -5,12 +5,16 @@ import (
"database/sql"
"encoding/json"
"errors"
"time"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
var ErrNotFound = redis.Nil
var (
// ErrNotFound is the error when cache not found.
ErrNotFound = redis.Nil
)
type (
// ExecCtxFn defines the sql exec method.
@@ -23,16 +27,21 @@ type (
QueryCtxFn func(conn *gorm.DB, v interface{}) error
CachedConn struct {
db *gorm.DB
cache *redis.Client
db *gorm.DB
cache *redis.Client
expiry time.Duration
notFoundExpiry time.Duration
}
)
// NewConn returns a CachedConn with a redis cluster cache.
func NewConn(db *gorm.DB, c *redis.Client) CachedConn {
func NewConn(db *gorm.DB, c *redis.Client, opts ...Option) CachedConn {
o := newOptions(opts...)
return CachedConn{
db: db,
cache: c,
db: db,
cache: c,
expiry: o.Expiry,
notFoundExpiry: o.NotFoundExpiry,
}
}
@@ -65,7 +74,7 @@ func (cc CachedConn) SetCache(key string, v interface{}) error {
return err
}
// set redis key
return cc.cache.Set(context.Background(), key, val, 0).Err()
return cc.cache.Set(context.Background(), key, val, cc.expiry).Err()
}
// ExecCtx runs given exec on given keys, and returns execution result.
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/perfect-panel/ppanel-server/pkg/orm"
"github.com/perfect-panel/server/pkg/orm"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
+1
View File
@@ -8,4 +8,5 @@ const (
CtxKeyRequestHost CtxKey = "requestHost"
CtxKeyPlatform CtxKey = "platform"
CtxKeyPayment CtxKey = "payment"
LoginType CtxKey = "loginType"
)
+15 -6
View File
@@ -1,8 +1,6 @@
package constant
import (
"encoding/json"
)
import "encoding/json"
// Used for type cloning conversion
const (
@@ -43,9 +41,20 @@ type TemporaryOrderInfo struct {
Identifier string `json:"identifier"`
AuthType string `json:"auth_type"`
Password string `json:"password"`
InviteCode string `json:"invite_code,omitempty"`
}
func (t TemporaryOrderInfo) Marshal() string {
value, _ := json.Marshal(t)
return string(value)
func (t *TemporaryOrderInfo) Unmarshal(data []byte) error {
type Alias TemporaryOrderInfo
aux := (*Alias)(t)
return json.Unmarshal(data, aux)
}
func (t *TemporaryOrderInfo) Marshal() ([]byte, error) {
type Alias TemporaryOrderInfo
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(t),
})
}
+4 -1
View File
@@ -1,4 +1,7 @@
package constant
// Version PPanel version
const Version = "0.3.0(3002)"
var (
Version = "unknown version"
BuildTime = "unknown time"
)
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,14 +0,0 @@
package countryCenter
import (
"testing"
)
func TestGetCountryCenter(t *testing.T) {
lat, lon, found := GetCountryCenterByCountryOrCity("SG", "Singapore")
if !found {
t.Error("GetCountryCenter('HK') should return found = true")
}
t.Logf("lat = %v, lon = %v", lat, lon)
}
+301 -61
View File
@@ -1,95 +1,302 @@
// Package deduction provides functionality for calculating remaining amounts
// in subscription billing systems, supporting various time units and traffic-based calculations.
package deduction
import (
"log"
"errors"
"fmt"
"math"
"time"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/server/pkg/tool"
)
const (
UnitTimeNoLimit = "NoLimit"
UnitTimeYear = "Year"
UnitTimeMonth = "Month"
UnitTimeDay = "Day"
UintTimeHour = "Hour"
UintTimeMinute = "Minute"
// Time unit constants for subscription billing
UnitTimeNoLimit = "NoLimit" // Unlimited time subscription
UnitTimeYear = "Year" // Annual subscription
UnitTimeMonth = "Month" // Monthly subscription
UnitTimeDay = "Day" // Daily subscription
UnitTimeHour = "Hour" // Hourly subscription
UnitTimeMinute = "Minute" // Per-minute subscription
ResetCycleNone = 0
ResetCycle1st = 1
ResetCycleMonthly = 2
ResetCycleYear = 3
// Reset cycle constants for traffic resets
ResetCycleNone = 0 // No reset cycle
ResetCycle1st = 1 // Reset on 1st of each month
ResetCycleMonthly = 2 // Reset monthly based on start date
ResetCycleYear = 3 // Reset yearly based on start date
// Safety limits for overflow protection
maxInt64 = math.MaxInt64
minInt64 = math.MinInt64
)
// Error definitions for validation and calculation failures
var (
ErrInvalidQuantity = errors.New("order quantity cannot be zero or negative")
ErrInvalidAmount = errors.New("order amount cannot be negative")
ErrInvalidTraffic = errors.New("traffic values cannot be negative")
ErrInvalidTimeRange = errors.New("expire time must be after start time")
ErrInvalidUnitTime = errors.New("invalid unit time")
ErrInvalidDeductionRatio = errors.New("deduction ratio must be between 0 and 100")
ErrOverflow = errors.New("calculation overflow")
)
// Subscribe represents a subscription with time and traffic limits
type Subscribe struct {
StartTime time.Time
ExpireTime time.Time
Traffic int64
Download int64
Upload int64
UnitTime string
UnitPrice int64
ResetCycle int64
DeductionRatio int64
StartTime time.Time // Subscription start time
ExpireTime time.Time // Subscription expiration time
Traffic int64 // Total traffic allowance in bytes
Download int64 // Downloaded traffic in bytes
Upload int64 // Uploaded traffic in bytes
UnitTime string // Time unit for billing (Year, Month, Day, etc.)
UnitPrice int64 // Price per unit time
ResetCycle int64 // Traffic reset cycle
DeductionRatio int64 // Deduction ratio for weighted calculations (0-100)
}
// Order represents a purchase order for subscription calculation
type Order struct {
Amount int64
Quantity int64
Amount int64 // Total order amount
Quantity int64 // Order quantity
}
func CalculateRemainingAmount(sub Subscribe, order Order) int64 {
if sub.UnitTime == UnitTimeNoLimit && sub.ResetCycle != 0 {
return 0
// Validate checks if the Subscribe struct contains valid data
func (s *Subscribe) Validate() error {
if s.Traffic < 0 || s.Download < 0 || s.Upload < 0 {
return ErrInvalidTraffic
}
log.Printf("开始计算订单剩余价值")
// 实际单价
sub.UnitPrice = order.Amount / order.Quantity
log.Printf("订阅实际单价: %d", sub.UnitPrice)
now := time.Now()
if s.Download+s.Upload > s.Traffic {
return fmt.Errorf("download + upload (%d) cannot exceed total traffic (%d)", s.Download+s.Upload, s.Traffic)
}
if !s.ExpireTime.After(s.StartTime) {
return ErrInvalidTimeRange
}
if s.DeductionRatio < 0 || s.DeductionRatio > 100 {
return ErrInvalidDeductionRatio
}
validUnitTimes := []string{UnitTimeNoLimit, UnitTimeYear, UnitTimeMonth, UnitTimeDay, UnitTimeHour, UnitTimeMinute}
valid := false
for _, ut := range validUnitTimes {
if s.UnitTime == ut {
valid = true
break
}
}
if !valid {
return ErrInvalidUnitTime
}
return nil
}
// Validate checks if the Order struct contains valid data
func (o *Order) Validate() error {
if o.Quantity <= 0 {
return ErrInvalidQuantity
}
if o.Amount < 0 {
return ErrInvalidAmount
}
return nil
}
// safeMultiply performs multiplication with overflow protection
func safeMultiply(a, b int64) (int64, error) {
if a == 0 || b == 0 {
return 0, nil
}
if a > 0 && b > 0 {
if a > maxInt64/b {
return 0, ErrOverflow
}
} else if a < 0 && b < 0 {
if a < maxInt64/b {
return 0, ErrOverflow
}
} else {
if (a > 0 && b < minInt64/a) || (a < 0 && b > minInt64/a) {
return 0, ErrOverflow
}
}
return a * b, nil
}
// safeAdd performs addition with overflow protection
func safeAdd(a, b int64) (int64, error) {
if (b > 0 && a > maxInt64-b) || (b < 0 && a < minInt64-b) {
return 0, ErrOverflow
}
return a + b, nil
}
// safeDivide performs division with zero-division protection
func safeDivide(a, b int64) (int64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// CalculateRemainingAmount calculates the remaining refund amount for a subscription
// based on unused time and traffic. Returns the amount and any calculation errors.
func CalculateRemainingAmount(sub Subscribe, order Order) (int64, error) {
if err := sub.Validate(); err != nil {
return 0, fmt.Errorf("invalid subscription: %w", err)
}
if err := order.Validate(); err != nil {
return 0, fmt.Errorf("invalid order: %w", err)
}
if sub.UnitTime == UnitTimeNoLimit && sub.ResetCycle != 0 {
return 0, nil
}
unitPrice, err := safeDivide(order.Amount, order.Quantity)
if err != nil {
return 0, fmt.Errorf("failed to calculate unit price: %w", err)
}
sub.UnitPrice = unitPrice
loc, err := time.LoadLocation(sub.StartTime.Location().String())
if err != nil {
loc = time.UTC
}
now := time.Now().In(loc)
switch sub.UnitTime {
case UnitTimeNoLimit:
log.Printf("订阅不限时长")
usedTraffic := sub.Traffic - sub.Download - sub.Upload
unitPrice := float64(order.Amount) / float64(sub.Traffic)
return int64(float64(usedTraffic) * unitPrice)
return calculateNoLimitAmount(sub, order)
case UnitTimeYear:
log.Printf("订阅时长为年")
remainingYears := tool.YearDiff(now, sub.ExpireTime)
remainingUnitTimeAmount := calculateRemainingUnitTimeAmount(sub)
return int64(remainingYears)*sub.UnitPrice + remainingUnitTimeAmount
remainingUnitTimeAmount, err := calculateRemainingUnitTimeAmount(sub)
if err != nil {
return 0, err
}
yearAmount, err := safeMultiply(int64(remainingYears), sub.UnitPrice)
if err != nil {
return 0, fmt.Errorf("year calculation overflow: %w", err)
}
total, err := safeAdd(yearAmount, remainingUnitTimeAmount)
if err != nil {
return 0, fmt.Errorf("total calculation overflow: %w", err)
}
return total, nil
case UnitTimeMonth:
log.Printf("订阅时长为月")
remainingMonths := tool.MonthDiff(now, sub.ExpireTime)
remainingUnitTimeAmount := calculateRemainingUnitTimeAmount(sub)
return int64(remainingMonths)*sub.UnitPrice + remainingUnitTimeAmount
remainingUnitTimeAmount, err := calculateRemainingUnitTimeAmount(sub)
if err != nil {
return 0, err
}
monthAmount, err := safeMultiply(int64(remainingMonths), sub.UnitPrice)
if err != nil {
return 0, fmt.Errorf("month calculation overflow: %w", err)
}
total, err := safeAdd(monthAmount, remainingUnitTimeAmount)
if err != nil {
return 0, fmt.Errorf("total calculation overflow: %w", err)
}
return total, nil
case UnitTimeDay:
remainingDays := tool.DayDiff(now, sub.ExpireTime)
remainingUnitTimeAmount, err := calculateRemainingUnitTimeAmount(sub)
if err != nil {
return 0, err
}
dayAmount, err := safeMultiply(remainingDays, sub.UnitPrice)
if err != nil {
return 0, fmt.Errorf("day calculation overflow: %w", err)
}
total, err := safeAdd(dayAmount, remainingUnitTimeAmount)
if err != nil {
return 0, fmt.Errorf("total calculation overflow: %w", err)
}
return total, nil
}
return 0
return 0, nil
}
func calculateRemainingUnitTimeAmount(sub Subscribe) int64 {
// calculateNoLimitAmount calculates refund amount for unlimited time subscriptions
// based on unused traffic only
func calculateNoLimitAmount(sub Subscribe, order Order) (int64, error) {
if sub.Traffic == 0 {
return 0, nil
}
usedTraffic := sub.Traffic - sub.Download - sub.Upload
if usedTraffic < 0 {
usedTraffic = 0
}
unitPrice := float64(order.Amount) / float64(sub.Traffic)
result := float64(usedTraffic) * unitPrice
if result > float64(maxInt64) || result < float64(minInt64) {
return 0, ErrOverflow
}
return int64(result), nil
}
// calculateRemainingUnitTimeAmount calculates the remaining amount based on
// both time and traffic usage, applying deduction ratios when specified
func calculateRemainingUnitTimeAmount(sub Subscribe) (int64, error) {
now := time.Now()
log.Printf("开始计算订阅剩余时长价值")
log.Printf("订阅开始时间: %s, 订阅到期时间: %s,订阅流量: %d", sub.StartTime.Format("2006-01-02 15:04:05"), sub.ExpireTime.Format("2006-01-02 15:04:05"), sub.Traffic)
trafficWeight, timeWeight := calculateWeights(sub.DeductionRatio)
remainingDays, totalDays := getRemainingAndTotalDays(sub, now)
remainingTraffic := sub.Traffic - sub.Download - sub.Upload
remainingTimeAmount := calculateProportionalAmount(sub.UnitPrice, remainingDays, totalDays)
remainingTrafficAmount := calculateProportionalAmount(sub.UnitPrice, remainingTraffic, sub.Traffic)
log.Printf("订阅剩余天数: %d, 总天数: %d, 剩余流量: %d, 剩余时间价值: %d, 剩余流量价值: %d", remainingDays, totalDays, remainingTraffic, remainingTimeAmount, remainingTrafficAmount)
if sub.Traffic == 0 {
return remainingTimeAmount
if totalDays == 0 {
return 0, nil
}
remainingTraffic := sub.Traffic - sub.Download - sub.Upload
if remainingTraffic < 0 {
remainingTraffic = 0
}
remainingTimeAmount, err := calculateProportionalAmount(sub.UnitPrice, remainingDays, totalDays)
if err != nil {
return 0, fmt.Errorf("time amount calculation failed: %w", err)
}
if sub.Traffic == 0 {
return remainingTimeAmount, nil
}
remainingTrafficAmount, err := calculateProportionalAmount(sub.UnitPrice, remainingTraffic, sub.Traffic)
if err != nil {
return 0, fmt.Errorf("traffic amount calculation failed: %w", err)
}
if sub.DeductionRatio != 0 {
return calculateWeightedAmount(sub.UnitPrice, remainingTraffic, sub.Traffic, remainingDays, totalDays, trafficWeight, timeWeight)
}
return min(remainingTimeAmount, remainingTrafficAmount)
return min(remainingTimeAmount, remainingTrafficAmount), nil
}
// calculateWeights converts deduction ratio to traffic and time weights
// for weighted calculations
func calculateWeights(deductionRatio int64) (float64, float64) {
if deductionRatio == 0 {
return 0, 0
@@ -99,22 +306,32 @@ func calculateWeights(deductionRatio int64) (float64, float64) {
return trafficWeight, timeWeight
}
// getRemainingAndTotalDays calculates remaining and total days based on
// the subscription's reset cycle configuration
func getRemainingAndTotalDays(sub Subscribe, now time.Time) (int64, int64) {
log.Printf("开始计算订阅剩余天数")
log.Printf("重置周期: %d", sub.ResetCycle)
switch sub.ResetCycle {
case ResetCycleNone:
remaining := sub.ExpireTime.Sub(now).Hours() / 24
total := sub.ExpireTime.Sub(sub.StartTime).Hours() / 24
if remaining < 0 {
remaining = 0
}
if total < 0 {
total = 0
}
return int64(remaining), int64(total)
case ResetCycle1st:
return tool.DaysToNextMonth(now), tool.GetLastDayOfMonth(now)
case ResetCycleMonthly:
// -1 to include the current day
return tool.DaysToMonthDay(now, sub.StartTime.Day()) - 1, tool.DaysToMonthDay(now, sub.StartTime.Day())
remaining := tool.DaysToMonthDay(now, sub.StartTime.Day()) - 1
total := tool.DaysToMonthDay(now, sub.StartTime.Day())
if remaining < 0 {
remaining = 0
}
return remaining, total
case ResetCycleYear:
return tool.DaysToYearDay(now, int(sub.StartTime.Month()), sub.StartTime.Day()),
tool.GetYearDays(now, int(sub.StartTime.Month()), sub.StartTime.Day())
@@ -122,13 +339,36 @@ func getRemainingAndTotalDays(sub Subscribe, now time.Time) (int64, int64) {
return 0, 0
}
func calculateWeightedAmount(unitPrice, remainingTraffic, totalTraffic, remainingDays, totalDays int64, trafficWeight, timeWeight float64) int64 {
// calculateWeightedAmount applies weighted calculation combining both time and traffic
// remaining ratios based on the specified weights
func calculateWeightedAmount(unitPrice, remainingTraffic, totalTraffic, remainingDays, totalDays int64, trafficWeight, timeWeight float64) (int64, error) {
if totalDays == 0 || totalTraffic == 0 {
return 0, nil
}
remainingTimeRatio := float64(remainingDays) / float64(totalDays)
remainingTrafficRatio := float64(remainingTraffic) / float64(totalTraffic)
weightedRemainingRatio := (timeWeight * remainingTimeRatio) + (trafficWeight * remainingTrafficRatio)
return int64(float64(unitPrice) * weightedRemainingRatio)
result := float64(unitPrice) * weightedRemainingRatio
if result > float64(maxInt64) || result < float64(minInt64) {
return 0, ErrOverflow
}
return int64(result), nil
}
func calculateProportionalAmount(unitPrice, remaining, total int64) int64 {
return int64(float64(unitPrice) * (float64(remaining) / float64(total)))
// calculateProportionalAmount calculates proportional amount based on
// remaining vs total ratio with overflow protection
func calculateProportionalAmount(unitPrice, remaining, total int64) (int64, error) {
if total == 0 {
return 0, nil
}
result := float64(unitPrice) * (float64(remaining) / float64(total))
if result > float64(maxInt64) || result < float64(minInt64) {
return 0, ErrOverflow
}
return int64(result), nil
}
+665
View File
@@ -0,0 +1,665 @@
package deduction
import (
"math"
"testing"
"time"
)
func TestSubscribe_Validate(t *testing.T) {
tests := []struct {
name string
sub Subscribe
wantErr bool
errType error
}{
{
name: "valid subscription",
sub: Subscribe{
StartTime: time.Now(),
ExpireTime: time.Now().Add(24 * time.Hour),
Traffic: 1000,
Download: 100,
Upload: 200,
UnitTime: UnitTimeMonth,
DeductionRatio: 50,
},
wantErr: false,
},
{
name: "negative traffic",
sub: Subscribe{
StartTime: time.Now(),
ExpireTime: time.Now().Add(24 * time.Hour),
Traffic: -1000,
Download: 100,
Upload: 200,
UnitTime: UnitTimeMonth,
DeductionRatio: 50,
},
wantErr: true,
errType: ErrInvalidTraffic,
},
{
name: "negative download",
sub: Subscribe{
StartTime: time.Now(),
ExpireTime: time.Now().Add(24 * time.Hour),
Traffic: 1000,
Download: -100,
Upload: 200,
UnitTime: UnitTimeMonth,
DeductionRatio: 50,
},
wantErr: true,
errType: ErrInvalidTraffic,
},
{
name: "download + upload exceeds traffic",
sub: Subscribe{
StartTime: time.Now(),
ExpireTime: time.Now().Add(24 * time.Hour),
Traffic: 1000,
Download: 600,
Upload: 500,
UnitTime: UnitTimeMonth,
DeductionRatio: 50,
},
wantErr: true,
},
{
name: "expire time before start time",
sub: Subscribe{
StartTime: time.Now(),
ExpireTime: time.Now().Add(-24 * time.Hour),
Traffic: 1000,
Download: 100,
Upload: 200,
UnitTime: UnitTimeMonth,
DeductionRatio: 50,
},
wantErr: true,
errType: ErrInvalidTimeRange,
},
{
name: "invalid deduction ratio - negative",
sub: Subscribe{
StartTime: time.Now(),
ExpireTime: time.Now().Add(24 * time.Hour),
Traffic: 1000,
Download: 100,
Upload: 200,
UnitTime: UnitTimeMonth,
DeductionRatio: -10,
},
wantErr: true,
errType: ErrInvalidDeductionRatio,
},
{
name: "invalid deduction ratio - over 100",
sub: Subscribe{
StartTime: time.Now(),
ExpireTime: time.Now().Add(24 * time.Hour),
Traffic: 1000,
Download: 100,
Upload: 200,
UnitTime: UnitTimeMonth,
DeductionRatio: 150,
},
wantErr: true,
errType: ErrInvalidDeductionRatio,
},
{
name: "invalid unit time",
sub: Subscribe{
StartTime: time.Now(),
ExpireTime: time.Now().Add(24 * time.Hour),
Traffic: 1000,
Download: 100,
Upload: 200,
UnitTime: "InvalidUnit",
DeductionRatio: 50,
},
wantErr: true,
errType: ErrInvalidUnitTime,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.sub.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Subscribe.Validate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.errType != nil && err != tt.errType {
t.Errorf("Subscribe.Validate() error = %v, want %v", err, tt.errType)
}
})
}
}
func TestOrder_Validate(t *testing.T) {
tests := []struct {
name string
order Order
wantErr bool
errType error
}{
{
name: "valid order",
order: Order{Amount: 1000, Quantity: 2},
wantErr: false,
},
{
name: "zero quantity",
order: Order{Amount: 1000, Quantity: 0},
wantErr: true,
errType: ErrInvalidQuantity,
},
{
name: "negative quantity",
order: Order{Amount: 1000, Quantity: -1},
wantErr: true,
errType: ErrInvalidQuantity,
},
{
name: "negative amount",
order: Order{Amount: -1000, Quantity: 2},
wantErr: true,
errType: ErrInvalidAmount,
},
{
name: "zero amount is valid",
order: Order{Amount: 0, Quantity: 1},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.order.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Order.Validate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.errType != nil && err != tt.errType {
t.Errorf("Order.Validate() error = %v, want %v", err, tt.errType)
}
})
}
}
func TestSafeMultiply(t *testing.T) {
tests := []struct {
name string
a, b int64
want int64
wantErr bool
}{
{
name: "normal multiplication",
a: 10,
b: 20,
want: 200,
wantErr: false,
},
{
name: "zero multiplication",
a: 10,
b: 0,
want: 0,
wantErr: false,
},
{
name: "negative multiplication",
a: -10,
b: 20,
want: -200,
wantErr: false,
},
{
name: "overflow case",
a: math.MaxInt64,
b: 2,
want: 0,
wantErr: true,
},
{
name: "large numbers no overflow",
a: 1000000,
b: 1000000,
want: 1000000000000,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := safeMultiply(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("safeMultiply() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("safeMultiply() = %v, want %v", got, tt.want)
}
})
}
}
func TestSafeAdd(t *testing.T) {
tests := []struct {
name string
a, b int64
want int64
wantErr bool
}{
{
name: "normal addition",
a: 10,
b: 20,
want: 30,
wantErr: false,
},
{
name: "negative addition",
a: -10,
b: 5,
want: -5,
wantErr: false,
},
{
name: "overflow case",
a: math.MaxInt64,
b: 1,
want: 0,
wantErr: true,
},
{
name: "underflow case",
a: math.MinInt64,
b: -1,
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := safeAdd(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("safeAdd() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("safeAdd() = %v, want %v", got, tt.want)
}
})
}
}
func TestSafeDivide(t *testing.T) {
tests := []struct {
name string
a, b int64
want int64
wantErr bool
}{
{
name: "normal division",
a: 20,
b: 10,
want: 2,
wantErr: false,
},
{
name: "division by zero",
a: 20,
b: 0,
want: 0,
wantErr: true,
},
{
name: "negative division",
a: -20,
b: 10,
want: -2,
wantErr: false,
},
{
name: "zero dividend",
a: 0,
b: 10,
want: 0,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := safeDivide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Errorf("safeDivide() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("safeDivide() = %v, want %v", got, tt.want)
}
})
}
}
func TestCalculateWeights(t *testing.T) {
tests := []struct {
name string
deductionRatio int64
wantTrafficWeight float64
wantTimeWeight float64
}{
{
name: "zero ratio",
deductionRatio: 0,
wantTrafficWeight: 0,
wantTimeWeight: 0,
},
{
name: "50% ratio",
deductionRatio: 50,
wantTrafficWeight: 0.5,
wantTimeWeight: 0.5,
},
{
name: "75% ratio",
deductionRatio: 75,
wantTrafficWeight: 0.75,
wantTimeWeight: 0.25,
},
{
name: "100% ratio",
deductionRatio: 100,
wantTrafficWeight: 1.0,
wantTimeWeight: 0.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotTrafficWeight, gotTimeWeight := calculateWeights(tt.deductionRatio)
if gotTrafficWeight != tt.wantTrafficWeight {
t.Errorf("calculateWeights() trafficWeight = %v, want %v", gotTrafficWeight, tt.wantTrafficWeight)
}
if gotTimeWeight != tt.wantTimeWeight {
t.Errorf("calculateWeights() timeWeight = %v, want %v", gotTimeWeight, tt.wantTimeWeight)
}
})
}
}
func TestCalculateProportionalAmount(t *testing.T) {
tests := []struct {
name string
unitPrice int64
remaining int64
total int64
want int64
wantErr bool
}{
{
name: "normal calculation",
unitPrice: 100,
remaining: 50,
total: 100,
want: 50,
wantErr: false,
},
{
name: "zero total",
unitPrice: 100,
remaining: 50,
total: 0,
want: 0,
wantErr: false,
},
{
name: "zero remaining",
unitPrice: 100,
remaining: 0,
total: 100,
want: 0,
wantErr: false,
},
{
name: "quarter remaining",
unitPrice: 200,
remaining: 25,
total: 100,
want: 50,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := calculateProportionalAmount(tt.unitPrice, tt.remaining, tt.total)
if (err != nil) != tt.wantErr {
t.Errorf("calculateProportionalAmount() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("calculateProportionalAmount() = %v, want %v", got, tt.want)
}
})
}
}
func TestCalculateNoLimitAmount(t *testing.T) {
tests := []struct {
name string
sub Subscribe
order Order
want int64
wantErr bool
}{
{
name: "normal no limit calculation",
sub: Subscribe{
Traffic: 1000,
Download: 300,
Upload: 200,
},
order: Order{
Amount: 1000,
},
want: 500, // (1000 - 300 - 200) / 1000 * 1000 = 500
wantErr: false,
},
{
name: "zero traffic",
sub: Subscribe{
Traffic: 0,
Download: 0,
Upload: 0,
},
order: Order{
Amount: 1000,
},
want: 0,
wantErr: false,
},
{
name: "overused traffic",
sub: Subscribe{
Traffic: 1000,
Download: 600,
Upload: 500,
},
order: Order{
Amount: 1000,
},
want: 0, // usedTraffic would be negative, clamped to 0
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := calculateNoLimitAmount(tt.sub, tt.order)
if (err != nil) != tt.wantErr {
t.Errorf("calculateNoLimitAmount() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("calculateNoLimitAmount() = %v, want %v", got, tt.want)
}
})
}
}
func TestCalculateRemainingAmount(t *testing.T) {
now := time.Now()
tests := []struct {
name string
sub Subscribe
order Order
wantErr bool
}{
{
name: "valid no limit subscription",
sub: Subscribe{
StartTime: now.Add(-24 * time.Hour),
ExpireTime: now.Add(24 * time.Hour),
Traffic: 1000,
Download: 300,
Upload: 200,
UnitTime: UnitTimeNoLimit,
ResetCycle: ResetCycleNone,
DeductionRatio: 0,
},
order: Order{
Amount: 1000,
Quantity: 1,
},
wantErr: false,
},
{
name: "invalid subscription",
sub: Subscribe{
StartTime: now,
ExpireTime: now.Add(-24 * time.Hour), // Invalid: expire before start
Traffic: 1000,
Download: 300,
Upload: 200,
UnitTime: UnitTimeMonth,
DeductionRatio: 0,
},
order: Order{
Amount: 1000,
Quantity: 1,
},
wantErr: true,
},
{
name: "invalid order",
sub: Subscribe{
StartTime: now.Add(-24 * time.Hour),
ExpireTime: now.Add(24 * time.Hour),
Traffic: 1000,
Download: 300,
Upload: 200,
UnitTime: UnitTimeMonth,
DeductionRatio: 0,
},
order: Order{
Amount: 1000,
Quantity: 0, // Invalid: zero quantity
},
wantErr: true,
},
{
name: "no limit with reset cycle",
sub: Subscribe{
StartTime: now.Add(-24 * time.Hour),
ExpireTime: now.Add(24 * time.Hour),
Traffic: 1000,
Download: 300,
Upload: 200,
UnitTime: UnitTimeNoLimit,
ResetCycle: ResetCycleMonthly, // Should return 0
DeductionRatio: 0,
},
order: Order{
Amount: 1000,
Quantity: 1,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := CalculateRemainingAmount(tt.sub, tt.order)
if (err != nil) != tt.wantErr {
t.Errorf("CalculateRemainingAmount() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestCalculateRemainingAmount_NoLimitWithResetCycle(t *testing.T) {
now := time.Now()
sub := Subscribe{
StartTime: now.Add(-24 * time.Hour),
ExpireTime: now.Add(24 * time.Hour),
Traffic: 1000,
Download: 300,
Upload: 200,
UnitTime: UnitTimeNoLimit,
ResetCycle: ResetCycleMonthly,
DeductionRatio: 0,
}
order := Order{
Amount: 1000,
Quantity: 1,
}
got, err := CalculateRemainingAmount(sub, order)
if err != nil {
t.Errorf("CalculateRemainingAmount() error = %v", err)
return
}
if got != 0 {
t.Errorf("CalculateRemainingAmount() = %v, want 0", got)
}
}
// Benchmark tests
func BenchmarkCalculateRemainingAmount(b *testing.B) {
now := time.Now()
sub := Subscribe{
StartTime: now.Add(-24 * time.Hour),
ExpireTime: now.Add(24 * time.Hour),
Traffic: 1000,
Download: 300,
Upload: 200,
UnitTime: UnitTimeMonth,
ResetCycle: ResetCycleNone,
DeductionRatio: 50,
}
order := Order{
Amount: 1000,
Quantity: 1,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = CalculateRemainingAmount(sub, order)
}
}
func BenchmarkSafeMultiply(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = safeMultiply(12345, 67890)
}
}
+134
View File
@@ -0,0 +1,134 @@
package email
import (
"context"
"sync"
"time"
"github.com/perfect-panel/server/pkg/logger"
"gorm.io/gorm"
)
var (
Manager *WorkerManager // 全局调度器实例
once sync.Once // 确保 Scheduler 只被初始化一次
limit sync.RWMutex // 控制并发限制
)
type WorkerManager struct {
db *gorm.DB // 数据库连接
sender Sender // 邮件发送器接口
mutex sync.RWMutex // 读写互斥锁,确保线程安全
workers map[int64]*Worker // 存储所有 Worker 实例
cancels map[int64]context.CancelFunc // 存储每个 Worker 的取消函数
}
func NewWorkerManager(db *gorm.DB, sender Sender) *WorkerManager {
if Manager != nil {
return Manager
}
once.Do(func() {
Manager = &WorkerManager{
db: db,
workers: make(map[int64]*Worker),
cancels: make(map[int64]context.CancelFunc),
sender: sender,
}
})
// 设置定时检查任务
go func() {
for {
// 每隔5分钟检查一次
select {
case <-time.After(1 * time.Minute):
checkWorker()
continue
}
}
}()
return Manager
}
// AddWorker 添加一个新的 Worker 实例
func (m *WorkerManager) AddWorker(id int64) {
m.mutex.Lock()
defer m.mutex.Unlock()
if _, exists := m.workers[id]; !exists {
ctx, cancel := context.WithCancel(context.Background())
worker := NewWorker(ctx, id, m.db, m.sender)
m.workers[id] = worker
m.cancels[id] = cancel
go worker.Start()
logger.Info("Batch Send Email",
logger.Field("message", "Added new worker"),
logger.Field("task_id", id),
)
} else {
logger.Info("Batch Send Email",
logger.Field("message", "Worker already exists"),
logger.Field("task_id", id),
)
}
}
// GetWorker 获取指定任务的 Worker 实例
func (m *WorkerManager) GetWorker(id int64) *Worker {
m.mutex.RLock()
defer m.mutex.RUnlock()
if worker, exists := m.workers[id]; exists {
return worker
} else {
logger.Error("Batch Send Email",
logger.Field("message", "Worker not found"),
logger.Field("task_id", id),
)
return nil
}
}
// RemoveWorker 移除指定任务的 Worker 实例
func (m *WorkerManager) RemoveWorker(id int64) {
m.mutex.Lock()
defer m.mutex.Unlock()
if _, exists := m.workers[id]; exists {
delete(m.workers, id)
if cancelFunc, ok := m.cancels[id]; ok {
cancelFunc() // 调用取消函数
delete(m.cancels, id)
}
logger.Info("Batch Send Email",
logger.Field("message", "Removed worker"),
logger.Field("task_id", id),
)
} else {
logger.Error("Batch Send Email",
logger.Field("message", "Worker not found for removal"),
logger.Field("task_id", id),
)
}
}
func checkWorker() {
if Manager == nil {
// 如果 Manager 未初始化,直接返回
return
}
Manager.mutex.Lock()
defer Manager.mutex.Unlock()
for id, worker := range Manager.workers {
if worker.IsRunning() == 2 {
// 如果Worker已完成,移除它
delete(Manager.workers, id)
if cancelFunc, ok := Manager.cancels[id]; ok {
cancelFunc() // 调用取消函数
delete(Manager.cancels, id)
}
logger.Info("Batch Send Email",
logger.Field("message", "Removed completed worker"),
logger.Field("task_id", id),
)
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
package email
import "github.com/perfect-panel/ppanel-server/internal/types"
import "github.com/perfect-panel/server/internal/types"
type Platform int
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"encoding/json"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/email/smtp"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/email/smtp"
"github.com/perfect-panel/server/pkg/logger"
)
type Sender interface {
-1
View File
@@ -282,7 +282,6 @@ const (
</body>
</html>
`
DefaultTrafficExceedEmailTemplate = `<!doctype html>
<html>
<head>
+192
View File
@@ -0,0 +1,192 @@
package email
import (
"context"
"encoding/json"
"time"
"github.com/perfect-panel/server/internal/model/task"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"gorm.io/gorm"
)
type ErrorInfo struct {
Error string `json:"error"`
Email string `json:"email"`
Time int64 `json:"time"`
}
type Worker struct {
id int64 // 任务ID
db *gorm.DB // 数据库连接
ctx context.Context // 上下文
sender Sender // 邮件发送器接口
status uint8 // 任务状态,0 表示未运行,1 表示运行中 2 表示已完成
}
func NewWorker(ctx context.Context, id int64, db *gorm.DB, sender Sender) *Worker {
return &Worker{
id: id,
db: db,
ctx: ctx,
sender: sender,
}
}
// GetID 获取Worker的任务ID
func (w *Worker) GetID() int64 {
return w.id
}
// IsRunning 检查Worker是否正在运行
func (w *Worker) IsRunning() uint8 {
return w.status
}
// Start 启动Worker,开始处理任务
func (w *Worker) Start() {
// 检查并发限制
limit.Lock()
defer limit.Unlock()
tx := w.db.WithContext(w.ctx)
var taskInfo task.Task
if err := tx.Model(&task.Task{}).Where("id = ?", w.id).First(&taskInfo).Error; err != nil {
logger.Error("Batch Send Email",
logger.Field("message", "Failed to find task"),
logger.Field("error", err.Error()),
logger.Field("task_id", w.id),
)
return
}
if taskInfo.Status != 0 {
logger.Error("Batch Send Email",
logger.Field("message", "Task already completed or in progress"),
logger.Field("task_id", w.id),
)
return
}
var scope task.EmailScope
if err := json.Unmarshal([]byte(taskInfo.Scope), &scope); err != nil {
logger.Error("Batch Send Email",
logger.Field("message", "Failed to parse task scope"),
logger.Field("error", err.Error()),
logger.Field("task_id", w.id),
)
return
}
if len(scope.Recipients) == 0 && len(scope.Additional) == 0 {
logger.Error("Batch Send Email",
logger.Field("message", "No recipients or additional emails provided"),
logger.Field("task_id", w.id),
)
return
}
var content task.EmailContent
if err := json.Unmarshal([]byte(taskInfo.Content), &content); err != nil {
logger.Error("Batch Send Email",
logger.Field("message", "Failed to parse task content"),
logger.Field("error", err.Error()),
logger.Field("task_id", w.id),
)
return
}
w.status = 1 // 设置状态为运行中
var recipients []string
// 解析收件人
if len(scope.Recipients) > 0 {
recipients = append(recipients, scope.Recipients...)
}
// 解析附加收件人
if len(scope.Additional) > 0 {
recipients = append(recipients, scope.Additional...)
}
// 去重和清理空字符串
recipients = tool.RemoveDuplicateElements(recipients...)
if len(recipients) == 0 {
logger.Error("Batch Send Email",
logger.Field("message", "No valid recipients found"),
logger.Field("task_id", w.id),
)
w.status = 2 // 设置状态为已完成
return
}
// 设置发送间隔时间
var intervalTime time.Duration
if scope.Interval == 0 {
intervalTime = 1 * time.Second
} else {
intervalTime = time.Duration(scope.Interval) * time.Second
}
var errors []ErrorInfo
var count uint64
for _, recipient := range recipients {
select {
case <-w.ctx.Done():
logger.Info("Batch Send Email",
logger.Field("message", "Worker stopped by context cancellation"),
logger.Field("task_id", w.id),
)
return
default:
}
if taskInfo.Status == 0 {
taskInfo.Status = 1 // 1 表示任务进行中
}
if err := w.sender.Send([]string{recipient}, content.Subject, content.Content); err != nil {
logger.Error("Batch Send Email",
logger.Field("message", "Failed to send email"),
logger.Field("error", err.Error()),
logger.Field("recipient", recipient),
logger.Field("task_id", w.id),
)
errors = append(errors, ErrorInfo{
Error: err.Error(),
Email: recipient,
Time: time.Now().Unix(),
})
text, _ := json.Marshal(errors)
taskInfo.Errors = string(text)
}
count++
taskInfo.Current = count
if err := tx.Model(&task.Task{}).Where("`id` = ?", taskInfo.Id).Save(&taskInfo).Error; err != nil {
logger.Error("Batch Send Email",
logger.Field("message", "Failed to update task progress"),
logger.Field("error", err.Error()),
logger.Field("task_id", w.id),
)
errors = append(errors, ErrorInfo{
Error: err.Error(),
Email: recipient,
Time: time.Now().Unix(),
})
w.status = 2 // 设置状态为已完成
}
time.Sleep(intervalTime)
}
taskInfo.Status = 2 // 2 表示任务已完成
w.status = 2 // 设置状态为已完成
if err := tx.Model(&task.Task{}).Where("`id` = ?", taskInfo.Id).Save(&taskInfo).Error; err != nil {
logger.Error("Batch Send Email",
logger.Field("message", "Failed to finalize task"),
logger.Field("error", err.Error()),
logger.Field("task_id", w.id),
)
} else {
logger.Info("Batch Send Email",
logger.Field("message", "Task completed successfully"),
logger.Field("task_id", w.id),
logger.Field("total_sent", count),
)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ package fs
import (
"os"
"github.com/perfect-panel/ppanel-server/pkg/hash"
"github.com/perfect-panel/server/pkg/hash"
)
// TempFileWithText creates the temporary file with the given content,
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strconv"
"sync"
"github.com/perfect-panel/ppanel-server/pkg/lang"
"github.com/perfect-panel/server/pkg/lang"
)
const (
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"sync/atomic"
"time"
"github.com/perfect-panel/ppanel-server/pkg/errorx"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/errorx"
"github.com/perfect-panel/server/pkg/logger"
"github.com/redis/go-redis/v9"
xrate "golang.org/x/time/rate"
)
+1 -1
View File
@@ -3,7 +3,7 @@ package logger
import (
"sync/atomic"
"github.com/perfect-panel/ppanel-server/pkg/color"
"github.com/perfect-panel/server/pkg/color"
)
// WithColor is a helper function to add color to a string, only in plain encoding.
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"sync/atomic"
"testing"
"github.com/perfect-panel/ppanel-server/pkg/color"
"github.com/perfect-panel/server/pkg/color"
"github.com/stretchr/testify/assert"
)
+2 -2
View File
@@ -8,13 +8,13 @@ type LogConf struct {
// console: log to console.
// file: log to file.
// volume: used in k8s, prepend the hostname to the log file name.
Mode string `yaml:"Mode" default:"console"`
Mode string `yaml:"Mode" default:"file"`
// Encoding represents the encoding type, default is `json`.
// json: json encoding.
// plain: plain text encoding, typically used in development.
Encoding string `yaml:"Encoding" default:"json"`
// TimeFormat represents the time format, default is `2006-01-02T15:04:05.000Z07:00`.
TimeFormat string `yaml:"TimeFormat" default:"2006-01-02T15:04:05.000Z07:00"`
TimeFormat string `yaml:"TimeFormat" default:"2006-01-02 15:04:05.000"`
// Path represents the log file path, default is `logs`.
Path string `yaml:"Path" default:"logs"`
// Level represents the log level, default is `info`.
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"sync/atomic"
"time"
"github.com/perfect-panel/ppanel-server/pkg/syncx"
"github.com/perfect-panel/ppanel-server/pkg/timex"
"github.com/perfect-panel/server/pkg/syncx"
"github.com/perfect-panel/server/pkg/timex"
)
type limitedExecutor struct {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/perfect-panel/ppanel-server/pkg/timex"
"github.com/perfect-panel/server/pkg/timex"
"github.com/stretchr/testify/assert"
)
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"io"
"testing"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
)
type Buffer struct {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"errors"
"testing"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
"github.com/stretchr/testify/assert"
)
+98
View File
@@ -0,0 +1,98 @@
package logger
import (
"bufio"
"fmt"
"io"
"os"
)
func ReadLastNLines(path string, n int) ([]string, error) {
// Open the file
file, err := os.Open(fmt.Sprintf("%s/%s", path, accessFilename))
if err != nil {
return nil, err
}
defer file.Close()
// Get file size
fileInfo, err := file.Stat()
if err != nil {
return nil, err
}
fileSize := fileInfo.Size()
// If file is empty, return empty slice
if fileSize == 0 {
return []string{}, nil
}
// Buffer for reading
bufferSize := int64(4096)
if bufferSize > fileSize {
bufferSize = fileSize
}
buffer := make([]byte, bufferSize)
// Start reading from the end
position := fileSize
lines := make([]string, 0, n)
lineCount := 0
for lineCount < n && position > 0 {
// How much to read
readSize := bufferSize
if position < bufferSize {
readSize = position
}
position -= readSize
// Read chunk from position
_, err := file.Seek(position, io.SeekStart)
if err != nil {
return nil, err
}
_, err = file.Read(buffer[:readSize])
if err != nil {
return nil, err
}
// Count newlines in reverse
for i := readSize - 1; i >= 0; i-- {
if buffer[i] == '\n' {
lineCount++
if lineCount > n {
// We found more than n lines
// Need to adjust position to read only last n lines
position += int64(i) + 1
break
}
}
}
}
// If we couldn't find n lines, start from beginning
if position < 0 {
position = 0
}
// Seek to the position where we want to start reading
_, err = file.Seek(position, io.SeekStart)
if err != nil {
return nil, err
}
// Read lines from position to end
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
// Check if we need to trim
if len(lines) > n {
lines = lines[len(lines)-n:]
}
return lines, scanner.Err()
}
+16
View File
@@ -0,0 +1,16 @@
package logger
import (
"testing"
)
func TestReadLastNLines(t *testing.T) {
t.Skipf("skip this test until this test fails")
lines, err := ReadLastNLines("/Users/tension/code/ppanel/server/logs", 10)
if err != nil {
t.Fatalf("Error reading last N lines: %v", err)
}
for i, line := range lines {
t.Logf("Line %d: %s", i, line)
}
}
+2 -2
View File
@@ -5,9 +5,9 @@ import (
"fmt"
"time"
"github.com/perfect-panel/ppanel-server/internal/trace"
"github.com/perfect-panel/server/internal/trace"
"github.com/perfect-panel/ppanel-server/pkg/timex"
"github.com/perfect-panel/server/pkg/timex"
)
// WithCallerSkip returns a Logger with given caller skip.
+2 -2
View File
@@ -13,8 +13,8 @@ import (
"sync"
"time"
"github.com/perfect-panel/ppanel-server/pkg/fs"
"github.com/perfect-panel/ppanel-server/pkg/lang"
"github.com/perfect-panel/server/pkg/fs"
"github.com/perfect-panel/server/pkg/lang"
)
const (
+2 -2
View File
@@ -11,9 +11,9 @@ import (
"testing"
"time"
"github.com/perfect-panel/ppanel-server/pkg/random"
"github.com/perfect-panel/server/pkg/random"
"github.com/perfect-panel/ppanel-server/pkg/fs"
"github.com/perfect-panel/server/pkg/fs"
"github.com/stretchr/testify/assert"
)
+1 -1
View File
@@ -3,7 +3,7 @@ package logger
import (
"errors"
"github.com/perfect-panel/ppanel-server/pkg/syncx"
"github.com/perfect-panel/server/pkg/syncx"
)
const (
+2 -2
View File
@@ -12,8 +12,8 @@ import (
"sync/atomic"
fatihcolor "github.com/fatih/color"
"github.com/perfect-panel/ppanel-server/pkg/color"
"github.com/perfect-panel/ppanel-server/pkg/errorx"
"github.com/perfect-panel/server/pkg/color"
"github.com/perfect-panel/server/pkg/errorx"
)
type (
+7 -2
View File
@@ -8,10 +8,15 @@ import (
func TestNewNodeMultiplierManager(t *testing.T) {
periods := []TimePeriod{
{
StartTime: "23:00",
EndTime: "1:59",
StartTime: "23:00.000",
EndTime: "1:59.000",
Multiplier: 1.2,
},
{
StartTime: "12:00.000",
EndTime: "13:59.000",
Multiplier: 0.5,
},
}
m := NewNodeMultiplierManager(periods)
if len(m.Periods) != 1 {
+2 -2
View File
@@ -28,8 +28,8 @@ func (m *Manager) GetMultiplier(current time.Time) float32 {
}
func (m *Manager) isInTimePeriod(current time.Time, start, end string) bool {
startTime, _ := time.Parse("15:04", start)
endTime, _ := time.Parse("15:04", end)
startTime, _ := time.Parse("15:04.000", start)
endTime, _ := time.Parse("15:04.000", end)
currentTime := time.Date(0, 1, 1, current.Hour(), current.Minute(), 0, 0, time.UTC)
startTimeFormatted := time.Date(0, 1, 1, startTime.Hour(), startTime.Minute(), 0, 0, time.UTC)
+1 -1
View File
@@ -11,7 +11,7 @@
AppleID.auth.init({
clientId: 'web.jiashus.com', // 替换为你的服务 ID
scope: 'name email', // 可选,授权范围
redirectURI: 'https://test.muran.org:8443/auth/apple/callback', // 替换为你的回调 URL
redirectURI: 'https://test.ppanel.dev:8443/auth/apple/callback', // 替换为你的回调 URL
state: 'optional-csrf-token', // 可选
usePopup: false // 可选,是否使用弹窗方式
});
+1 -1
View File
@@ -39,7 +39,7 @@ func handleAppleCallBack(ctx context.Context, request CallbackRequest) {
ClientID: ClientID,
KeyID: KeyID,
ClientSecret: ClientSecret,
RedirectURI: "https://test.muran.org:8443/auth/apple/callback",
RedirectURI: "https://test.ppanel.dev:8443/auth/apple/callback",
})
if err != nil {
fmt.Println("error creating apple client: " + err.Error())
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"encoding/json"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"fmt"
"time"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
"gorm.io/driver/mysql"
"gorm.io/gorm"

Some files were not shown because too many files have changed in this diff Show More