├── Docker ├── Caddy_V2ray │ ├── Caddyfile │ ├── Dockerfile │ ├── docker-compose.yml │ └── index.html ├── V2ray │ └── docker-compose.yml ├── alpine │ ├── Dockerfile │ ├── config.json │ └── runV2ray.sh └── arm │ └── Dockerfile ├── Manager └── Manager.go ├── README.md ├── client ├── gRPCClient.go ├── handlerServiceClient.go ├── statsServiceClient.go └── userRuleServerClient.go ├── config ├── cipher.go └── config.go ├── db ├── db.go ├── mysql.go ├── sspanel.go ├── sspaneltabel.go ├── ssrpanel.go ├── ssrpaneltabel.go └── webapi.go ├── go-panel.go ├── install-release.sh ├── install.sh ├── model └── model.go ├── panel.go ├── plugin.go ├── speedtest ├── client.go ├── config.go ├── coordinates.go ├── download.go ├── latency.go ├── latency_test.go ├── opts.go ├── server.go ├── speedtest_thread.go ├── upload.go ├── upload_test.go └── version.go ├── utility └── utility.go └── webapi └── webapi.go /Docker/Caddy_V2ray/Caddyfile: -------------------------------------------------------------------------------- 1 | {$V2RAY_DOMAIN}:{$V2RAY_OUTSIDE_PORT} 2 | { 3 | root /srv/www 4 | log ./caddy.log 5 | proxy {$V2RAY_PATH} 127.0.0.1:{$V2RAY_PORT} { 6 | websocket 7 | header_upstream -Origin 8 | } 9 | gzip 10 | tls {$V2RAY_EMAIL} { 11 | protocols tls1.0 tls1.2 12 | # remove comment if u want to use cloudflare (for DNS challenge authentication) 13 | # dns cloudflare 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Docker/Caddy_V2ray/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jessestuart/caddy-cloudflare:v0.11.0 2 | 3 | RUN mkdir /srv/www 4 | COPY index.html /srv/www/index.html 5 | EXPOSE 80 443 2015 6 | VOLUME /root/.caddy /srv 7 | WORKDIR /srv 8 | 9 | ENTRYPOINT ["/bin/parent", "caddy"] 10 | CMD ["--conf", "/etc/Caddyfile", "--log", "stdout", "--agree=$ACME_AGREE"] -------------------------------------------------------------------------------- /Docker/Caddy_V2ray/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | v2ray: 5 | image: githubphone/v2ray_v3:go_pay 6 | restart: always 7 | network_mode: "host" 8 | environment: 9 | sspanel_url: "https://xxxx" 10 | key: "xxxx" 11 | docker: "true" 12 | speedtest: 6 13 | node_id: 10 14 | api_port: 2333 15 | downWithPanel: 1 16 | TZ: "Asia/Shanghai" 17 | MYSQLHOST: "https://bing.com" 18 | MYSQLDBNAME: "demo_dbname" 19 | MYSQLUSR: "demo_user" 20 | MYSQLPASSWD: "demo_dbpassword" 21 | MYSQLPORT: 3306 22 | PANELTYPE: 0 23 | usemysql: 0 24 | volumes: 25 | - /etc/localtime:/etc/localtime:ro 26 | logging: 27 | options: 28 | max-size: "10m" 29 | max-file: "3" 30 | 31 | caddy: 32 | image: githubphone/v2ray_v3:caddy 33 | restart: always 34 | environment: 35 | - ACME_AGREE=true 36 | # if u want to use cloudflare (for DNS challenge authentication) 37 | # - CLOUDFLARE_EMAIL=xxxxxx@out.look.com 38 | # - CLOUDFLARE_API_KEY=xxxxxxx 39 | - V2RAY_DOMAIN=xxxx.com 40 | - V2RAY_PATH=/v2ray 41 | - V2RAY_EMAIL=xxxx@outlook.com 42 | - V2RAY_PORT=10550 43 | - V2RAY_OUTSIDE_PORT=443 44 | network_mode: "host" 45 | volumes: 46 | - ./.caddy:/root/.caddy 47 | - ./Caddyfile:/etc/Caddyfile 48 | -------------------------------------------------------------------------------- /Docker/Caddy_V2ray/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | / 8 | 35 | 36 | 37 |

Across the Great Wall we can reach every corner in the world...

38 | 39 | -------------------------------------------------------------------------------- /Docker/V2ray/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | v2ray: 5 | image: githubphone/v2ray_v3:go_pay 6 | restart: always 7 | network_mode: "host" 8 | environment: 9 | sspanel_url: "https://xxxx" 10 | key: "xxxx" 11 | speedtest: 6 12 | node_id: 10 13 | api_port: 2333 14 | downWithPanel: 1 15 | TZ: "Asia/Shanghai" 16 | MYSQLHOST: "https://bing.com" 17 | MYSQLDBNAME: "demo_dbname" 18 | MYSQLUSR: "demo_user" 19 | MYSQLPASSWD: "demo_dbpassword" 20 | MYSQLPORT: 3306 21 | PANELTYPE: 0 22 | usemysql: 0 23 | volumes: 24 | - /etc/localtime:/etc/localtime:ro 25 | logging: 26 | options: 27 | max-size: "10m" 28 | max-file: "3" -------------------------------------------------------------------------------- /Docker/alpine/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest as builder 2 | 3 | LABEL maintainer="Rico " 4 | 5 | ENV v2ray_version=4.18.4 6 | 7 | RUN apt-get update 8 | RUN apt-get install curl -y 9 | RUN curl -L -o /tmp/go.sh https://raw.githubusercontent.com/githubphone/pay-v2ray-sspanel-v3-mod_Uim-plugin/master/install-release.sh 10 | RUN chmod +x /tmp/go.sh 11 | RUN /tmp/go.sh --version ${v2ray_version} --panelurl https://google.com --panelkey 55fUxDGFzH3n --nodeid 123456 --downwithpanel 1 --mysqlhost https://bing.com --mysqldbname demo_dbname --mysqluser demo_user --mysqlpasswd demo_dbpassword --mysqlport 3306 --speedtestrate 6 --paneltype 0 --usemysql 0 12 | 13 | FROM alpine:latest 14 | 15 | COPY --from=builder /usr/bin/v2ray/v2ray /usr/bin/v2ray/ 16 | COPY --from=builder /usr/bin/v2ray/v2ctl /usr/bin/v2ray/ 17 | COPY --from=builder /usr/bin/v2ray/geoip.dat /usr/bin/v2ray/ 18 | COPY --from=builder /usr/bin/v2ray/geosite.dat /usr/bin/v2ray/ 19 | COPY config.json /etc/v2ray/config.json 20 | COPY runV2ray.sh /usr/bin/v2ray/runV2ray.sh 21 | RUN set -ex && \ 22 | apk --no-cache add dcron ca-certificates openssl coreutils bind-tools curl socat && \ 23 | update-ca-certificates && \ 24 | apk add --update tzdata && \ 25 | mkdir /var/log/v2ray/ && \ 26 | chmod +x /usr/bin/v2ray/v2ctl && \ 27 | chmod +x /usr/bin/v2ray/v2ray && \ 28 | chmod +x /usr/bin/v2ray/runV2ray.sh && \ 29 | rm -rf /var/cache/apk/* 30 | 31 | RUN mkdir -p /var/log/cron && mkdir -m 0644 -p /var/spool/cron/crontabs && touch /var/log/cron/cron.log && mkdir -m 0644 -p /etc/cron.d 32 | 33 | 34 | #Install 35 | 36 | RUN curl https://get.acme.sh | sh 37 | 38 | 39 | RUN ln -s /root/.acme.sh/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null##' | crontab - 40 | 41 | ENV TZ=Asia/Shanghai 42 | ENV PATH /usr/bin/v2ray:$PATH 43 | VOLUME /var/log/v2ray/ /root/.acme.sh/ 44 | WORKDIR /var/log/v2ray/ 45 | 46 | CMD sh /usr/bin/v2ray/runV2ray.sh 47 | -------------------------------------------------------------------------------- /Docker/alpine/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "services": [ 4 | "HandlerService", 5 | "LoggerService", 6 | "StatsService", 7 | "RuleService" 8 | ], 9 | "tag": "api" 10 | }, 11 | "inbounds": [{ 12 | "listen": "127.0.0.1", 13 | "port": 2333, 14 | "protocol": "dokodemo-door", 15 | "settings": { 16 | "address": "127.0.0.1" 17 | }, 18 | "tag": "api" 19 | } 20 | ], 21 | "log": { 22 | "loglevel": "info" 23 | }, 24 | "outbounds": [{ 25 | "protocol": "freedom", 26 | "settings": {} 27 | }, 28 | { 29 | "protocol": "blackhole", 30 | "settings": {}, 31 | "tag": "blocked" 32 | } 33 | ], 34 | "policy": { 35 | "levels": { 36 | "0": { 37 | "connIdle": 300, 38 | "downlinkOnly": 5, 39 | "handshake": 4, 40 | "statsUserDownlink": true, 41 | "statsUserUplink": true, 42 | "uplinkOnly": 2 43 | } 44 | }, 45 | "system": { 46 | "statsInboundDownlink": false, 47 | "statsInboundUplink": false 48 | } 49 | }, 50 | "reverse": {}, 51 | "routing": { 52 | "settings": { 53 | "rules": [{ 54 | "ip": [ 55 | "0.0.0.0/8", 56 | "10.0.0.0/8", 57 | "100.64.0.0/10", 58 | "127.0.0.0/8", 59 | "169.254.0.0/16", 60 | "172.16.0.0/12", 61 | "192.0.0.0/24", 62 | "192.0.2.0/24", 63 | "192.168.0.0/16", 64 | "198.18.0.0/15", 65 | "198.51.100.0/24", 66 | "203.0.113.0/24", 67 | "::1/128", 68 | "fc00::/7", 69 | "fe80::/10" 70 | ], 71 | "outboundTag": "blocked", 72 | "protocol": [ 73 | "bittorrent" 74 | ], 75 | "type": "field" 76 | }, 77 | { 78 | "inboundTag": [ 79 | "api" 80 | ], 81 | "outboundTag": "api", 82 | "type": "field" 83 | }, 84 | { 85 | "domain": [ 86 | "regexp:(api|ps|sv|offnavi|newvector|ulog\\.imap|newloc)(\\.map|)\\.(baidu|n\\.shifen)\\.com", 87 | "regexp:(.+\\.|^)(360|so)\\.(cn|com)", 88 | "regexp:(.?)(xunlei|sandai|Thunder|XLLiveUD)(.)" 89 | ], 90 | "outboundTag": "blocked", 91 | "type": "field" 92 | } 93 | ] 94 | }, 95 | "strategy": "rules" 96 | }, 97 | "stats": {}, 98 | "sspanel": { 99 | "nodeid": 123456, 100 | "checkRate": 60, 101 | "SpeedTestCheckRate": 6, 102 | "panelUrl": "https://google.com", 103 | "panelKey": "55fUxDGFzH3n", 104 | "downWithPanel": 1, 105 | "mu_regex": "%5m%id.%suffix", 106 | "mu_suffix": "microsoft.com", 107 | "mysql": { 108 | "host": "https://bing.com", 109 | "port": 3306, 110 | "user": "demo_user", 111 | "password": "demo_dbpassword", 112 | "dbname": "demo_dbname" 113 | }, 114 | "paneltype": 0, 115 | "usemysql": 0, 116 | "go_panel_host": "bwg.com", 117 | "go_panel_key": "bbbbaaaaaaa" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Docker/alpine/runV2ray.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -z "${api_port}" ] 4 | then 5 | sed -i "s|\"port\": 2333,|\"port\": ${api_port},|" "/etc/v2ray/config.json" 6 | fi 7 | if [ ! -z "${sspanel_url}" ] 8 | then 9 | sed -i "s|\"https://google.com\"|\"${sspanel_url}\"|g" "/etc/v2ray/config.json" 10 | fi 11 | if [ ! -z "${key}" ] 12 | then 13 | sed -i "s/\"55fUxDGFzH3n\"/\"${key}\"/g" "/etc/v2ray/config.json" 14 | fi 15 | 16 | if [ ! -z "${node_id}" ] 17 | then 18 | sed -i "s/123456/${node_id}/g" "/etc/v2ray/config.json" 19 | fi 20 | 21 | if [ ! -z "${speedtest}" ] 22 | then 23 | sed -i "s/\"SpeedTestCheckRate\": 6/\"SpeedTestCheckRate\": ${speedtest}/g" "/etc/v2ray/config.json" 24 | fi 25 | if [ ! -z "${downWithPanel}" ] 26 | then 27 | sed -i "s/\"downWithPanel\": 6/\"downWithPanel\": ${downWithPanel}/g" "/etc/v2ray/config.json" 28 | fi 29 | 30 | if [ ! -z "${MYSQLHOST}" ] 31 | then 32 | sed -i "s|"https://bing.com"|"${MYSQLHOST}"|g" "/etc/v2ray/config.json" 33 | fi 34 | 35 | if [ ! -z "${MYSQLDBNAME}" ] 36 | then 37 | sed -i "s/"demo_dbname"/"${MYSQLDBNAME}"/g" "/etc/v2ray/config.json" 38 | fi 39 | 40 | if [ ! -z "${MYSQLUSR}" ] 41 | then 42 | sed -i "s|\"demo_user\"|\"${MYSQLUSR}\"|g" "/etc/v2ray/config.json" 43 | fi 44 | if [ ! -z "${MYSQLPASSWD}" ] 45 | then 46 | sed -i "s/"demo_dbpassword"/"${MYSQLPASSWD}"/g" "/etc/v2ray/config.json" 47 | fi 48 | if [ ! -z "${MYSQLPORT}" ] 49 | then 50 | sed -i "s/3306,/${MYSQLPORT},/g" "/etc/v2ray/config.json" 51 | fi 52 | 53 | if [ ! -z "${PANELTYPE}" ] 54 | then 55 | sed -i "s|\"paneltype\": 0|\"paneltype\": ${PANELTYPE}|g" "/etc/v2ray/config.json" 56 | fi 57 | 58 | if [ ! -z "${usemysql}" ] 59 | then 60 | sed -i "s|\"usemysql\": 0|\"usemysql\": ${usemysql}|g" "/etc/v2ray/config.json" 61 | fi 62 | cat /etc/v2ray/config.json 63 | v2ray -config=/etc/v2ray/config.json -------------------------------------------------------------------------------- /Docker/arm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM armv7/armhf-ubuntu:latest as builder 2 | ENV v2ray_version=4.12.0 3 | RUN apt-get install curl -y 4 | RUN curl -L -o /tmp/go.sh https://install.direct/go.sh 5 | RUN chmod +x /tmp/go.sh 6 | RUN /tmp/go.sh --version ${v2ray_version} 7 | 8 | FROM arm32v6/alpine:latest 9 | 10 | COPY --from=builder /usr/bin/v2ray/v2ray /usr/bin/v2ray/ 11 | COPY --from=builder /usr/bin/v2ray/v2ctl /usr/bin/v2ray/ 12 | COPY --from=builder /usr/bin/v2ray/geoip.dat /usr/bin/v2ray/ 13 | COPY --from=builder /usr/bin/v2ray/geosite.dat /usr/bin/v2ray/ 14 | COPY --from=builder /etc/v2ray/config.json /etc/v2ray/config.json 15 | 16 | RUN set -ex && \ 17 | apk --no-cache add ca-certificates && \ 18 | mkdir /var/log/v2ray/ &&\ 19 | chmod +x /usr/bin/v2ray/v2ctl && \ 20 | chmod +x /usr/bin/v2ray/v2ray 21 | 22 | ENV PATH /usr/bin/v2ray:$PATH 23 | 24 | CMD ["v2ray", "-config=/etc/v2ray/config.json"] -------------------------------------------------------------------------------- /Manager/Manager.go: -------------------------------------------------------------------------------- 1 | package Manager 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/client" 8 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/model" 9 | "os" 10 | "os/exec" 11 | "os/user" 12 | "strconv" 13 | "v2ray.com/core/common/errors" 14 | "v2ray.com/core/common/net" 15 | "v2ray.com/core/common/serial" 16 | "v2ray.com/core/infra/conf" 17 | "v2ray.com/core/transport/internet" 18 | ) 19 | 20 | type Manager struct { 21 | HandlerServiceClient *client.HandlerServiceClient 22 | StatsServiceClient *client.StatsServiceClient 23 | RuleServiceClient *client.RuleServerClient 24 | CurrentNodeInfo *model.NodeInfo 25 | NextNodeInfo *model.NodeInfo 26 | UserChanged bool 27 | UserToBeMoved map[string]model.UserModel 28 | UserToBeAdd map[string]model.UserModel 29 | Users map[string]model.UserModel 30 | Id2PrefixedIdmap map[uint]string 31 | Id2DisServer map[uint]string 32 | MainAddress string 33 | MainListenPort uint16 34 | NodeID uint 35 | CheckRate int 36 | SpeedTestCheckRate int 37 | } 38 | 39 | func homeDir() string { 40 | usr, err := user.Current() 41 | if err != nil { 42 | os.Exit(1) 43 | } 44 | return usr.HomeDir 45 | } 46 | 47 | func (manager *Manager) GetUsers() map[string]model.UserModel { 48 | return manager.Users 49 | } 50 | 51 | func (manager *Manager) Add(user model.UserModel) { 52 | manager.UserChanged = true 53 | manager.UserToBeAdd[user.PrefixedId] = user 54 | } 55 | 56 | func (manager *Manager) Remove(prefixedId string) bool { 57 | user, ok := manager.Users[prefixedId] 58 | if ok { 59 | manager.UserChanged = true 60 | manager.UserToBeMoved[user.PrefixedId] = user 61 | return true 62 | } 63 | return false 64 | } 65 | 66 | func (manager *Manager) UpdataUsers() { 67 | var successfully_removed, successfully_add []string 68 | if manager.CurrentNodeInfo.Server_raw != "" { 69 | if manager.CurrentNodeInfo.Sort == 0 || manager.CurrentNodeInfo.Sort == 13 { 70 | // SS server 71 | /// remove inbounds 72 | for key, value := range manager.UserToBeMoved { 73 | if err := manager.HandlerServiceClient.RemoveInbound(value.PrefixedId); err == nil { 74 | newErrorf("Successfully remove user %s", key).AtInfo().WriteToLog() 75 | successfully_removed = append(successfully_removed, key) 76 | } else { 77 | newError(err).AtDebug().WriteToLog() 78 | successfully_removed = append(successfully_removed, key) 79 | } 80 | if manager.CurrentNodeInfo.Sort == 13 { 81 | newErrorf("Remove AttrMachter %s", value.Muhost).AtInfo().WriteToLog() 82 | manager.HandlerServiceClient.RemoveOutbound("out_" + value.Muhost) 83 | manager.RuleServiceClient.RemveUserAttrMachter("out_" + value.Muhost) 84 | manager.HandlerServiceClient.DelUser(value.Muhost) 85 | } 86 | } 87 | } else if manager.CurrentNodeInfo.Sort == 11 || manager.CurrentNodeInfo.Sort == 12 { 88 | // VMESS 89 | // Remove users 90 | for key, value := range manager.UserToBeMoved { 91 | if err := manager.HandlerServiceClient.DelUser(value.Email); err == nil { 92 | newErrorf("Successfully remove user %s ", key).AtInfo().WriteToLog() 93 | successfully_removed = append(successfully_removed, key) 94 | } else { 95 | newError(err).AtDebug().WriteToLog() 96 | successfully_removed = append(successfully_removed, key) 97 | } 98 | } 99 | } 100 | 101 | } 102 | if manager.NextNodeInfo.Server_raw != "" { 103 | if manager.NextNodeInfo.Sort == 0 || manager.NextNodeInfo.Sort == 13 { 104 | // SS server 105 | /// add inbounds 106 | for key, value := range manager.UserToBeAdd { 107 | var streamsetting *internet.StreamConfig 108 | if manager.NextNodeInfo.Sort == 13 { 109 | newErrorf("ADD WS+SS %s %s", key, value.Muhost).AtInfo().WriteToLog() 110 | streamsetting = client.GetDomainsocketStreamConfig(fmt.Sprintf("/etc/v2ray/%s.sock", value.Muhost)) 111 | manager.RuleServiceClient.AddUserAttrMachter("out_"+value.Muhost, fmt.Sprintf("attrs['host'] == '%s'", value.Muhost)) 112 | manager.HandlerServiceClient.AddFreedomOutbound("out_"+value.Muhost, streamsetting) 113 | manager.HandlerServiceClient.AddDokodemoUser(value) 114 | } 115 | if err := manager.HandlerServiceClient.AddSSInbound(value, "0.0.0.0", streamsetting); err == nil { 116 | newErrorf("Successfully add user %s ", key).AtInfo().WriteToLog() 117 | successfully_add = append(successfully_add, key) 118 | } else { 119 | newError(err).AtDebug().WriteToLog() 120 | successfully_add = append(successfully_add, key) 121 | } 122 | } 123 | } else if manager.NextNodeInfo.Sort == 11 || manager.NextNodeInfo.Sort == 12 { 124 | // VMESS 125 | // add users 126 | for key, value := range manager.UserToBeAdd { 127 | if err := manager.HandlerServiceClient.AddVmessUser(value); err == nil { 128 | newErrorf("Successfully add user %s ", key).AtInfo().WriteToLog() 129 | successfully_add = append(successfully_add, key) 130 | } else { 131 | newError(err).AtDebug().WriteToLog() 132 | successfully_add = append(successfully_add, key) 133 | } 134 | } 135 | } 136 | } 137 | for index := range successfully_removed { 138 | delete(manager.Users, successfully_removed[index]) 139 | delete(manager.UserToBeMoved, successfully_removed[index]) 140 | } 141 | for index := range successfully_add { 142 | manager.Users[successfully_add[index]] = manager.UserToBeAdd[successfully_add[index]] 143 | delete(manager.UserToBeAdd, successfully_add[index]) 144 | } 145 | manager.Id2PrefixedIdmap = map[uint]string{} 146 | for key, value := range manager.Users { 147 | manager.Id2PrefixedIdmap[value.UserID] = key 148 | } 149 | } 150 | 151 | func (manager *Manager) UpdateMainAddressAndProt(node_info *model.NodeInfo) { 152 | if node_info.Sort == 11 || node_info.Sort == 12 || node_info.Sort == 13 { 153 | if node_info.Server["port"] == "0" || node_info.Server["port"] == "" { 154 | manager.MainAddress = "127.0.0.1" 155 | manager.MainListenPort = 10550 156 | if node_info.Server["inside_port"] != "" { 157 | port, err := strconv.ParseUint(node_info.Server["inside_port"].(string), 10, 0) 158 | if err == nil { 159 | manager.MainListenPort = uint16(port) 160 | } 161 | } 162 | } else { 163 | manager.MainAddress = "0.0.0.0" 164 | manager.MainListenPort = 10550 165 | if node_info.Server["port"] != "" { 166 | port, err := strconv.ParseUint(node_info.Server["port"].(string), 10, 0) 167 | if err == nil { 168 | manager.MainListenPort = uint16(port) 169 | } 170 | } 171 | } 172 | } 173 | } 174 | func (m *Manager) AddCert(server string) (*serial.TypedMessage, error) { 175 | var tlsconfig *conf.TLSConfig 176 | newError("Starting Issuing Tls Cert, please make sure 80 is free").AtInfo().WriteToLog() 177 | //cmd := exec.Command(fmt.Sprintf("command: %s %s %s %s", fmt.Sprintf("%s/.acme.sh/acme.sh", homeDir()), "--issue", fmt.Sprintf("-d %s", server), "--standalone")) 178 | cmd := exec.Command("sh", "-c", fmt.Sprintf("%s/.acme.sh/acme.sh --issue -d %s --standalone", homeDir(), server)) 179 | var out bytes.Buffer 180 | var stderr bytes.Buffer 181 | cmd.Stdout = &out 182 | cmd.Stderr = &stderr 183 | cmd.Run() 184 | newError(out.String()).AtInfo().WriteToLog() 185 | newError(stderr.String()).AtInfo().WriteToLog() 186 | tlsconfig = &conf.TLSConfig{ 187 | Certs: []*conf.TLSCertConfig{&conf.TLSCertConfig{ 188 | CertFile: fmt.Sprintf("%s/.acme.sh/%s/fullchain.cer", homeDir(), server), 189 | KeyFile: fmt.Sprintf("%[1]s/.acme.sh/%[2]s/%[2]s.key", homeDir(), server), 190 | }}, 191 | InsecureCiphers: true, 192 | } 193 | cert, err := tlsconfig.Build() 194 | if err != nil { 195 | return nil, newError("Failed to build TLS config.").Base(err) 196 | } 197 | tm := serial.ToTypedMessage(cert) 198 | return tm, nil 199 | 200 | } 201 | func (m *Manager) StopCert(server string) error { 202 | newErrorf("Starting to remove %s from renew list", server).AtInfo().WriteToLog() 203 | cmd := exec.Command("sh", "-c", fmt.Sprintf("%s/.acme.sh/acme.sh --remove -d %s", homeDir(), server)) 204 | var out bytes.Buffer 205 | var stderr bytes.Buffer 206 | cmd.Stdout = &out 207 | cmd.Stderr = &stderr 208 | cmd.Run() 209 | newError(out.String()).AtInfo().WriteToLog() 210 | newError(stderr.String()).AtInfo().WriteToLog() 211 | //newErrorf("Starting Remove %s certs", server).AtInfo().WriteToLog() 212 | //cmd = exec.Command("rm", "-rf", fmt.Sprintf("%s/.acme.sh/%s/fullchain.cer", homeDir(), server), fmt.Sprintf("%[1]s/.acme.sh/%[2]s/%[2]s.key", homeDir(), server)) 213 | //cmd.Stdout = &out 214 | //cmd.Stderr = &stderr 215 | //cmd.Run() 216 | //newError(out.String()).AtInfo().WriteToLog() 217 | //newError(stderr.String()).AtInfo().WriteToLog() 218 | return nil 219 | } 220 | func (m *Manager) AddMainInbound() error { 221 | if m.NextNodeInfo.Server_raw != "" { 222 | if m.NextNodeInfo.Sort == 13 { 223 | m.UpdateMainAddressAndProt(m.NextNodeInfo) 224 | var streamsetting *internet.StreamConfig 225 | var tm *serial.TypedMessage 226 | if m.NextNodeInfo.Server["protocol"] == "ws" { 227 | host := "www.bing.com" 228 | path := "/" 229 | if m.NextNodeInfo.Server["path"] != "" { 230 | path = m.NextNodeInfo.Server["path"].(string) 231 | } 232 | if m.NextNodeInfo.Server["host"] != "" { 233 | host = m.NextNodeInfo.Server["host"].(string) 234 | } 235 | if m.NextNodeInfo.Server["protocol_param"] == "tls" && m.MainAddress == "0.0.0.0" { 236 | if m.NextNodeInfo.Server["server"] != "" { 237 | tm, _ = m.AddCert(m.NextNodeInfo.Server["server"].(string)) 238 | } else if net.ParseAddress(m.NextNodeInfo.Server["server_address"].(string)).Family() == net.AddressFamilyDomain { 239 | tm, _ = m.AddCert(m.NextNodeInfo.Server["server_address"].(string)) 240 | } 241 | } 242 | streamsetting = client.GetWebSocketStreamConfig(path, host, tm) 243 | } 244 | if err := m.HandlerServiceClient.AddDokodemoInbound(m.MainListenPort, m.MainAddress, streamsetting); err != nil { 245 | return err 246 | } else { 247 | newErrorf("Successfully add MAIN DokodemoInbound %s port %d", m.MainAddress, m.MainListenPort).AtInfo().WriteToLog() 248 | } 249 | } else if m.NextNodeInfo.Sort == 11 || m.NextNodeInfo.Sort == 12 { 250 | m.UpdateMainAddressAndProt(m.NextNodeInfo) 251 | var streamsetting *internet.StreamConfig 252 | var tm *serial.TypedMessage 253 | //var err error 254 | streamsetting = &internet.StreamConfig{} 255 | 256 | if m.NextNodeInfo.Server["protocol"] == "ws" { 257 | host := "www.bing.com" 258 | path := "/" 259 | if m.NextNodeInfo.Server["path"] != "" { 260 | path = m.NextNodeInfo.Server["path"].(string) 261 | } 262 | if m.NextNodeInfo.Server["host"] != "" { 263 | host = m.NextNodeInfo.Server["host"].(string) 264 | } 265 | if m.NextNodeInfo.Server["protocol_param"] == "tls" && m.MainAddress == "0.0.0.0" { 266 | if m.NextNodeInfo.Server["server"] != "" { 267 | tm, _ = m.AddCert(m.NextNodeInfo.Server["server"].(string)) 268 | } else if net.ParseAddress(m.NextNodeInfo.Server["server_address"].(string)).Family() == net.AddressFamilyDomain { 269 | tm, _ = m.AddCert(m.NextNodeInfo.Server["server_address"].(string)) 270 | } 271 | } 272 | streamsetting = client.GetWebSocketStreamConfig(path, host, tm) 273 | } else if m.NextNodeInfo.Server["protocol"] == "kcp" || m.NextNodeInfo.Server["protocol"] == "mkcp" { 274 | header_key := "noop" 275 | if m.NextNodeInfo.Server["protocol_param"] != "" { 276 | header_key = m.NextNodeInfo.Server["protocol_param"].(string) 277 | } 278 | streamsetting = client.GetKcpStreamConfig(header_key) 279 | } 280 | if err := m.HandlerServiceClient.AddVmessInbound(m.MainListenPort, m.MainAddress, streamsetting); err != nil { 281 | return err 282 | } else { 283 | newErrorf("Successfully add MAIN INBOUND %s port %d", m.MainAddress, m.MainListenPort).AtInfo().WriteToLog() 284 | } 285 | } 286 | 287 | } 288 | return nil 289 | } 290 | func (m *Manager) AddOuntBound(disnodeinfo *model.DisNodeInfo) error { 291 | if disnodeinfo.Server_raw != "" { 292 | if disnodeinfo.Sort == 11 || disnodeinfo.Sort == 12 { 293 | var streamsetting *internet.StreamConfig 294 | var tm *serial.TypedMessage 295 | streamsetting = &internet.StreamConfig{} 296 | 297 | if disnodeinfo.Server["protocol"] == "ws" { 298 | host := "www.bing.com" 299 | path := "/" 300 | if disnodeinfo.Server["path"] != "" { 301 | path = disnodeinfo.Server["path"].(string) 302 | } 303 | if disnodeinfo.Server["host"] != "" { 304 | host = disnodeinfo.Server["host"].(string) 305 | } 306 | if disnodeinfo.Server["protocol_param"] == "tls" { 307 | tlsconfig := &conf.TLSConfig{ 308 | InsecureCiphers: true, 309 | } 310 | cert, _ := tlsconfig.Build() 311 | tm = serial.ToTypedMessage(cert) 312 | } 313 | streamsetting = client.GetWebSocketStreamConfig(path, host, tm) 314 | } else if disnodeinfo.Server["protocol"] == "kcp" || disnodeinfo.Server["protocol"] == "mkcp" { 315 | header_key := "noop" 316 | if disnodeinfo.Server["protocol_param"] != "" { 317 | header_key = disnodeinfo.Server["protocol_param"].(string) 318 | } 319 | streamsetting = client.GetKcpStreamConfig(header_key) 320 | } 321 | if err := m.HandlerServiceClient.AddVmessOutbound(disnodeinfo.Server_raw+fmt.Sprintf("%d", disnodeinfo.UserId), disnodeinfo.Port, disnodeinfo.Server["server_address"].(string), streamsetting, m.HandlerServiceClient.ConvertVmessUser( 322 | m.Users[m.Id2PrefixedIdmap[disnodeinfo.UserId]])); err != nil { 323 | return err 324 | } else { 325 | newErrorf("Successfully add Outbound %s port %d", disnodeinfo.Server_raw+fmt.Sprintf("%d", disnodeinfo.UserId), disnodeinfo.Port).AtInfo().WriteToLog() 326 | } 327 | } 328 | if disnodeinfo.Sort == 0 { 329 | if err := m.HandlerServiceClient.AddSSOutbound(m.Users[m.Id2PrefixedIdmap[disnodeinfo.UserId]], disnodeinfo); err != nil { 330 | return newError("User Chipter %S", m.Users[m.Id2PrefixedIdmap[disnodeinfo.UserId]].Method).Base(err) 331 | } else { 332 | newErrorf("Successfully add user %s outbound %s ", m.Id2PrefixedIdmap[disnodeinfo.UserId], disnodeinfo.Server_raw).AtInfo().WriteToLog() 333 | 334 | } 335 | } 336 | m.AddUserRule(disnodeinfo.Server_raw+fmt.Sprintf("%d", disnodeinfo.UserId), m.Users[m.Id2PrefixedIdmap[disnodeinfo.UserId]].Email) 337 | } 338 | 339 | return nil 340 | } 341 | func (m *Manager) AddUserRule(tag, email string) { 342 | if err := m.RuleServiceClient.AddUserRelyRule(tag, []string{email}); err == nil { 343 | newErrorf("Successfully add user %s %s server rule ", email, tag).AtInfo().WriteToLog() 344 | } else { 345 | newError(err).AtDebug().WriteToLog() 346 | } 347 | } 348 | func (m *Manager) RemoveUserRule(email string) { 349 | 350 | if err := m.RuleServiceClient.RemveUserRelayRule([]string{email}); err == nil { 351 | newErrorf("Successfully remove user %s rule", email).AtInfo().WriteToLog() 352 | } else { 353 | newError(err).AtDebug().WriteToLog() 354 | } 355 | } 356 | 357 | func (m *Manager) RemoveInbound() { 358 | if m.CurrentNodeInfo.Server_raw != "" { 359 | if m.CurrentNodeInfo.Sort == 11 || m.CurrentNodeInfo.Sort == 12 || m.CurrentNodeInfo.Sort == 13 { 360 | m.UpdateMainAddressAndProt(m.CurrentNodeInfo) 361 | if err := m.HandlerServiceClient.RemoveInbound(m.HandlerServiceClient.InboundTag); err != nil { 362 | newError(err).AtWarning().WriteToLog() 363 | } else { 364 | newErrorf("Successfully remove main inbound %s", m.HandlerServiceClient.InboundTag).AtInfo().WriteToLog() 365 | } 366 | if m.CurrentNodeInfo.Server["protocol_param"] == "tls" && m.MainAddress == "0.0.0.0" { 367 | newError("Starting to Rmove Cert").AtInfo().WriteToLog() 368 | if m.CurrentNodeInfo.Server["server"] != "" { 369 | m.StopCert(m.CurrentNodeInfo.Server["server"].(string)) 370 | } else if net.ParseAddress(m.CurrentNodeInfo.Server["server_address"].(string)).Family() == net.AddressFamilyDomain { 371 | m.StopCert(m.CurrentNodeInfo.Server["server_address"].(string)) 372 | } 373 | 374 | } 375 | } 376 | } 377 | } 378 | func (m *Manager) RemoveOutBound(tag string, userid uint) { 379 | if err := m.HandlerServiceClient.RemoveOutbound(tag); err != nil { 380 | newError(err).AtWarning().WriteToLog() 381 | } else { 382 | newErrorf("Successfully remove outbound %s", tag).AtInfo().WriteToLog() 383 | } 384 | m.RemoveUserRule(m.Users[m.Id2PrefixedIdmap[userid]].Email) 385 | } 386 | func (m *Manager) RemoveAllUserOutBound() { 387 | for id, server := range m.Id2DisServer { 388 | m.RemoveOutBound(server+fmt.Sprintf("%d", id), id) 389 | } 390 | } 391 | func (m *Manager) CopyUsers() { 392 | jsonString, err := json.Marshal(m.Users) 393 | if err != nil { 394 | newError(err).AtWarning().WriteToLog() 395 | } 396 | var usertobemoved map[string]model.UserModel 397 | err = json.Unmarshal(jsonString, &usertobemoved) 398 | if err != nil { 399 | newError(err).AtWarning().WriteToLog() 400 | } 401 | m.UserToBeMoved = usertobemoved 402 | m.UserToBeAdd = map[string]model.UserModel{} 403 | } 404 | func (m *Manager) CopyNodeinfo() { 405 | jsonString, err := json.Marshal(m.NextNodeInfo) 406 | if err != nil { 407 | newError(err).AtWarning().WriteToLog() 408 | } 409 | currentnodeinfo := model.NodeInfo{} 410 | err = json.Unmarshal(jsonString, ¤tnodeinfo) 411 | if err != nil { 412 | newError(err).AtWarning().WriteToLog() 413 | } 414 | m.CurrentNodeInfo = ¤tnodeinfo 415 | 416 | } 417 | func (m *Manager) UpdateServer() error { 418 | m.CopyUsers() 419 | m.UpdataUsers() 420 | m.RemoveInbound() 421 | err := m.AddMainInbound() 422 | m.Users = map[string]model.UserModel{} 423 | return err 424 | } 425 | 426 | func newErrorf(format string, a ...interface{}) *errors.Error { 427 | return newError(fmt.Sprintf(format, a...)) 428 | } 429 | 430 | func newError(values ...interface{}) *errors.Error { 431 | values = append([]interface{}{"SSPanelPlugin: "}, values...) 432 | return errors.New(values...) 433 | } 434 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # v2ray-sspanel-v3-mod_Uim-plugin 2 | 3 | ## 公告 4 | 1. 在完成限速功能后,该功能开始将会是收费版本(不在提供源码,只提供二进制文件,需要联系tg @ricobb 获取授权码) 5 | ## Thanks 6 | 1. 感恩的 [ColetteContreras's repo](https://github.com/ColetteContreras/v2ray-ssrpanel-plugin). 让我一个go小白有了下手地。主要起始框架来源于这里 7 | 2. 感恩 [eycorsican](https://github.com/eycorsican) 在v2ray-core [issue](https://github.com/v2ray/v2ray-core/issues/1514), 促成了go版本提上日程 8 | 9 | 10 | # 划重点 11 | 1. 用户务必保证,host 务必填写没有被墙的地址 12 | 2. 目前端口设置为0,才会监听本地,不再是443 13 | 3. 已经适配了中转,必须用我自己维护的[panel](https://github.com/githubphone/ss-panel-v3-mod_Uim) 14 | 15 | ## 项目状态 16 | 17 | 支持 [ss-panel-v3-mod_Uim](https://github.com/NimaQu/ss-panel-v3-mod_Uim) 的 webapi。 目前自己也尝试维护了一个版本, [panel](https://github.com/githubphone/ss-panel-v3-mod_Uim) 18 | 19 | 目前只适配了流量记录、服务器是否在线、在线人数,在线ip上报、负载、中转,后端根据前端的设定自动调用 API 增加用户。 20 | 21 | v2ray 后端 kcp、tcp、ws 都是多用户共用一个端口。 22 | 23 | 也可作为 ss 后端一个用户一个端口。 24 | 25 | ## 已知 Bug 26 | 27 | ## 作为 ss 后端 28 | 29 | 面板配置是节点类型为 Shadowsocks,普通端口。 30 | 31 | 加密方式只支持: 32 | 33 | - [x] aes-256-cfb 34 | - [x] aes-128-cfb 35 | - [x] chacha20 36 | - [x] chacha20-ietf 37 | - [x] aes-256-gcm 38 | - [x] aes-128-gcm 39 | - [x] chacha20-poly1305 或称 chacha20-ietf-poly1305 40 | 41 | ## 作为 V2ray 后端 42 | 43 | 这里面板设置是节点类型v2ray, 普通端口。 v2ray的API接口默认是2333 44 | 45 | 支持 tcp,kcp、ws+(tls 由镜像 Caddy或者ngnix 提供,默认是443接口哦)。或者自己调整。 46 | 47 | [面板设置说明 主要是这个](https://github.com/NimaQu/ss-panel-v3-mod_Uim/wiki/v2ray-%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B) 48 | 49 | ~~~ 50 | 没有CDN的域名或者ip;端口(外部链接的);AlterId;协议层;;额外参数(path=/v2ray|host=xxxx.win|inside_port=10550这个端口内部监听)) 51 | 52 | // ws 示例 53 | xxxxx.com;10550;4;ws;;path=/v2ray|host=oxxxx.com 54 | 55 | // ws + tls (Caddy 提供) 56 | xxxxx.com;0;4;tls;ws;path=/v2ray|host=oxxxx.com|inside_port=10550 57 | xxxxx.com;;4;tls;ws;path=/v2ray|host=oxxxx.com|inside_port=10550 58 | 59 | 60 | 61 | // nat🐔 ws 示例 62 | xxxxx.com;11120;4;ws;;path=/v2ray|host=oxxxx.com 63 | 64 | // nat🐔 ws + tls (Caddy 提供) 65 | xxxxx.com;0;4;tls;ws;path=/v2ray|host=oxxxx.com|inside_port=10550|outside_port=11120 66 | xxxxx.com;;4;tls;ws;path=/v2ray|host=oxxxx.com|inside_port=10550|outside_port=11120 67 | ~~~ 68 | 69 | 目前的逻辑是 70 | 71 | - 如果为外部链接的端口是0或者不填,则默认监听本地127.0.0.1:inside_port 72 | - 如果外部端口设定不是 0或者空,则监听 0.0.0.0:外部设定端口,此端口为所有用户的单端口,此时 inside_port 弃用。 73 | - 默认使用 Caddy 镜像来提供 tls,控制代码不会生成 tls 相关的配置。Caddyfile 可以在Docker/Caddy_V2ray文件夹里面找到。 74 | - Nat🐔,如果要用ws+tls,则需要使用outside_port=xxx,php后端会生成订阅时候,使用outside_port覆盖port部分。 outside_port是内部映射端口, 75 | 建议内网和外网的两个端口数值一致。 76 | 77 | tcp 配置: 78 | 79 | ~~~ 80 | xxxxx.com;非0;4;tcp;; 81 | ~~~ 82 | 83 | kcp 支持所有 v2ray 的 type: 84 | 85 | - none: 默认值,不进行伪装,发送的数据是没有特征的数据包。 86 | 87 | ~~~ 88 | xxxxx.com;非0;4;kcp;noop; 89 | ~~~ 90 | 91 | - srtp: 伪装成 SRTP 数据包,会被识别为视频通话数据(如 FaceTime)。 92 | 93 | ~~~ 94 | xxxxx.com;非0;4;kcp;srtp; 95 | ~~~ 96 | 97 | - utp: 伪装成 uTP 数据包,会被识别为 BT 下载数据。 98 | 99 | ~~~ 100 | xxxxx.com;非0;4;kcp;utp; 101 | ~~~ 102 | 103 | - wechat-video: 伪装成微信视频通话的数据包。 104 | 105 | ~~~ 106 | xxxxx.com;非0;4;kcp;wechat-video; 107 | ~~~ 108 | 109 | - dtls: 伪装成 DTLS 1.2 数据包。 110 | 111 | ~~~ 112 | xxxxx.com;非0;4;kcp;dtls; 113 | ~~~ 114 | 115 | - wireguard: 伪装成 WireGuard 数据包(并不是真正的 WireGuard 协议) 。 116 | 117 | ~~~ 118 | xxxxx.com;非0;4;kcp;wireguard; 119 | ~~~ 120 | 121 | ### [可选] 安装 BBR 122 | 123 | 看 [Rat的](https://www.moerats.com/archives/387/) 124 | OpenVZ 看这里 [南琴浪](https://github.com/tcp-nanqinlang/wiki/wiki/lkl-haproxy) 125 | 126 | ~~~ 127 | wget -N --no-check-certificate "https://raw.githubusercontent.com/chiakge/Linux-NetSpeed/master/tcp.sh" && chmod +x tcp.sh && ./tcp.sh 128 | ~~~ 129 | 130 | Ubuntu 18.04 魔改 BBR 暂时有点问题,可使用以下命令安装: 131 | 132 | ~~~ 133 | wget -N --no-check-certificate "https://raw.githubusercontent.com/chiakge/Linux-NetSpeed/master/tcp.sh" 134 | apt install make gcc -y 135 | sed -i 's#/usr/bin/gcc-4.9#/usr/bin/gcc#g' '/root/tcp.sh' 136 | chmod +x tcp.sh && ./tcp.sh 137 | ~~~ 138 | ### [可选] 增加swap 139 | 整数是M 140 | ~~~ 141 | wget https://www.moerats.com/usr/shell/swap.sh && bash swap.sh 142 | ~~~ 143 | 144 | ### [推荐] 脚本部署 145 | 146 | #### Docker-compose 安装 147 | 这里一直保持最新版 148 | ~~~ 149 | mkdir v2ray-agent && \ 150 | cd v2ray-agent && \ 151 | curl https://raw.githubusercontent.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/master/install.sh -o install.sh && \ 152 | chmod +x install.sh && \ 153 | bash install.sh 154 | ~~~ 155 | 156 | 157 | #### 普通安装 158 | ##### 安装v2ray 159 | 修改了官方安装脚本 160 | 用脚本指定面板信息,请务必删除原有的config.json, 否则不会更新config.json 161 | 162 | 安装(这里保持最新版本) 163 | ~~~ 164 | bash <(curl -L -s https://raw.githubusercontent.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/master/install-release.sh) --panelurl https://xxxx --panelkey xxxx --nodeid 21 165 | ~~~ 166 | 167 | 后续升级(如果要更新到最新版本) 168 | ~~~ 169 | bash <(curl -L -s https://raw.githubusercontent.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/master/install-release.sh) 170 | ~~~ 171 | 172 | 173 | 如果要强制安装某个版本 174 | 175 | ~~~ 176 | bash <(curl -L -s https://raw.githubusercontent.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/master/install-release.sh) -f --version 4.12.0 177 | ~~~ 178 | 179 | 180 | config.json Example 181 | 182 | ~~~ 183 | { 184 | "api": { 185 | "services": [ 186 | "HandlerService", 187 | "LoggerService", 188 | "StatsService", 189 | "RuleService" 190 | ], 191 | "tag": "api" 192 | }, 193 | "inbounds": [{ 194 | "listen": "127.0.0.1", 195 | "port": 2333, 196 | "protocol": "dokodemo-door", 197 | "settings": { 198 | "address": "127.0.0.1" 199 | }, 200 | "tag": "api" 201 | } 202 | ], 203 | "log": { 204 | "access": "/var/log/v2ray/access.log", 205 | "error": "/var/log/v2ray/error.log", 206 | "loglevel": "info" 207 | }, 208 | "outbounds": [{ 209 | "protocol": "freedom", 210 | "settings": {} 211 | }, 212 | { 213 | "protocol": "blackhole", 214 | "settings": {}, 215 | "tag": "blocked" 216 | } 217 | ], 218 | "policy": { 219 | "levels": { 220 | "0": { 221 | "connIdle": 300, 222 | "downlinkOnly": 5, 223 | "handshake": 4, 224 | "statsUserDownlink": true, 225 | "statsUserUplink": true, 226 | "uplinkOnly": 2 227 | } 228 | }, 229 | "system": { 230 | "statsInboundDownlink": false, 231 | "statsInboundUplink": false 232 | } 233 | }, 234 | "reverse": {}, 235 | "routing": { 236 | "settings": { 237 | "rules": [{ 238 | "ip": [ 239 | "0.0.0.0/8", 240 | "10.0.0.0/8", 241 | "100.64.0.0/10", 242 | "127.0.0.0/8", 243 | "169.254.0.0/16", 244 | "172.16.0.0/12", 245 | "192.0.0.0/24", 246 | "192.0.2.0/24", 247 | "192.168.0.0/16", 248 | "198.18.0.0/15", 249 | "198.51.100.0/24", 250 | "203.0.113.0/24", 251 | "::1/128", 252 | "fc00::/7", 253 | "fe80::/10" 254 | ], 255 | "outboundTag": "blocked", 256 | "protocol": [ 257 | "bittorrent" 258 | ], 259 | "type": "field" 260 | }, 261 | { 262 | "inboundTag": [ 263 | "api" 264 | ], 265 | "outboundTag": "api", 266 | "type": "field" 267 | }, 268 | { 269 | "domain": [ 270 | "regexp:(api|ps|sv|offnavi|newvector|ulog\\.imap|newloc)(\\.map|)\\.(baidu|n\\.shifen)\\.com", 271 | "regexp:(.+\\.|^)(360|so)\\.(cn|com)", 272 | "regexp:(.?)(xunlei|sandai|Thunder|XLLiveUD)(.)" 273 | ], 274 | "outboundTag": "blocked", 275 | "type": "field" 276 | } 277 | ] 278 | }, 279 | "strategy": "rules" 280 | }, 281 | "stats": {}, 282 | "sspanel": { 283 | "nodeid": 123456, 284 | "checkRate": 60, 285 | "SpeedTestCheckRate": 6, 286 | "panelUrl": "https://google.com", 287 | "panelKey": "55fUxDGFzH3n", 288 | "downWithPanel": 1, 289 | "mysql": { 290 | "host": "https://bing.com", 291 | "port": 3306, 292 | "user": "demo_user", 293 | "password": "demo_dbpassword", 294 | "dbname": "demo_dbname" 295 | }, 296 | "paneltype": 0 297 | } 298 | } 299 | 300 | ~~~ 301 | ##### 安装caddy 302 | 303 | 一键安装 caddy 和cf ddns tls插件 304 | 305 | ~~~ 306 | curl https://getcaddy.com | bash -s dyndns,tls.dns.cloudflare 307 | ~~~ 308 | 309 | Caddyfile 310 | 311 | 自行修改,或者设置对应环境变量 312 | 313 | ~~~ 314 | {$V2RAY_DOMAIN}:{$V2RAY_OUTSIDE_PORT} 315 | { 316 | root /srv/www 317 | log ./caddy.log 318 | proxy {$V2RAY_PATH} 127.0.0.1:{$V2RAY_PORT} { 319 | websocket 320 | header_upstream -Origin 321 | } 322 | gzip 323 | tls {$V2RAY_EMAIL} { 324 | protocols tls1.0 tls1.2 325 | # remove comment if u want to use cloudflare ddns 326 | # dns cloudflare 327 | } 328 | } 329 | ~~~ 330 | -------------------------------------------------------------------------------- /client/gRPCClient.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "google.golang.org/grpc" 5 | "time" 6 | ) 7 | 8 | func ConnectGRPC(address string, timeoutDuration time.Duration) (conn *grpc.ClientConn, err error) { 9 | timeout := time.After(timeoutDuration) 10 | tick := time.Tick(500 * time.Millisecond) 11 | 12 | for { 13 | select { 14 | case <-timeout: 15 | return 16 | case <-tick: 17 | conn, err = grpc.Dial(address, grpc.WithInsecure()) 18 | if err == nil { 19 | return 20 | } 21 | } 22 | } 23 | 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /client/handlerServiceClient.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/model" 7 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/utility" 8 | "google.golang.org/grpc" 9 | "strings" 10 | "v2ray.com/core" 11 | "v2ray.com/core/app/proxyman" 12 | "v2ray.com/core/app/proxyman/command" 13 | "v2ray.com/core/common/net" 14 | "v2ray.com/core/common/protocol" 15 | "v2ray.com/core/common/serial" 16 | "v2ray.com/core/common/uuid" 17 | "v2ray.com/core/proxy/dokodemo" 18 | "v2ray.com/core/proxy/freedom" 19 | "v2ray.com/core/proxy/mtproto" 20 | "v2ray.com/core/proxy/shadowsocks" 21 | "v2ray.com/core/proxy/vmess" 22 | "v2ray.com/core/proxy/vmess/inbound" 23 | "v2ray.com/core/proxy/vmess/outbound" 24 | "v2ray.com/core/transport/internet" 25 | "v2ray.com/core/transport/internet/domainsocket" 26 | "v2ray.com/core/transport/internet/headers/noop" 27 | "v2ray.com/core/transport/internet/headers/srtp" 28 | "v2ray.com/core/transport/internet/headers/tls" 29 | "v2ray.com/core/transport/internet/headers/utp" 30 | "v2ray.com/core/transport/internet/headers/wechat" 31 | "v2ray.com/core/transport/internet/headers/wireguard" 32 | "v2ray.com/core/transport/internet/kcp" 33 | "v2ray.com/core/transport/internet/websocket" 34 | ) 35 | 36 | var KcpHeadMap = map[string]*serial.TypedMessage{ 37 | "wechat-video": serial.ToTypedMessage(&wechat.VideoConfig{}), 38 | "srtp": serial.ToTypedMessage(&srtp.Config{}), 39 | "utp": serial.ToTypedMessage(&utp.Config{}), 40 | "wireguard": serial.ToTypedMessage(&wireguard.WireguardConfig{}), 41 | "dtls": serial.ToTypedMessage(&tls.PacketConfig{}), 42 | "noop": serial.ToTypedMessage(&noop.Config{}), 43 | } 44 | var CipherTypeMap = map[string]shadowsocks.CipherType{ 45 | "aes-256-cfb": shadowsocks.CipherType_AES_256_CFB, 46 | "aes-128-cfb": shadowsocks.CipherType_AES_128_CFB, 47 | "aes-128-gcm": shadowsocks.CipherType_AES_128_GCM, 48 | "aes-256-gcm": shadowsocks.CipherType_AES_256_GCM, 49 | "chacha20": shadowsocks.CipherType_CHACHA20, 50 | "chacha20-ietf": shadowsocks.CipherType_CHACHA20_IETF, 51 | "chacha20-ploy1305": shadowsocks.CipherType_CHACHA20_POLY1305, 52 | "chacha20-ietf-poly1305": shadowsocks.CipherType_CHACHA20_POLY1305, 53 | "xchacha20-ietf-poly1305": shadowsocks.CipherType_XCHACHA20_POLY1305, 54 | } 55 | 56 | type HandlerServiceClient struct { 57 | command.HandlerServiceClient 58 | InboundTag string 59 | } 60 | 61 | func NewHandlerServiceClient(client *grpc.ClientConn, inboundTag string) *HandlerServiceClient { 62 | return &HandlerServiceClient{ 63 | HandlerServiceClient: command.NewHandlerServiceClient(client), 64 | InboundTag: inboundTag, 65 | } 66 | } 67 | 68 | // user 69 | func (h *HandlerServiceClient) DelUser(user string) error { 70 | req := &command.AlterInboundRequest{ 71 | Tag: h.InboundTag, 72 | Operation: serial.ToTypedMessage(&command.RemoveUserOperation{Email: user}), 73 | } 74 | return h.AlterInbound(req) 75 | } 76 | 77 | func (h *HandlerServiceClient) AddVmessUser(user model.UserModel) error { 78 | req := &command.AlterInboundRequest{ 79 | Tag: h.InboundTag, 80 | Operation: serial.ToTypedMessage(&command.AddUserOperation{User: h.ConvertVmessUser(user)}), 81 | } 82 | return h.AlterInbound(req) 83 | } 84 | func (h *HandlerServiceClient) AddDokodemoUser(user model.UserModel) error { 85 | req := &command.AlterInboundRequest{ 86 | Tag: h.InboundTag, 87 | Operation: serial.ToTypedMessage(&command.AddUserOperation{User: h.ConverDokodemoUser(user)}), 88 | } 89 | return h.AlterInbound(req) 90 | } 91 | 92 | func (h *HandlerServiceClient) AlterInbound(req *command.AlterInboundRequest) error { 93 | _, err := h.HandlerServiceClient.AlterInbound(context.Background(), req) 94 | return err 95 | } 96 | 97 | //streaming 98 | func GetKcpStreamConfig(headkey string) *internet.StreamConfig { 99 | var streamsetting internet.StreamConfig 100 | head, _ := KcpHeadMap["noop"] 101 | if _, ok := KcpHeadMap[headkey]; ok { 102 | head, _ = KcpHeadMap[headkey] 103 | } 104 | streamsetting = internet.StreamConfig{ 105 | ProtocolName: "mkcp", 106 | TransportSettings: []*internet.TransportConfig{ 107 | &internet.TransportConfig{ 108 | ProtocolName: "mkcp", 109 | Settings: serial.ToTypedMessage( 110 | &kcp.Config{ 111 | HeaderConfig: head, 112 | }), 113 | }, 114 | }, 115 | } 116 | return &streamsetting 117 | } 118 | 119 | func GetWebSocketStreamConfig(path string, host string, tm *serial.TypedMessage) *internet.StreamConfig { 120 | var streamsetting internet.StreamConfig 121 | if tm == nil { 122 | streamsetting = internet.StreamConfig{ 123 | ProtocolName: "websocket", 124 | TransportSettings: []*internet.TransportConfig{ 125 | &internet.TransportConfig{ 126 | ProtocolName: "websocket", 127 | Settings: serial.ToTypedMessage(&websocket.Config{ 128 | Path: path, 129 | Header: []*websocket.Header{ 130 | &websocket.Header{ 131 | Key: "Hosts", 132 | Value: host, 133 | }, 134 | }, 135 | }), 136 | }, 137 | }, 138 | } 139 | } else { 140 | streamsetting = internet.StreamConfig{ 141 | ProtocolName: "websocket", 142 | TransportSettings: []*internet.TransportConfig{ 143 | &internet.TransportConfig{ 144 | ProtocolName: "websocket", 145 | Settings: serial.ToTypedMessage(&websocket.Config{ 146 | Path: path, 147 | Header: []*websocket.Header{ 148 | &websocket.Header{ 149 | Key: "Hosts", 150 | Value: host, 151 | }, 152 | }, 153 | }), 154 | }, 155 | }, 156 | SecuritySettings: []*serial.TypedMessage{tm}, 157 | SecurityType: tm.Type, 158 | } 159 | } 160 | return &streamsetting 161 | } 162 | 163 | func GetDomainsocketStreamConfig(filepath string) *internet.StreamConfig { 164 | var streamsetting internet.StreamConfig 165 | streamsetting = internet.StreamConfig{ 166 | ProtocolName: "domainsocket", 167 | TransportSettings: []*internet.TransportConfig{ 168 | &internet.TransportConfig{ 169 | ProtocolName: "domainsocket", 170 | Settings: serial.ToTypedMessage(&domainsocket.Config{ 171 | Path: filepath, 172 | }), 173 | }, 174 | }, 175 | } 176 | return &streamsetting 177 | } 178 | 179 | // different type inbounds 180 | func (h *HandlerServiceClient) AddVmessInbound(port uint16, address string, streamsetting *internet.StreamConfig) error { 181 | var addinboundrequest command.AddInboundRequest 182 | addinboundrequest = command.AddInboundRequest{ 183 | Inbound: &core.InboundHandlerConfig{ 184 | Tag: h.InboundTag, 185 | ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ 186 | PortRange: net.SinglePortRange(net.Port(port)), 187 | Listen: net.NewIPOrDomain(net.ParseAddress(address)), 188 | StreamSettings: streamsetting, 189 | }), 190 | ProxySettings: serial.ToTypedMessage(&inbound.Config{ 191 | User: []*protocol.User{ 192 | { 193 | Level: 0, 194 | Rate: 0, 195 | Email: "githubphone@xxx.com", 196 | Account: serial.ToTypedMessage(&vmess.Account{ 197 | Id: protocol.NewID(uuid.New()).String(), 198 | AlterId: 2, 199 | }), 200 | }, 201 | }, 202 | }), 203 | }, 204 | } 205 | return h.AddInbound(&addinboundrequest) 206 | } 207 | 208 | func (h *HandlerServiceClient) AddVmessOutbound(tag string, port uint16, address string, streamsetting *internet.StreamConfig, user *protocol.User) error { 209 | var addoutboundrequest command.AddOutboundRequest 210 | addoutboundrequest = command.AddOutboundRequest{ 211 | Outbound: &core.OutboundHandlerConfig{ 212 | Tag: tag, 213 | SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ 214 | StreamSettings: streamsetting, 215 | }), 216 | ProxySettings: serial.ToTypedMessage(&outbound.Config{ 217 | Receiver: []*protocol.ServerEndpoint{ 218 | { 219 | Address: net.NewIPOrDomain(net.ParseAddress(address)), 220 | Port: uint32(port), 221 | User: []*protocol.User{ 222 | user, 223 | }, 224 | }, 225 | }, 226 | }), 227 | }, 228 | } 229 | return h.AddOutbound(&addoutboundrequest) 230 | } 231 | 232 | func (h *HandlerServiceClient) AddSSOutbound(user model.UserModel, dist *model.DisNodeInfo) error { 233 | var addoutboundrequest command.AddOutboundRequest 234 | addoutboundrequest = command.AddOutboundRequest{ 235 | Outbound: &core.OutboundHandlerConfig{ 236 | Tag: dist.Server_raw + fmt.Sprintf("%d", user.UserID), 237 | ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{ 238 | Server: []*protocol.ServerEndpoint{ 239 | { 240 | Address: net.NewIPOrDomain(net.ParseAddress(dist.Server["server_address"].(string))), 241 | Port: uint32(dist.Port), 242 | User: []*protocol.User{ 243 | h.ConverSSUser(user), 244 | }, 245 | }, 246 | }, 247 | }), 248 | }, 249 | } 250 | return h.AddOutbound(&addoutboundrequest) 251 | } 252 | func (h *HandlerServiceClient) AddMTInbound(port uint16, address string, streamsetting *internet.StreamConfig) error { 253 | var addinboundrequest command.AddInboundRequest 254 | addinboundrequest = command.AddInboundRequest{ 255 | Inbound: &core.InboundHandlerConfig{ 256 | Tag: "tg-in", 257 | ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ 258 | PortRange: net.SinglePortRange(net.Port(port)), 259 | Listen: net.NewIPOrDomain(net.ParseAddress(address)), 260 | }), 261 | ProxySettings: serial.ToTypedMessage(&mtproto.ServerConfig{ 262 | User: []*protocol.User{ 263 | { 264 | Level: 0, 265 | Email: "githubphone@xxx.com", 266 | Rate: 0, 267 | Account: serial.ToTypedMessage(&mtproto.Account{ 268 | Secret: utility.MD5(utility.GetRandomString(16)), 269 | }), 270 | }, 271 | }, 272 | }), 273 | }, 274 | } 275 | return h.AddInbound(&addinboundrequest) 276 | } 277 | func (h *HandlerServiceClient) AddSSInbound(user model.UserModel, address string, streamsetting *internet.StreamConfig) error { 278 | var addinboundrequest command.AddInboundRequest 279 | addinboundrequest = command.AddInboundRequest{ 280 | Inbound: &core.InboundHandlerConfig{ 281 | Tag: user.PrefixedId, 282 | ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ 283 | PortRange: net.SinglePortRange(net.Port(user.Port)), 284 | Listen: net.NewIPOrDomain(net.ParseAddress(address)), 285 | StreamSettings: streamsetting, 286 | }), 287 | ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{ 288 | User: h.ConverSSUser(user), 289 | Network: []net.Network{net.Network_TCP, net.Network_UDP}, 290 | }), 291 | }, 292 | } 293 | return h.AddInbound(&addinboundrequest) 294 | } 295 | 296 | func (h *HandlerServiceClient) AddDokodemoInbound(port uint16, address string, streamsetting *internet.StreamConfig) error { 297 | var addinboundrequest command.AddInboundRequest 298 | addinboundrequest = command.AddInboundRequest{ 299 | Inbound: &core.InboundHandlerConfig{ 300 | Tag: h.InboundTag, 301 | ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ 302 | PortRange: net.SinglePortRange(net.Port(port)), 303 | StreamSettings: streamsetting, 304 | }), 305 | ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ 306 | Address: net.NewIPOrDomain(net.ParseAddress("v1.mux.cool")), 307 | Networks: []net.Network{net.Network_TCP, net.Network_UDP}, 308 | FollowRedirect: false, 309 | User: []*protocol.User{ 310 | { 311 | Level: 0, 312 | Rate: 0, 313 | Email: "githubphone@xxx.com", 314 | Account: serial.ToTypedMessage(&dokodemo.Account{ 315 | Mu_Host: "githubphone", 316 | }), 317 | }, 318 | }, 319 | }), 320 | }, 321 | } 322 | return h.AddInbound(&addinboundrequest) 323 | } 324 | func (h *HandlerServiceClient) AddFreedomOutbound(tag string, streamsetting *internet.StreamConfig) error { 325 | var addoutboundrequest command.AddOutboundRequest 326 | addoutboundrequest = command.AddOutboundRequest{ 327 | Outbound: &core.OutboundHandlerConfig{ 328 | Tag: tag, 329 | SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ 330 | StreamSettings: streamsetting, 331 | }), 332 | ProxySettings: serial.ToTypedMessage(&freedom.Config{}), 333 | }, 334 | } 335 | return h.AddOutbound(&addoutboundrequest) 336 | } 337 | 338 | func (h *HandlerServiceClient) AddInbound(req *command.AddInboundRequest) error { 339 | _, err := h.HandlerServiceClient.AddInbound(context.Background(), req) 340 | return err 341 | } 342 | func (h *HandlerServiceClient) AddOutbound(req *command.AddOutboundRequest) error { 343 | _, err := h.HandlerServiceClient.AddOutbound(context.Background(), req) 344 | return err 345 | } 346 | func (h *HandlerServiceClient) RemoveInbound(tag string) error { 347 | req := command.RemoveInboundRequest{ 348 | Tag: tag, 349 | } 350 | _, err := h.HandlerServiceClient.RemoveInbound(context.Background(), &req) 351 | return err 352 | } 353 | func (h *HandlerServiceClient) RemoveOutbound(tag string) error { 354 | req := command.RemoveOutboundRequest{ 355 | Tag: tag, 356 | } 357 | _, err := h.HandlerServiceClient.RemoveOutbound(context.Background(), &req) 358 | return err 359 | } 360 | 361 | func (h *HandlerServiceClient) ConvertVmessUser(userModel model.UserModel) *protocol.User { 362 | return &protocol.User{ 363 | Level: 0, 364 | Email: userModel.Email, 365 | Rate: userModel.Rate, 366 | Account: serial.ToTypedMessage(&vmess.Account{ 367 | Id: userModel.Uuid, 368 | AlterId: userModel.AlterId, 369 | SecuritySettings: &protocol.SecurityConfig{ 370 | Type: protocol.SecurityType(protocol.SecurityType_value[strings.ToUpper("AUTO")]), 371 | }, 372 | }), 373 | } 374 | } 375 | func (h *HandlerServiceClient) ConverDokodemoUser(userModel model.UserModel) *protocol.User { 376 | return &protocol.User{ 377 | Level: 0, 378 | Email: userModel.Email, 379 | Rate: userModel.Rate, 380 | Account: serial.ToTypedMessage(&dokodemo.Account{ 381 | Mu_Host: userModel.Muhost, 382 | }), 383 | } 384 | } 385 | func (h *HandlerServiceClient) ConverSSUser(userModel model.UserModel) *protocol.User { 386 | return &protocol.User{ 387 | Level: 0, 388 | Email: userModel.Email, 389 | Rate: userModel.Rate, 390 | Account: serial.ToTypedMessage(&shadowsocks.Account{ 391 | Password: userModel.Passwd, 392 | CipherType: CipherTypeMap[strings.ToLower(userModel.Method)], 393 | Ota: shadowsocks.Account_Auto, 394 | }), 395 | } 396 | } 397 | 398 | func (h *HandlerServiceClient) ConverMTUser(userModel model.UserModel) *protocol.User { 399 | return &protocol.User{ 400 | Level: 0, 401 | Email: userModel.Email, 402 | Rate: userModel.Rate, 403 | Account: serial.ToTypedMessage(&mtproto.Account{ 404 | Secret: utility.MD5(userModel.Uuid), 405 | }), 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /client/statsServiceClient.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "google.golang.org/grpc" 7 | "google.golang.org/grpc/status" 8 | "strings" 9 | statsservice "v2ray.com/core/app/stats/command" 10 | ) 11 | 12 | type StatsServiceClient struct { 13 | statsservice.StatsServiceClient 14 | } 15 | 16 | func NewStatsServiceClient(client *grpc.ClientConn) *StatsServiceClient { 17 | return &StatsServiceClient{ 18 | StatsServiceClient: statsservice.NewStatsServiceClient(client), 19 | } 20 | } 21 | 22 | // traffic 23 | func (s *StatsServiceClient) GetUserUplink(email string) (uint64, error) { 24 | return s.GetUserTraffic(fmt.Sprintf("user>>>%s>>>traffic>>>uplink", email), true) 25 | } 26 | 27 | func (s *StatsServiceClient) GetUserDownlink(email string) (uint64, error) { 28 | return s.GetUserTraffic(fmt.Sprintf("user>>>%s>>>traffic>>>downlink", email), true) 29 | } 30 | 31 | func (s *StatsServiceClient) GetUserTraffic(name string, reset bool) (uint64, error) { 32 | req := &statsservice.GetStatsRequest{ 33 | Name: name, 34 | Reset_: reset, 35 | } 36 | 37 | res, err := s.GetStats(context.Background(), req) 38 | if err != nil { 39 | if status, ok := status.FromError(err); ok && strings.HasSuffix(status.Message(), fmt.Sprintf("%s not found.", name)) { 40 | return 0, nil 41 | } 42 | 43 | return 0, err 44 | } 45 | 46 | return uint64(res.Stat.Value), nil 47 | } 48 | 49 | // ips 50 | 51 | func (s *StatsServiceClient) GetUserIPs(email string) ([]string, error) { 52 | name := fmt.Sprintf("user>>>%s>>>traffic>>>ips", email) 53 | req := &statsservice.GetStatsRequest{ 54 | Name: name, 55 | Reset_: true, 56 | } 57 | 58 | res, err := s.GetStats(context.Background(), req) 59 | 60 | if err != nil { 61 | if status, ok := status.FromError(err); ok && strings.HasSuffix(status.Message(), fmt.Sprintf("%s not found.", name)) { 62 | return []string{}, nil 63 | } 64 | return []string{}, err 65 | } 66 | ips := strings.Split(res.Stat.Name, ";") 67 | if len(ips) > 1 { 68 | ips = ips[1:] 69 | } else { 70 | ips = []string{} 71 | } 72 | return ips, nil 73 | } 74 | -------------------------------------------------------------------------------- /client/userRuleServerClient.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "google.golang.org/grpc" 6 | ruleservice "v2ray.com/core/app/router/command" 7 | ) 8 | 9 | type RuleServerClient struct { 10 | ruleservice.RuleServerClient 11 | } 12 | 13 | func NewRuleServerClient(client *grpc.ClientConn) *RuleServerClient { 14 | return &RuleServerClient{ 15 | RuleServerClient: ruleservice.NewRuleServerClient(client), 16 | } 17 | } 18 | 19 | func (s *RuleServerClient) AddUserRelyRule(targettag string, emails []string) error { 20 | _, err := s.AddUserRule(context.Background(), &ruleservice.AddUserRuleRequest{ 21 | TargetTag: targettag, 22 | Email: emails, 23 | }) 24 | return err 25 | } 26 | 27 | func (s *RuleServerClient) RemveUserRelayRule(email []string) error { 28 | _, err := s.RemoveUserRule(context.Background(), &ruleservice.RemoveUserRequest{ 29 | Email: email, 30 | }) 31 | return err 32 | } 33 | 34 | func (s *RuleServerClient) AddUserAttrMachter(targettag string, code string) error { 35 | _, err := s.AddAttrMachter(context.Background(), &ruleservice.AddAttrMachterRequest{ 36 | TargetTag: targettag, 37 | Code: code, 38 | }) 39 | return err 40 | } 41 | 42 | func (s *RuleServerClient) RemveUserAttrMachter(targettag string) error { 43 | _, err := s.RemoveAttrMachter(context.Background(), &ruleservice.RemoveAttrMachterRequest{ 44 | TargetTag: targettag, 45 | }) 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /config/cipher.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "encoding/hex" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | var ivspec = []byte("0000000000000000") 13 | 14 | const Key = "iKwb6Kt5pnqcVZcd" 15 | 16 | func PKCS5Padding(ciphertext []byte, blockSize int) []byte { 17 | padding := blockSize - len(ciphertext)%blockSize 18 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 19 | return append(ciphertext, padtext...) 20 | } 21 | 22 | func PKCS5Trimming(encrypt []byte) []byte { 23 | padding := encrypt[len(encrypt)-1] 24 | return encrypt[:len(encrypt)-int(padding)] 25 | } 26 | func AESEncodeStr(src, key string) string { 27 | block, err := aes.NewCipher([]byte(key)) 28 | if err != nil { 29 | fmt.Println("key error1", err) 30 | } 31 | if src == "" { 32 | fmt.Println("plain content empty") 33 | } 34 | ecb := cipher.NewCBCEncrypter(block, ivspec) 35 | content := []byte(src) 36 | content = PKCS5Padding(content, block.BlockSize()) 37 | crypted := make([]byte, len(content)) 38 | ecb.CryptBlocks(crypted, content) 39 | return hex.EncodeToString(crypted) 40 | } 41 | 42 | func AESDecodeStr(crypt, key string) string { 43 | crypted, err := hex.DecodeString(strings.ToLower(crypt)) 44 | if err != nil || len(crypted) == 0 { 45 | fmt.Println("plain content empty") 46 | } 47 | block, err := aes.NewCipher([]byte(key)) 48 | if err != nil { 49 | fmt.Println("key error1", err) 50 | } 51 | ecb := cipher.NewCBCDecrypter(block, ivspec) 52 | decrypted := make([]byte, len(crypted)) 53 | ecb.CryptBlocks(decrypted, crypted) 54 | 55 | return string(PKCS5Trimming(decrypted)) 56 | } 57 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "github.com/go-sql-driver/mysql" 8 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/utility" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "time" 13 | "v2ray.com/core/common/errors" 14 | "v2ray.com/core/common/platform" 15 | "v2ray.com/core/infra/conf" 16 | ) 17 | 18 | //var ( 19 | // CommandLine = flag.NewFlagSet(os.Args[0]+"-sspanel_v3_mod_Uim_plugin", flag.ContinueOnError) 20 | // 21 | // ConfigFile = CommandLine.String("config", "", "Config file for V2Ray.") 22 | // _ = CommandLine.Bool("version", false, "Show current version of V2Ray.") 23 | // Test = CommandLine.Bool("test", false, "Test config file only, without launching V2Ray server.") 24 | // _ = CommandLine.String("format", "json", "Format of input file.") 25 | // _ = CommandLine.Bool("plugin", false, "True to load plugins.") 26 | //) 27 | 28 | var ( 29 | CommandLine = flag.NewFlagSet(os.Args[0]+"-sspanel_v3_mod_Uim_plugin", flag.ContinueOnError) 30 | 31 | ConfigFile = CommandLine.String("config", "", "Config file for V2Ray.") 32 | _ = CommandLine.Bool("version", false, "Show current version of V2Ray.") 33 | Test = CommandLine.Bool("test", false, "Test config file only, without launching V2Ray server.") 34 | _ = CommandLine.String("format", "json", "Format of input file.") 35 | _ = CommandLine.Bool("plugin", false, "True to load plugins.") 36 | ) 37 | 38 | type MySQLConfig struct { 39 | Host string `json:"host"` 40 | Port int `json:"port"` 41 | User string `json:"user"` 42 | Password string `json:"password"` 43 | DBName string `json:"dbname"` 44 | } 45 | 46 | func (c *MySQLConfig) FormatDSN() (string, error) { 47 | loc, err := time.LoadLocation("Asia/Shanghai") 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | cc := &mysql.Config{ 53 | Collation: "utf8mb4_unicode_ci", 54 | User: c.User, 55 | Passwd: c.Password, 56 | Loc: loc, 57 | DBName: c.DBName, 58 | Net: "tcp", 59 | Addr: fmt.Sprintf("%s:%d", c.Host, c.Port), 60 | AllowNativePasswords: true, 61 | ParseTime: true, 62 | } 63 | 64 | return cc.FormatDSN(), nil 65 | } 66 | 67 | type Config struct { 68 | NodeID uint `json:"nodeId"` 69 | CheckRate int `json:"checkRate"` 70 | PanelUrl string `json:"panelUrl"` 71 | PanelKey string `json:"panelKey"` 72 | SpeedTestCheckRate int `json:"speedTestCheckrate"` 73 | DownWithPanel int `json:"downWithPanel"` 74 | MySQL *MySQLConfig `json:"mysql"` 75 | Paneltype int `json:"paneltype"` 76 | Usemysql int `json:"usemysql"` 77 | MU_REGEX string `json:"mu_regex"` 78 | MU_SUFFIX string `json:"mu_suffix"` 79 | GoPanelKey string `json:"go_panel_key"` 80 | GoPanelHost string `json:"go_panel_host"` 81 | V2rayConfig *conf.Config 82 | } 83 | 84 | func GetConfig() (*Config, error) { 85 | type config struct { 86 | *conf.Config 87 | SSPanel *Config `json:"sspanel"` 88 | } 89 | 90 | configFile := GetConfigFilePath() 91 | // Open our jsonFile 92 | jsonFile, err := os.Open(configFile) 93 | // if we os.Open returns an error then handle it 94 | if err != nil { 95 | return nil, errors.New("failed to open config: ", configFile).Base(err) 96 | } 97 | 98 | // defer the closing of our jsonFile so that we can parse it later on 99 | defer jsonFile.Close() 100 | 101 | byteValue, _ := ioutil.ReadAll(jsonFile) 102 | 103 | cfg := &config{} 104 | err = json.Unmarshal(byteValue, &cfg) 105 | if err != nil { 106 | return nil, err 107 | } 108 | if cfg.SSPanel != nil { 109 | cfg.SSPanel.V2rayConfig = cfg.Config 110 | if err = CheckCfg(cfg.SSPanel); err != nil { 111 | return nil, err 112 | } 113 | } 114 | return cfg.SSPanel, err 115 | } 116 | 117 | func CheckCfg(cfg *Config) error { 118 | 119 | if cfg.V2rayConfig.Api == nil { 120 | return errors.New("Api must be set") 121 | } 122 | 123 | apiTag := cfg.V2rayConfig.Api.Tag 124 | if len(apiTag) == 0 { 125 | return errors.New("Api tag can't be empty") 126 | } 127 | 128 | services := cfg.V2rayConfig.Api.Services 129 | if !utility.InStr("HandlerService", services) { 130 | return errors.New("Api service, HandlerService, must be enabled") 131 | } 132 | if !utility.InStr("StatsService", services) { 133 | return errors.New("Api service, StatsService, must be enabled") 134 | } 135 | 136 | if cfg.V2rayConfig.Stats == nil { 137 | return errors.New("Stats must be enabled") 138 | } 139 | 140 | if apiInbound := GetInboundConfigByTag(apiTag, cfg.V2rayConfig.InboundConfigs); apiInbound == nil { 141 | return errors.New(fmt.Sprintf("Miss an inbound tagged %s", apiTag)) 142 | } else if apiInbound.Protocol != "dokodemo-door" { 143 | return errors.New(fmt.Sprintf("The protocol of inbound tagged %s must be \"dokodemo-door\"", apiTag)) 144 | } else { 145 | if apiInbound.ListenOn == nil || apiInbound.PortRange == nil { 146 | return errors.New(fmt.Sprintf("Fields, \"listen\" and \"port\", of inbound tagged %s must be set", apiTag)) 147 | } 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func GetInboundConfigByTag(apiTag string, inbounds []conf.InboundDetourConfig) *conf.InboundDetourConfig { 154 | for _, inbound := range inbounds { 155 | if inbound.Tag == apiTag { 156 | return &inbound 157 | } 158 | } 159 | return nil 160 | } 161 | 162 | func GetConfigFilePath() string { 163 | if len(*ConfigFile) > 0 { 164 | return *ConfigFile 165 | } 166 | 167 | if workingDir, err := os.Getwd(); err == nil { 168 | configFile := filepath.Join(workingDir, "config.json") 169 | if fileExists(configFile) { 170 | return configFile 171 | } 172 | } 173 | 174 | if configFile := platform.GetConfigurationPath(); fileExists(configFile) { 175 | return configFile 176 | } 177 | 178 | return "" 179 | } 180 | 181 | func fileExists(file string) bool { 182 | info, err := os.Stat(file) 183 | return err == nil && !info.IsDir() 184 | } 185 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "github.com/imroc/req" 8 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/model" 9 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/speedtest" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | func Min(x, y int64) int64 { 16 | if x < y { 17 | return x 18 | } 19 | return y 20 | } 21 | 22 | type NodeinfoResponse struct { 23 | Ret uint `json:"ret"` 24 | Data *model.NodeInfo `json:"data"` 25 | } 26 | type PostResponse struct { 27 | Ret uint `json:"ret"` 28 | Data string `json:"data"` 29 | } 30 | type UsersResponse struct { 31 | Ret uint `json:"ret"` 32 | Data []model.UserModel `json:"data"` 33 | } 34 | type AllUsers struct { 35 | Ret uint 36 | Data map[string]model.UserModel 37 | } 38 | 39 | type DisNodenfoResponse struct { 40 | Ret uint `json:"ret"` 41 | Data []*model.DisNodeInfo `json:"data"` 42 | } 43 | type AuthResponse struct { 44 | Ret uint `json:"ret"` 45 | Token string `json:"token"` 46 | } 47 | 48 | var id2string = map[uint]string{ 49 | 0: "server_address", 50 | 1: "port", 51 | 2: "alterid", 52 | 3: "protocol", 53 | 4: "protocol_param", 54 | 5: "path", 55 | 6: "host", 56 | 7: "inside_port", 57 | 8: "server", 58 | } 59 | var maps = map[string]interface{}{ 60 | "server_address": "", 61 | "port": "", 62 | "alterid": "2", 63 | "protocol": "tcp", 64 | "protocol_param": "", 65 | "path": "", 66 | "host": "", 67 | "inside_port": "", 68 | "server": "", 69 | } 70 | 71 | func getMD5(data string) string { 72 | md5Ctx := md5.New() 73 | md5Ctx.Write([]byte(data)) 74 | cipherStr := md5Ctx.Sum(nil) 75 | current_md5 := hex.EncodeToString(cipherStr) 76 | return current_md5 77 | } 78 | func get_mu_host(id uint, md5 string, MU_REGEX string, MU_SUFFIX string) string { 79 | regex_text := MU_REGEX 80 | regex_text = strings.Replace(regex_text, "%id", fmt.Sprintf("%d", id), -1) 81 | regex_text = strings.Replace(regex_text, "%suffix", MU_SUFFIX, -1) 82 | regex := regexp.MustCompile(`%-?[1-9]\d*m`) 83 | for _, item := range regex.FindAllString(regex_text, -1) { 84 | regex_num := strings.Replace(item, "%", "", -1) 85 | regex_num = strings.Replace(regex_num, "m", "", -1) 86 | md5_length, _ := strconv.ParseInt(regex_num, 10, 0) 87 | if md5_length < 0 { 88 | regex_text = strings.Replace(regex_text, item, md5[32+md5_length:], -1) 89 | } else { 90 | regex_text = strings.Replace(regex_text, item, md5[:md5_length], -1) 91 | } 92 | } 93 | return regex_text 94 | } 95 | 96 | type Db interface { 97 | GetApi(url string, params map[string]interface{}) (*req.Resp, error) 98 | 99 | GetNodeInfo(nodeid uint) (*NodeinfoResponse, error) 100 | 101 | GetDisNodeInfo(nodeid uint) (*DisNodenfoResponse, error) 102 | 103 | GetALLUsers(info *model.NodeInfo) (*AllUsers, error) 104 | 105 | Post(url string, params map[string]interface{}, data map[string]interface{}) (*req.Resp, error) 106 | 107 | UploadSystemLoad(nodeid uint) bool 108 | 109 | UpLoadUserTraffics(nodeid uint, trafficLog []model.UserTrafficLog) bool 110 | UploadSpeedTest(nodeid uint, speedresult []speedtest.Speedresult) bool 111 | UpLoadOnlineIps(nodeid uint, onlineIPS []model.UserOnLineIP) bool 112 | } 113 | -------------------------------------------------------------------------------- /db/mysql.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jinzhu/gorm" 6 | _ "github.com/jinzhu/gorm/dialects/mysql" 7 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/config" 8 | "os" 9 | "time" 10 | "v2ray.com/core/common/errors" 11 | ) 12 | 13 | func NewMySQLConn(config *config.MySQLConfig) (*gorm.DB, error) { 14 | newError("Connecting database...").AtInfo().WriteToLog() 15 | defer newError("Connected").AtInfo().WriteToLog() 16 | 17 | dsn, err := config.FormatDSN() 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | db, err := gorm.Open("mysql", dsn) 23 | if err != nil { 24 | return nil, err 25 | } 26 | db.SingularTable(true) 27 | 28 | return db, nil 29 | } 30 | 31 | func newErrorf(format string, a ...interface{}) *errors.Error { 32 | return newError(fmt.Sprintf(format, a...)) 33 | } 34 | 35 | func newError(values ...interface{}) *errors.Error { 36 | values = append([]interface{}{"SSPanelPlugin: "}, values...) 37 | return errors.New(values...) 38 | } 39 | 40 | func fatal(values ...interface{}) { 41 | newError(values...).AtError().WriteToLog() 42 | // Wait log 43 | time.Sleep(1 * time.Second) 44 | os.Exit(-2) 45 | } 46 | -------------------------------------------------------------------------------- /db/sspanel.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gofrs/uuid" 6 | "github.com/imroc/req" 7 | "github.com/jinzhu/gorm" 8 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/model" 9 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/speedtest" 10 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/utility" 11 | "math" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | type SSpanel struct { 18 | Db *gorm.DB 19 | MU_REGEX string 20 | MU_SUFFIX string 21 | } 22 | 23 | func (api *SSpanel) GetApi(url string, params map[string]interface{}) (*req.Resp, error) { 24 | return nil, nil 25 | } 26 | 27 | func (api *SSpanel) GetNodeInfo(nodeid uint) (*NodeinfoResponse, error) { 28 | var response = NodeinfoResponse{Data: &model.NodeInfo{}} 29 | var nodeinfotable SSNode 30 | err := api.Db.First(&nodeinfotable, "id = ?", nodeid).Error 31 | if err != nil { 32 | return nil, newError("nodeinfo not find,please check your nodeid or mysql setting").Base(err) 33 | } 34 | response.Ret = 1 35 | response.Data.Server_raw = nodeinfotable.Server 36 | response.Data.Sort = nodeinfotable.Sort 37 | response.Data.NodeSpeedlimit = uint(nodeinfotable.NodeSpeedlimit) 38 | response.Data.TrafficRate = nodeinfotable.TrafficRate 39 | response.Data.NodeID = nodeinfotable.Id 40 | if response.Data.Server_raw != "" { 41 | response.Data.Server_raw = strings.ToLower(response.Data.Server_raw) 42 | data := strings.Split(response.Data.Server_raw, ";") 43 | var count uint 44 | count = 0 45 | for v := range data { 46 | if len(data[v]) > 0 { 47 | maps[id2string[count]] = data[v] 48 | } 49 | count += 1 50 | } 51 | var extraArgues []string 52 | if len(data) == 6 { 53 | extraArgues = append(extraArgues, strings.Split(data[5], "|")...) 54 | for item := range extraArgues { 55 | data = strings.Split(extraArgues[item], "=") 56 | if len(data) > 0 { 57 | if len(data[1]) > 0 { 58 | maps[data[0]] = data[1] 59 | } 60 | 61 | } 62 | } 63 | } 64 | 65 | if maps["protocol"] == "tls" { 66 | temp := maps["protocol_param"] 67 | maps["protocol"] = temp 68 | maps["protocol_param"] = "tls" 69 | } 70 | response.Data.Server = maps 71 | } 72 | response.Data.NodeID = nodeid 73 | return &response, nil 74 | } 75 | 76 | func (api *SSpanel) GetDisNodeInfo(nodeid uint) (*DisNodenfoResponse, error) { 77 | var response = DisNodenfoResponse{} 78 | var rules []Relay 79 | 80 | err := api.Db.Find(&rules, "source_node_id =?", nodeid).Error 81 | if err != nil { 82 | return nil, newError("no relay rule ,please check your nodeid or mysql setting").Base(err) 83 | } 84 | response.Ret = 1 85 | for index := range rules { 86 | rule := rules[index] 87 | if rule.DistNodeId == -1 { 88 | continue 89 | } 90 | var dis SSNode 91 | err = api.Db.First(&dis, "id = ?", rule.DistNodeId).Error 92 | if err != nil { 93 | continue 94 | } else { 95 | disnode := model.DisNodeInfo{ 96 | Server_raw: dis.Server, 97 | Sort: dis.Sort, 98 | Port: uint16(rule.Port), 99 | UserId: rule.Userid} 100 | response.Data = append(response.Data, &disnode) 101 | } 102 | } 103 | if len(response.Data) > 0 { 104 | for _, relayrule := range response.Data { 105 | relayrule.Server_raw = strings.ToLower(relayrule.Server_raw) 106 | data := strings.Split(relayrule.Server_raw, ";") 107 | var count uint 108 | count = 0 109 | for v := range data { 110 | if len(data[v]) > 0 { 111 | maps[id2string[count]] = data[v] 112 | } 113 | count += 1 114 | } 115 | var extraArgues []string 116 | if len(data) == 6 { 117 | extraArgues = append(extraArgues, strings.Split(data[5], "|")...) 118 | for item := range extraArgues { 119 | data = strings.Split(extraArgues[item], "=") 120 | if len(data) > 0 { 121 | if len(data[1]) > 0 { 122 | maps[data[0]] = data[1] 123 | } 124 | 125 | } 126 | } 127 | } 128 | 129 | if maps["protocol"] == "tls" { 130 | temp := maps["protocol_param"] 131 | maps["protocol"] = temp 132 | maps["protocol_param"] = "tls" 133 | } 134 | relayrule.Server = maps 135 | } 136 | } 137 | return &response, nil 138 | } 139 | 140 | func (api *SSpanel) GetALLUsers(info *model.NodeInfo) (*AllUsers, error) { 141 | sort := info.Sort 142 | var prifix string 143 | var allusers = AllUsers{ 144 | Data: map[string]model.UserModel{}, 145 | } 146 | if sort == 0 { 147 | prifix = "SS_" 148 | } else { 149 | prifix = "Vmess_" 150 | if info.Server["protocol"] == "tcp" { 151 | prifix += "tcp_" 152 | } else if info.Server["protocol"] == "ws" { 153 | if info.Server["protocol_param"] != "" { 154 | prifix += "ws_" + info.Server["protocol_param"].(string) + "_" 155 | } else { 156 | prifix += "ws_" + "none" + "_" 157 | } 158 | } else if info.Server["protocol"] == "kcp" { 159 | if info.Server["protocol_param"] != "" { 160 | prifix += "kcp_" + info.Server["protocol_param"].(string) + "_" 161 | } else { 162 | prifix += "kcp_" + "none" + "_" 163 | } 164 | } 165 | } 166 | var response = UsersResponse{} 167 | var node SSNode 168 | err := api.Db.First(&node, "id = ?", info.NodeID).Error 169 | if err != nil { 170 | return nil, err 171 | } 172 | node.NodeHeartbeat = time.Now().Unix() 173 | api.Db.Save(&node) 174 | 175 | var users []User 176 | time1 := time.Now() 177 | location, _ := time.LoadLocation("Asia/Shanghai") 178 | t_in := time1.In(location).Format("2006-01-02 15:04:05") 179 | if node.NodeGroup != 0 { 180 | err := api.Db.Where("class >= ? AND node_group = ?", node.NodeClass, node.NodeGroup).Or("is_admin = 1").Where("enable =1 AND expire_in > ?", t_in).Find(&users).Error 181 | if err != nil { 182 | return nil, err 183 | } 184 | } else { 185 | err := api.Db.Where("class >= ?", node.NodeClass).Or("is_admin = 1").Where("enable =1 AND expire_in > ?", t_in).Find(&users).Error 186 | if err != nil { 187 | return nil, err 188 | } 189 | } 190 | response.Ret = 1 191 | 192 | var filterd_user []model.UserModel 193 | for index := range users { 194 | user := users[index] 195 | if user.TransferEnable > user.Download+user.Upload { 196 | filterd_user = append(filterd_user, model.UserModel{ 197 | UserID: user.ID, 198 | Uuid: uuid.NewV3(uuid.NamespaceDNS, fmt.Sprintf("%d|%s", user.ID, user.Passwd)).String(), 199 | Email: user.Email, 200 | Passwd: user.Passwd, 201 | Method: user.Method, 202 | Port: user.Port, 203 | NodeSpeedlimit: uint(user.NodeSpeedlimit), 204 | Obfs: user.Obfs, 205 | Protocol: user.Protocol, 206 | }) 207 | } 208 | } 209 | if node.NodeBrandwithLimit != 0 { 210 | if node.NodeBrandwithLimit < node.NodeBrandwith { 211 | response.Data = []model.UserModel{} 212 | } else { 213 | response.Data = filterd_user 214 | } 215 | } else { 216 | response.Data = filterd_user 217 | } 218 | for index := range response.Data { 219 | // 按照node 限速来调整用户限速 220 | if info.NodeSpeedlimit != 0 { 221 | if info.NodeSpeedlimit > response.Data[index].NodeSpeedlimit && response.Data[index].NodeSpeedlimit != 0 { 222 | } else if response.Data[index].NodeSpeedlimit == 0 || response.Data[index].NodeSpeedlimit > info.NodeSpeedlimit { 223 | response.Data[index].NodeSpeedlimit = info.NodeSpeedlimit 224 | } 225 | } 226 | // 接受到的是 Mbps, 然后我们的一个buffer 是2048byte, 差不多61个 227 | response.Data[index].Rate = uint32(response.Data[index].NodeSpeedlimit * 62) 228 | if info.Server["alterid"].(string) == "" { 229 | response.Data[index].AlterId = 2 230 | } else { 231 | alterid, err := strconv.ParseUint(info.Server["alterid"].(string), 10, 0) 232 | if err == nil { 233 | response.Data[index].AlterId = uint32(alterid) 234 | } 235 | } 236 | user := response.Data[index] 237 | response.Data[index].Muhost = get_mu_host(user.UserID, getMD5(fmt.Sprintf("%d%s%s%s%s", user.UserID, user.Passwd, user.Method, user.Obfs, user.Protocol)), api.MU_REGEX, api.MU_SUFFIX) 238 | key := prifix + response.Data[index].Email + fmt.Sprintf("Rate_%d_AlterID_%d_Method_%s_Passwd_%s_Port_%d_Obfs_%s_Protocol_%s", response.Data[index].Rate, 239 | response.Data[index].AlterId, response.Data[index].Method, response.Data[index].Passwd, response.Data[index].Port, response.Data[index].Obfs, response.Data[index].Protocol, 240 | ) 241 | response.Data[index].PrefixedId = key 242 | allusers.Data[key] = response.Data[index] 243 | } 244 | return &allusers, nil 245 | } 246 | 247 | func (api *SSpanel) Post(url string, params map[string]interface{}, data map[string]interface{}) (*req.Resp, error) { 248 | 249 | return nil, nil 250 | } 251 | 252 | func (api *SSpanel) UploadSystemLoad(nodeid uint) bool { 253 | uptime, _ := strconv.ParseFloat(utility.GetSystemUptime(), 64) 254 | err := api.Db.Create(&SSNodeInfo{ 255 | NodeId: nodeid, 256 | Load: utility.GetSystemLoad(), 257 | Uptime: uptime, 258 | Logtime: time.Now().Unix(), 259 | }).Error 260 | if err != nil { 261 | return false 262 | } 263 | return true 264 | } 265 | 266 | func (api *SSpanel) flowAutoShow(value float64) string { 267 | value_float := value 268 | kb := 1024.0 269 | mb := 1048576.0 270 | gb := 1073741824.0 271 | tb := gb * 1024.0 272 | pb := tb * 1024.0 273 | if math.Abs(value_float) > pb { 274 | return fmt.Sprintf("%.2fPB", math.Round(value_float/pb)) 275 | } else if math.Abs(value_float) > tb { 276 | return fmt.Sprintf("%.2fTB", math.Round(value_float/tb)) 277 | } else if math.Abs(value_float) > gb { 278 | return fmt.Sprintf("%.2fGB", math.Round(value_float/gb)) 279 | } else if math.Abs(value_float) > mb { 280 | return fmt.Sprintf("%.2fMB", math.Round(value_float/mb)) 281 | } else if math.Abs(value_float) > kb { 282 | return fmt.Sprintf("%.2fKB", math.Round(value_float/kb)) 283 | } else { 284 | return fmt.Sprintf("%.2fB", math.Round(value_float)) 285 | } 286 | } 287 | 288 | func (api *SSpanel) UpLoadUserTraffics(nodeid uint, trafficLog []model.UserTrafficLog) bool { 289 | var ssnode SSNode 290 | err := api.Db.First(&ssnode, "id = ?", nodeid).Error 291 | if err != nil { 292 | return false 293 | } 294 | var this_time_total_bandwidth uint64 = 0 295 | if len(trafficLog) > 0 { 296 | for index := range trafficLog { 297 | traffic := trafficLog[index] 298 | 299 | var user User 300 | err := api.Db.First(&user, "id = ?", traffic.UserID).Error 301 | 302 | if err != nil { 303 | continue 304 | } 305 | user.Time = time.Now().Unix() 306 | user.Upload += int64(float64(traffic.Uplink) * ssnode.TrafficRate) 307 | user.Download += int64(float64(traffic.Downlink) * ssnode.TrafficRate) 308 | this_time_total_bandwidth += traffic.Uplink + traffic.Downlink 309 | api.Db.Save(&user) 310 | 311 | api.Db.Save(&UserTrafficLog{ 312 | Userid: traffic.UserID, 313 | Upload: traffic.Uplink, 314 | Download: traffic.Downlink, 315 | Nodeid: nodeid, 316 | Rate: ssnode.TrafficRate, 317 | Logtime: time.Now().Unix(), 318 | Traffic: api.flowAutoShow(float64(traffic.Uplink+traffic.Downlink) * ssnode.TrafficRate), 319 | }) 320 | } 321 | } 322 | ssnode.NodeBrandwith += this_time_total_bandwidth 323 | api.Db.Save(&ssnode) 324 | api.Db.Create(&SsNodeOnlineLog{ 325 | NodeId: nodeid, 326 | OnlineUser: uint(len(trafficLog)), 327 | Logtime: time.Now().Unix(), 328 | }) 329 | return true 330 | } 331 | func (api *SSpanel) UploadSpeedTest(nodeid uint, speedresult []speedtest.Speedresult) bool { 332 | var ssnode SSNode 333 | err := api.Db.First(&ssnode, "id = ?", nodeid).Error 334 | if err != nil { 335 | return false 336 | } 337 | 338 | if len(speedresult) > 0 { 339 | for index := range speedresult { 340 | speed := speedresult[index] 341 | api.Db.Create(&Speedtest{ 342 | Nodeid: nodeid, 343 | Datetime: time.Now().Unix(), 344 | Telecomping: speed.CTPing, 345 | Telecomeupload: speed.CTUpSpeed, 346 | Telecomedownload: speed.CTDLSpeed, 347 | Unicomping: speed.CUPing, 348 | Unicomupload: speed.CUUpSpeed, 349 | Unicomdownload: speed.CUDLSpeed, 350 | Cmccping: speed.CMPing, 351 | Cmccupload: speed.CMUpSpeed, 352 | Cmccdownload: speed.CMDLSpeed, 353 | }) 354 | } 355 | } 356 | return true 357 | } 358 | func (api *SSpanel) UpLoadOnlineIps(nodeid uint, onlineIPS []model.UserOnLineIP) bool { 359 | var ssnode SSNode 360 | err := api.Db.First(&ssnode, "id = ?", nodeid).Error 361 | if err != nil { 362 | return false 363 | } 364 | if len(onlineIPS) > 0 { 365 | for index := range onlineIPS { 366 | ip := onlineIPS[index] 367 | api.Db.Create(&AliveIP{ 368 | Nodeid: nodeid, 369 | Userid: ip.UserId, 370 | Ip: ip.Ip, 371 | Datetime: time.Now().Unix(), 372 | }) 373 | } 374 | } 375 | return true 376 | } 377 | 378 | func (api *SSpanel) CheckAuth(url string, params map[string]interface{}) (*AuthResponse, error) { 379 | var response = AuthResponse{} 380 | parm := req.Param{} 381 | for k, v := range params { 382 | parm[k] = v 383 | } 384 | r, err := req.Get(url, parm) 385 | if err != nil { 386 | return nil, err 387 | } else { 388 | err = r.ToJSON(&response) 389 | if err != nil { 390 | return &response, err 391 | } else if response.Ret != 1 { 392 | return nil, err 393 | } 394 | } 395 | return &response, nil 396 | } 397 | -------------------------------------------------------------------------------- /db/sspaneltabel.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type AliveIP struct { 8 | Nodeid uint `gorm:"column:nodeid"` 9 | Userid uint `gorm:"column:userid"` 10 | Ip string `gorm:"column:ip"` 11 | Datetime int64 `gorm:"column:datetime"` 12 | } 13 | 14 | func (*AliveIP) TableName() string { 15 | return "alive_ip" 16 | } 17 | 18 | type Speedtest struct { 19 | Nodeid uint `gorm:"column:nodeid"` 20 | Datetime int64 `gorm:"column:datetime"` 21 | Telecomping string `gorm:"column:telecomping"` 22 | Telecomeupload string `gorm:"column:telecomeupload"` 23 | Telecomedownload string `gorm:"column:telecomedownload"` 24 | Unicomping string `gorm:"column:unicomping"` 25 | Unicomupload string `gorm:"column:unicomupload"` 26 | Unicomdownload string `gorm:"column:unicomdownload"` 27 | Cmccping string `gorm:"column:cmccping"` 28 | Cmccupload string `gorm:"column:cmccupload"` 29 | Cmccdownload string `gorm:"column:cmccdownload"` 30 | } 31 | 32 | func (*Speedtest) TableName() string { 33 | return "speedtest" 34 | } 35 | 36 | type SSNode struct { 37 | Id uint `gorm:"column:id"` 38 | Name string `gorm:"column:name"` 39 | Type uint `gorm:"column:type"` 40 | Server string `gorm:"column:server"` 41 | Method string `gorm:"column:method"` 42 | Info string `gorm:"column:info"` 43 | Status string `gorm:"column:status"` 44 | Sort uint `gorm:"column:sort"` 45 | CustomMethod uint `gorm:"column:custom_method"` 46 | TrafficRate float64 `gorm:"column:traffic_rate"` 47 | NodeClass uint `gorm:"column:node_class"` 48 | NodeSpeedlimit float64 `gorm:"column:node_speedlimit"` 49 | NodeConnector uint `gorm:"column:node_connector"` 50 | NodeBrandwith uint64 `gorm:"column:node_bandwidth"` 51 | NodeBrandwithLimit uint64 `gorm:"column:node_bandwidth_limit"` 52 | BrandwithlimitRestday uint `gorm:"column:bandwidthlimit_resetday"` 53 | NodeHeartbeat int64 `gorm:"column:node_heartbeat"` 54 | NodeIp string `gorm:"column:node_ip"` 55 | NodeGroup uint `gorm:"column:node_group"` 56 | } 57 | 58 | func (*SSNode) TableName() string { 59 | return "ss_node" 60 | } 61 | 62 | type SSNodeInfo struct { 63 | NodeId uint `gorm:"column:node_id"` 64 | Uptime float64 `gorm:"column:uptime"` 65 | Load string `gorm:"column:load"` 66 | Logtime int64 `gorm:"column:log_time"` 67 | } 68 | 69 | func (*SSNodeInfo) TableName() string { 70 | return "ss_node_info" 71 | } 72 | 73 | type SsNodeOnlineLog struct { 74 | NodeId uint `gorm:"column:node_id"` 75 | OnlineUser uint `gorm:"column:online_user"` 76 | Logtime int64 `gorm:"column:log_time"` 77 | } 78 | 79 | func (*SsNodeOnlineLog) TableName() string { 80 | return "ss_node_online_log" 81 | } 82 | 83 | type User struct { 84 | ID uint `gorm:"column:id"` 85 | Email string `gorm:"column:email"` 86 | Passwd string `gorm:"column:passwd"` 87 | Method string `gorm:"column:method"` 88 | Port uint16 `gorm:"column:port"` 89 | NodeSpeedlimit float64 `gorm:"column:node_speedlimit"` 90 | Upload int64 `gorm:"column:u"` 91 | Download int64 `gorm:"column:d"` 92 | Enable uint `gorm:"column:enable"` 93 | ExpireTime uint `gorm:"column:expire_time"` 94 | IsAdmin uint `gorm:"column:is_admin"` 95 | Class uint `gorm:"column:class"` 96 | ExpireIN time.Time `gorm:"colum:expire_in"` 97 | TransferEnable int64 `gorm:"column:transfer_enable"` 98 | Time int64 `gorm:"column:t"` 99 | Obfs string `gorm:"column:obfs"` 100 | Protocol string `gorm:"column:protocol"` 101 | } 102 | 103 | func (*User) TableName() string { 104 | return "user" 105 | } 106 | 107 | type UserTrafficLog struct { 108 | Userid uint `gorm:"column:user_id"` 109 | Upload uint64 `gorm:"column:u"` 110 | Download uint64 `gorm:"column:d"` 111 | Nodeid uint `gorm:"column:node_id"` 112 | Rate float64 `gorm:"column:rate"` 113 | Logtime int64 `gorm:"column:log_time"` 114 | Traffic string `gorm:"column:traffic"` 115 | } 116 | 117 | func (*UserTrafficLog) TableName() string { 118 | return "user_traffic_log" 119 | } 120 | 121 | type Relay struct { 122 | ID uint `gorm:"column:id"` 123 | Userid uint `gorm:"column:user_id"` 124 | SourceNodeId uint `gorm:"column:source_node_id"` 125 | DistNodeId int `gorm:"column:dist_node_id"` 126 | DistIp string `gorm:"column:dist_ip"` 127 | Port uint `gorm:"column:port"` 128 | Priority uint `gorm:"column:priority"` 129 | } 130 | 131 | func (*Relay) TableName() string { 132 | return "relay" 133 | } 134 | -------------------------------------------------------------------------------- /db/ssrpanel.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/imroc/req" 7 | "github.com/jinzhu/gorm" 8 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/model" 9 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/speedtest" 10 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/utility" 11 | "math" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | type SSRpanel struct { 17 | Db *gorm.DB 18 | MU_REGEX string 19 | MU_SUFFIX string 20 | } 21 | 22 | func (api *SSRpanel) GetApi(url string, params map[string]interface{}) (*req.Resp, error) { 23 | return nil, nil 24 | } 25 | 26 | func (api *SSRpanel) GetNodeInfo(nodeid uint) (*NodeinfoResponse, error) { 27 | var response = NodeinfoResponse{Data: &model.NodeInfo{}} 28 | 29 | node := SSRNode{} 30 | err := api.Db.First(&node, nodeid).Error 31 | if err != nil { 32 | return nil, newError("nodeinfo not find,please check your nodeid or mysql setting").Base(err) 33 | } 34 | 35 | response.Ret = 1 36 | sever_raw, _ := json.Marshal(node) 37 | response.Data.Server_raw = string(sever_raw) 38 | if node.Type == 1 { 39 | response.Data.Sort = 0 40 | } else if node.Type == 2 { 41 | response.Data.Sort = 11 42 | } else { 43 | return nil, newError("not implement type ") 44 | } 45 | response.Data.NodeSpeedlimit = node.Bandwidth 46 | response.Data.TrafficRate = node.TrafficRate 47 | response.Data.NodeID = node.ID 48 | response.Data.Server = map[string]interface{}{ 49 | "server_address": node.Server, 50 | "port": fmt.Sprintf("%d", node.V2Port), 51 | "alterid": fmt.Sprintf("%d", node.V2AlterId), 52 | "protocol": node.V2Type, 53 | "protocol_param": node.V2Net, 54 | "path": node.V2Path, 55 | "host": node.V2Host, 56 | "inside_port": node.V2rayInsiderPort, 57 | "server": node.Server, 58 | } 59 | if node.V2Tls == 1 { 60 | response.Data.Server["protocol_param"] = "tls" 61 | } 62 | return &response, nil 63 | } 64 | 65 | func (api *SSRpanel) GetDisNodeInfo(nodeid uint) (*DisNodenfoResponse, error) { 66 | var response = DisNodenfoResponse{} 67 | return &response, nil 68 | } 69 | 70 | func (api *SSRpanel) GetALLUsers(info *model.NodeInfo) (*AllUsers, error) { 71 | sort := info.Sort 72 | var prifix string 73 | var allusers = AllUsers{ 74 | Data: map[string]model.UserModel{}, 75 | } 76 | if sort == 0 { 77 | prifix = "SS_" 78 | } else { 79 | prifix = "Vmess_" 80 | if info.Server["protocol"] == "tcp" { 81 | prifix += "tcp_" 82 | } else if info.Server["protocol"] == "ws" { 83 | if info.Server["protocol_param"] != "" { 84 | prifix += "ws_" + info.Server["protocol_param"].(string) + "_" 85 | } else { 86 | prifix += "ws_" + "none" + "_" 87 | } 88 | } else if info.Server["protocol"] == "kcp" { 89 | if info.Server["protocol_param"] != "" { 90 | prifix += "kcp_" + info.Server["protocol_param"].(string) + "_" 91 | } else { 92 | prifix += "kcp_" + "none" + "_" 93 | } 94 | } 95 | } 96 | var response = UsersResponse{} 97 | users := make([]SSRUserModel, 0) 98 | err := api.Db.Select("id, vmess_id, username").Where("enable = 1 AND u + d < transfer_enable").Find(&users).Error 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | response.Ret = 1 104 | 105 | var filterd_user []model.UserModel 106 | for index := range users { 107 | user := users[index] 108 | filterd_user = append(filterd_user, model.UserModel{ 109 | UserID: user.ID, 110 | Uuid: user.VmessID, 111 | Email: user.Email, 112 | Passwd: user.Passwd, 113 | Method: user.Method, 114 | Port: uint16(user.Port), 115 | NodeSpeedlimit: uint(user.SpeedLimitPerUser / 125000), 116 | Obfs: user.Obfs, 117 | Protocol: user.Protocol, 118 | }) 119 | 120 | } 121 | 122 | response.Data = filterd_user 123 | 124 | for index := range response.Data { 125 | // 按照node 限速来调整用户限速 126 | if info.NodeSpeedlimit != 0 { 127 | if info.NodeSpeedlimit > response.Data[index].NodeSpeedlimit && response.Data[index].NodeSpeedlimit != 0 { 128 | } else if response.Data[index].NodeSpeedlimit == 0 || response.Data[index].NodeSpeedlimit > info.NodeSpeedlimit { 129 | response.Data[index].NodeSpeedlimit = info.NodeSpeedlimit 130 | } 131 | } 132 | // 接受到的是 Mbps, 然后我们的一个buffer 是2048byte, 差不多61个 133 | response.Data[index].Rate = uint32(response.Data[index].NodeSpeedlimit * 62) 134 | 135 | if info.Server["alterid"].(string) == "" { 136 | response.Data[index].AlterId = 2 137 | } else { 138 | alterid, err := strconv.ParseUint(info.Server["alterid"].(string), 10, 0) 139 | if err == nil { 140 | response.Data[index].AlterId = uint32(alterid) 141 | } 142 | } 143 | user := response.Data[index] 144 | response.Data[index].Muhost = get_mu_host(user.UserID, getMD5(fmt.Sprintf("%d%s%s%s%s", user.UserID, user.Passwd, user.Method, user.Obfs, user.Protocol)), api.MU_REGEX, api.MU_SUFFIX) 145 | key := prifix + response.Data[index].Email + fmt.Sprintf("Rate_%d_AlterID_%d_Method_%s_Passwd_%s_Port_%d_Obfs_%s_Protocol_%s", response.Data[index].Rate, 146 | response.Data[index].AlterId, response.Data[index].Method, response.Data[index].Passwd, response.Data[index].Port, response.Data[index].Obfs, response.Data[index].Protocol, 147 | ) 148 | response.Data[index].PrefixedId = key 149 | allusers.Data[key] = response.Data[index] 150 | } 151 | return &allusers, nil 152 | } 153 | 154 | func (api *SSRpanel) Post(url string, params map[string]interface{}, data map[string]interface{}) (*req.Resp, error) { 155 | 156 | return nil, nil 157 | } 158 | 159 | func (api *SSRpanel) UploadSystemLoad(nodeid uint) bool { 160 | uptime, _ := strconv.ParseFloat(utility.GetSystemUptime(), 64) 161 | err := api.Db.Create(&SSRNodeInfo{ 162 | NodeID: nodeid, 163 | Load: utility.GetSystemLoad(), 164 | Uptime: uptime, 165 | }).Error 166 | if err != nil { 167 | return false 168 | } 169 | return true 170 | } 171 | 172 | func (api *SSRpanel) flowAutoShow(value float64) string { 173 | value_float := value 174 | kb := 1024.0 175 | mb := 1048576.0 176 | gb := 1073741824.0 177 | tb := gb * 1024.0 178 | pb := tb * 1024.0 179 | if math.Abs(value_float) > pb { 180 | return fmt.Sprintf("%.2fPB", math.Round(value_float/pb)) 181 | } else if math.Abs(value_float) > tb { 182 | return fmt.Sprintf("%.2fTB", math.Round(value_float/tb)) 183 | } else if math.Abs(value_float) > gb { 184 | return fmt.Sprintf("%.2fGB", math.Round(value_float/gb)) 185 | } else if math.Abs(value_float) > mb { 186 | return fmt.Sprintf("%.2fMB", math.Round(value_float/mb)) 187 | } else if math.Abs(value_float) > kb { 188 | return fmt.Sprintf("%.2fKB", math.Round(value_float/kb)) 189 | } else { 190 | return fmt.Sprintf("%.2fB", math.Round(value_float)) 191 | } 192 | } 193 | 194 | func (api *SSRpanel) UpLoadUserTraffics(nodeid uint, trafficLog []model.UserTrafficLog) bool { 195 | var ssrnode SSRNode 196 | err := api.Db.First(&ssrnode, "id = ?", nodeid).Error 197 | if err != nil { 198 | return false 199 | } 200 | var this_time_total_bandwidth uint64 = 0 201 | if len(trafficLog) > 0 { 202 | for index := range trafficLog { 203 | traffic := trafficLog[index] 204 | 205 | var user SSRUserModel 206 | err := api.Db.First(&user, "id = ?", traffic.UserID).Error 207 | 208 | if err != nil { 209 | continue 210 | } 211 | user.Time = time.Now().Unix() 212 | user.Upload += int64(float64(traffic.Uplink) * ssrnode.TrafficRate) 213 | user.Download += int64(float64(traffic.Downlink) * ssrnode.TrafficRate) 214 | this_time_total_bandwidth += traffic.Uplink + traffic.Downlink 215 | api.Db.Save(&user) 216 | 217 | api.Db.Save(&SSRUserTrafficLog{ 218 | UserID: traffic.UserID, 219 | Uplink: traffic.Uplink, 220 | Downlink: traffic.Downlink, 221 | NodeID: nodeid, 222 | Rate: ssrnode.TrafficRate, 223 | LogTime: time.Now().Unix(), 224 | Traffic: api.flowAutoShow(float64(traffic.Uplink+traffic.Downlink) * ssrnode.TrafficRate), 225 | }) 226 | } 227 | } 228 | 229 | api.Db.Create(&SSRNodeOnlineLog{ 230 | NodeID: nodeid, 231 | OnlineUser: len(trafficLog), 232 | LogTime: time.Now().Unix(), 233 | }) 234 | return true 235 | } 236 | func (api *SSRpanel) UploadSpeedTest(nodeid uint, speedresult []speedtest.Speedresult) bool { 237 | return true 238 | } 239 | func (api *SSRpanel) UpLoadOnlineIps(nodeid uint, onlineIPS []model.UserOnLineIP) bool { 240 | var ssrnode SSRNode 241 | err := api.Db.First(&ssrnode, "id = ?", nodeid).Error 242 | if err != nil { 243 | return false 244 | } 245 | if len(onlineIPS) > 0 { 246 | for index := range onlineIPS { 247 | ip := onlineIPS[index] 248 | api.Db.Create(&SSRNodeIP{ 249 | NodeID: nodeid, 250 | UserID: ip.UserId, 251 | CreatedAt: time.Now().Unix(), 252 | }) 253 | } 254 | } 255 | return true 256 | } 257 | -------------------------------------------------------------------------------- /db/ssrpaneltabel.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "time" 6 | ) 7 | 8 | type SSRUserModel struct { 9 | ID uint 10 | VmessID string 11 | Passwd string 12 | Method string 13 | Port int 14 | SpeedLimitPerUser int 15 | Email string `gorm:"column:username"` 16 | Time int64 `gorm:"column:t"` 17 | Upload int64 `gorm:"column:u"` 18 | Download int64 `gorm:"column:d"` 19 | Obfs string `gorm:"column:obfs"` 20 | Protocol string `gorm:"column:protocol"` 21 | } 22 | 23 | func (*SSRUserModel) TableName() string { 24 | return "user" 25 | } 26 | 27 | type SSRUserTrafficLog struct { 28 | ID uint `gorm:"primary_key"` 29 | UserID uint 30 | Uplink uint64 `gorm:"column:u"` 31 | Downlink uint64 `gorm:"column:d"` 32 | NodeID uint 33 | Rate float64 34 | Traffic string 35 | LogTime int64 36 | } 37 | 38 | func (l *SSRUserTrafficLog) BeforeCreate(scope *gorm.Scope) error { 39 | l.LogTime = time.Now().Unix() 40 | return nil 41 | } 42 | 43 | type SSRNodeOnlineLog struct { 44 | ID uint `gorm:"primary_key"` 45 | NodeID uint 46 | OnlineUser int 47 | LogTime int64 48 | } 49 | 50 | func (*SSRNodeOnlineLog) TableName() string { 51 | return "ss_node_online_log" 52 | } 53 | 54 | func (l *SSRNodeOnlineLog) BeforeCreate(scope *gorm.Scope) error { 55 | l.LogTime = time.Now().Unix() 56 | return nil 57 | } 58 | 59 | type SSRNodeInfo struct { 60 | ID uint `gorm:"primary_key"` 61 | NodeID uint 62 | Uptime float64 63 | Load string 64 | LogTime int64 65 | } 66 | 67 | func (*SSRNodeInfo) TableName() string { 68 | return "ss_node_info" 69 | } 70 | 71 | func (l *SSRNodeInfo) BeforeCreate(scope *gorm.Scope) error { 72 | l.LogTime = time.Now().Unix() 73 | return nil 74 | } 75 | 76 | type SSRNode struct { 77 | ID uint `gorm:"primary_key"` 78 | TrafficRate float64 79 | Type uint `gorm:"column:type"` 80 | Method string `gorm:"column:method"` 81 | V2AlterId uint 82 | V2Port uint 83 | V2Net string 84 | V2Type string 85 | V2Host string 86 | V2Path string 87 | V2Tls uint 88 | Bandwidth uint 89 | Ip string 90 | V2rayInsiderPort string 91 | Server string 92 | } 93 | 94 | func (*SSRNode) TableName() string { 95 | return "ss_node" 96 | } 97 | 98 | type SSRNodeIP struct { 99 | ID uint `gorm:"primary_key"` 100 | NodeID uint 101 | UserID uint 102 | Ip string 103 | CreatedAt int64 104 | } 105 | 106 | func (*SSRNodeIP) TableName() string { 107 | return "ss_node_ip" 108 | } 109 | -------------------------------------------------------------------------------- /db/webapi.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "github.com/imroc/req" 6 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/model" 7 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/speedtest" 8 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/utility" 9 | "log" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type Webapi struct { 16 | WebToken string 17 | WebBaseURl string 18 | MU_REGEX string 19 | MU_SUFFIX string 20 | } 21 | 22 | func (api *Webapi) GetApi(url string, params map[string]interface{}) (*req.Resp, error) { 23 | req.SetTimeout(50 * time.Second) 24 | parm := req.Param{ 25 | "key": api.WebToken, 26 | } 27 | for k, v := range params { 28 | parm[k] = v 29 | } 30 | 31 | r, err := req.Get(fmt.Sprintf("%s/mod_mu/%s", api.WebBaseURl, url), parm) 32 | return r, err 33 | } 34 | 35 | func (api *Webapi) GetNodeInfo(nodeid uint) (*NodeinfoResponse, error) { 36 | var response = NodeinfoResponse{} 37 | var params map[string]interface{} 38 | 39 | r, err := api.GetApi(fmt.Sprintf("nodes/%d/info", nodeid), params) 40 | if err != nil { 41 | return &response, err 42 | } else { 43 | err = r.ToJSON(&response) 44 | if err != nil { 45 | return &response, err 46 | } else if response.Ret != 1 { 47 | return &response, err 48 | } 49 | } 50 | 51 | if response.Data.Server_raw != "" { 52 | response.Data.Server_raw = strings.ToLower(response.Data.Server_raw) 53 | data := strings.Split(response.Data.Server_raw, ";") 54 | var count uint 55 | count = 0 56 | for v := range data { 57 | if len(data[v]) > 0 { 58 | maps[id2string[count]] = data[v] 59 | } 60 | count += 1 61 | } 62 | var extraArgues []string 63 | if len(data) == 6 { 64 | extraArgues = append(extraArgues, strings.Split(data[5], "|")...) 65 | for item := range extraArgues { 66 | data = strings.Split(extraArgues[item], "=") 67 | if len(data) > 0 { 68 | if len(data[1]) > 0 { 69 | maps[data[0]] = data[1] 70 | } 71 | 72 | } 73 | } 74 | } 75 | 76 | if maps["protocol"] == "tls" { 77 | temp := maps["protocol_param"] 78 | maps["protocol"] = temp 79 | maps["protocol_param"] = "tls" 80 | } 81 | response.Data.Server = maps 82 | } 83 | response.Data.NodeID = nodeid 84 | return &response, nil 85 | } 86 | 87 | func (api *Webapi) GetDisNodeInfo(nodeid uint) (*DisNodenfoResponse, error) { 88 | var response = DisNodenfoResponse{} 89 | var params map[string]interface{} 90 | params = map[string]interface{}{ 91 | "node_id": nodeid, 92 | } 93 | r, err := api.GetApi("func/relay_rules", params) 94 | if err != nil { 95 | return &response, err 96 | } else { 97 | err = r.ToJSON(&response) 98 | if err != nil { 99 | return &response, err 100 | } else if response.Ret != 1 { 101 | return &response, err 102 | } 103 | } 104 | 105 | if len(response.Data) > 0 { 106 | for _, relayrule := range response.Data { 107 | relayrule.Server_raw = strings.ToLower(relayrule.Server_raw) 108 | data := strings.Split(relayrule.Server_raw, ";") 109 | var count uint 110 | count = 0 111 | for v := range data { 112 | if len(data[v]) > 1 { 113 | maps[id2string[count]] = data[v] 114 | } 115 | count += 1 116 | } 117 | var extraArgues []string 118 | if len(data) == 6 { 119 | extraArgues = append(extraArgues, strings.Split(data[5], "|")...) 120 | for item := range extraArgues { 121 | data = strings.Split(extraArgues[item], "=") 122 | if len(data) > 1 { 123 | if len(data[1]) > 1 { 124 | maps[data[0]] = data[1] 125 | } 126 | 127 | } 128 | } 129 | } 130 | 131 | if maps["protocol"] == "tls" { 132 | temp := maps["protocol_param"] 133 | maps["protocol"] = temp 134 | maps["protocol_param"] = "tls" 135 | } 136 | relayrule.Server = maps 137 | } 138 | } 139 | return &response, nil 140 | } 141 | 142 | func (api *Webapi) GetALLUsers(info *model.NodeInfo) (*AllUsers, error) { 143 | sort := info.Sort 144 | var prifix string 145 | var allusers = AllUsers{ 146 | Data: map[string]model.UserModel{}, 147 | } 148 | if sort == 0 { 149 | prifix = "SS_" 150 | } else { 151 | prifix = "Vmess_" 152 | if info.Server["protocol"] == "tcp" { 153 | prifix += "tcp_" 154 | } else if info.Server["protocol"] == "ws" { 155 | if info.Server["protocol_param"] != "" { 156 | prifix += "ws_" + info.Server["protocol_param"].(string) + "_" 157 | } else { 158 | prifix += "ws_" + "none" + "_" 159 | } 160 | } else if info.Server["protocol"] == "kcp" { 161 | if info.Server["protocol_param"] != "" { 162 | prifix += "kcp_" + info.Server["protocol_param"].(string) + "_" 163 | } else { 164 | prifix += "kcp_" + "none" + "_" 165 | } 166 | } 167 | } 168 | var response = UsersResponse{} 169 | params := map[string]interface{}{ 170 | "node_id": info.NodeID, 171 | } 172 | r, err := api.GetApi("users", params) 173 | if err != nil { 174 | return &allusers, err 175 | } else { 176 | err = r.ToJSON(&response) 177 | allusers.Ret = response.Ret 178 | if err != nil { 179 | return &allusers, err 180 | } else if response.Ret != 1 { 181 | return &allusers, err 182 | } 183 | } 184 | for index := range response.Data { 185 | // 按照node 限速来调整用户限速 186 | if info.NodeSpeedlimit != 0 { 187 | if info.NodeSpeedlimit > response.Data[index].NodeSpeedlimit && response.Data[index].NodeSpeedlimit != 0 { 188 | } else if response.Data[index].NodeSpeedlimit == 0 || response.Data[index].NodeSpeedlimit > info.NodeSpeedlimit { 189 | response.Data[index].NodeSpeedlimit = info.NodeSpeedlimit 190 | } 191 | } 192 | // 接受到的是 Mbps, 然后我们的一个buffer 是2048byte, 差不多61个 193 | response.Data[index].Rate = uint32(response.Data[index].NodeSpeedlimit * 62) 194 | if info.Server["alterid"].(string) == "" { 195 | response.Data[index].AlterId = 2 196 | } else { 197 | alterid, err := strconv.ParseUint(info.Server["alterid"].(string), 10, 0) 198 | if err == nil { 199 | response.Data[index].AlterId = uint32(alterid) 200 | } 201 | } 202 | user := response.Data[index] 203 | response.Data[index].Muhost = get_mu_host(user.UserID, getMD5(fmt.Sprintf("%d%s%s%s%s", user.UserID, user.Passwd, user.Method, user.Obfs, user.Protocol)), api.MU_REGEX, api.MU_SUFFIX) 204 | key := prifix + response.Data[index].Email + fmt.Sprintf("Rate_%d_AlterID_%d_Method_%s_Passwd_%s_Port_%d_Obfs_%s_Protocol_%s", response.Data[index].Rate, 205 | response.Data[index].AlterId, response.Data[index].Method, response.Data[index].Passwd, response.Data[index].Port, response.Data[index].Obfs, response.Data[index].Protocol, 206 | ) 207 | response.Data[index].PrefixedId = key 208 | allusers.Data[key] = response.Data[index] 209 | } 210 | return &allusers, nil 211 | } 212 | 213 | func (api *Webapi) Post(url string, params map[string]interface{}, data map[string]interface{}) (*req.Resp, error) { 214 | parm := req.Param{ 215 | "key": api.WebToken, 216 | } 217 | for k, v := range params { 218 | parm[k] = v 219 | } 220 | r, err := req.Post(fmt.Sprintf("%s/mod_mu/%s", api.WebBaseURl, url), parm, req.BodyJSON(&data)) 221 | return r, err 222 | } 223 | 224 | func (api *Webapi) UploadSystemLoad(nodeid uint) bool { 225 | var postresponse PostResponse 226 | params := map[string]interface{}{ 227 | "node_id": nodeid, 228 | } 229 | upload_systemload := map[string]interface{}{ 230 | "uptime": utility.GetSystemUptime(), 231 | "load": utility.GetSystemLoad(), 232 | } 233 | r, err := api.Post(fmt.Sprintf("nodes/%d/info", nodeid), params, upload_systemload) 234 | if err != nil { 235 | return false 236 | } else { 237 | err = r.ToJSON(&postresponse) 238 | if err != nil { 239 | return false 240 | } else if postresponse.Ret != 1 { 241 | log.Fatal(postresponse.Data) 242 | } 243 | } 244 | return true 245 | } 246 | 247 | func (api *Webapi) UpLoadUserTraffics(nodeid uint, trafficLog []model.UserTrafficLog) bool { 248 | var postresponse PostResponse 249 | params := map[string]interface{}{ 250 | "node_id": nodeid, 251 | } 252 | 253 | data := map[string]interface{}{ 254 | "data": trafficLog, 255 | } 256 | r, err := api.Post("users/traffic", params, data) 257 | if err != nil { 258 | return false 259 | } else { 260 | err = r.ToJSON(&postresponse) 261 | if err != nil { 262 | return false 263 | } else if postresponse.Ret != 1 { 264 | log.Fatal(postresponse.Data) 265 | } 266 | } 267 | return true 268 | } 269 | func (api *Webapi) UploadSpeedTest(nodeid uint, speedresult []speedtest.Speedresult) bool { 270 | var postresponse PostResponse 271 | params := map[string]interface{}{ 272 | "node_id": nodeid, 273 | } 274 | 275 | data := map[string]interface{}{ 276 | "data": speedresult, 277 | } 278 | r, err := api.Post("func/speedtest", params, data) 279 | if err != nil { 280 | return false 281 | } else { 282 | err = r.ToJSON(&postresponse) 283 | if err != nil { 284 | return false 285 | } else if postresponse.Ret != 1 { 286 | log.Fatal(postresponse.Data) 287 | } 288 | } 289 | return true 290 | } 291 | func (api *Webapi) UpLoadOnlineIps(nodeid uint, onlineIPS []model.UserOnLineIP) bool { 292 | var postresponse PostResponse 293 | params := map[string]interface{}{ 294 | "node_id": nodeid, 295 | } 296 | 297 | data := map[string]interface{}{ 298 | "data": onlineIPS, 299 | } 300 | r, err := api.Post("users/aliveip", params, data) 301 | if err != nil { 302 | return false 303 | } else { 304 | err = r.ToJSON(&postresponse) 305 | if err != nil { 306 | return false 307 | } else if postresponse.Ret != 1 { 308 | log.Fatal(postresponse.Data) 309 | } 310 | } 311 | return true 312 | } 313 | -------------------------------------------------------------------------------- /install-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file is accessible as https://install.direct/go.sh 4 | # Original source is located at github.com/v2ray/v2ray-core/release/install-release.sh 5 | 6 | # If not specify, default meaning of return value: 7 | # 0: Success 8 | # 1: System error 9 | # 2: Application error 10 | # 3: Network error 11 | 12 | CUR_VER="" 13 | NEW_VER="" 14 | ARCH="" 15 | VDIS="64" 16 | ZIPFILE="/tmp/v2ray/v2ray.zip" 17 | V2RAY_RUNNING=0 18 | VSRC_ROOT="/tmp/v2ray" 19 | EXTRACT_ONLY=0 20 | ERROR_IF_UPTODATE=0 21 | 22 | CMD_INSTALL="" 23 | CMD_UPDATE="" 24 | SOFTWARE_UPDATED=0 25 | 26 | SYSTEMCTL_CMD=$(command -v systemctl 2>/dev/null) 27 | SERVICE_CMD=$(command -v service 2>/dev/null) 28 | 29 | CHECK="" 30 | FORCE="" 31 | HELP="" 32 | 33 | #######color code######## 34 | RED="31m" # Error message 35 | GREEN="32m" # Success message 36 | YELLOW="33m" # Warning message 37 | BLUE="36m" # Info message 38 | 39 | 40 | ######################### 41 | while [[ $# > 0 ]];do 42 | key="$1" 43 | case $key in 44 | -p|--proxy) 45 | PROXY="-x ${2}" 46 | shift # past argument 47 | ;; 48 | -h|--help) 49 | HELP="1" 50 | ;; 51 | -f|--force) 52 | FORCE="1" 53 | ;; 54 | -c|--check) 55 | CHECK="1" 56 | ;; 57 | --remove) 58 | REMOVE="1" 59 | ;; 60 | --version) 61 | VERSION="$2" 62 | shift 63 | ;; 64 | --extract) 65 | VSRC_ROOT="$2" 66 | shift 67 | ;; 68 | --extractonly) 69 | EXTRACT_ONLY="1" 70 | ;; 71 | -l|--local) 72 | LOCAL="$2" 73 | LOCAL_INSTALL="1" 74 | shift 75 | ;; 76 | --errifuptodate) 77 | ERROR_IF_UPTODATE="1" 78 | ;; 79 | --panelurl) 80 | PANELURL="$2" 81 | ;; 82 | --panelkey) 83 | PANELKEY="$2" 84 | ;; 85 | --nodeid) 86 | NODEID="$2" 87 | ;; 88 | --downwithpanel) 89 | DOWNWITHPANEL="$2" 90 | ;; 91 | --mysqlhost) 92 | MYSQLHOST="$2" 93 | ;; 94 | --mysqldbname) 95 | MYSQLDBNAME="$2" 96 | ;; 97 | --mysqluser) 98 | MYSQLUSR="$2" 99 | ;; 100 | --mysqlpasswd) 101 | MYSQLPASSWD="$2" 102 | ;; 103 | --mysqlport) 104 | MYSQLPORT="$2" 105 | ;; 106 | --speedtestrate) 107 | SPEEDTESTRATE="$2" 108 | ;; 109 | --paneltype) 110 | PANELTYPE="$2" 111 | ;; 112 | --usemysql) 113 | USEMYSQL="$2" 114 | ;; 115 | *) 116 | # unknown option 117 | ;; 118 | esac 119 | shift # past argument or value 120 | done 121 | 122 | ############################### 123 | colorEcho(){ 124 | COLOR=$1 125 | echo -e "\033[${COLOR}${@:2}\033[0m" 126 | } 127 | 128 | sysArch(){ 129 | ARCH=$(uname -m) 130 | if [[ "$ARCH" == "i686" ]] || [[ "$ARCH" == "i386" ]]; then 131 | VDIS="32" 132 | elif [[ "$ARCH" == *"armv7"* ]] || [[ "$ARCH" == "armv6l" ]]; then 133 | VDIS="arm" 134 | elif [[ "$ARCH" == *"armv8"* ]] || [[ "$ARCH" == "aarch64" ]]; then 135 | VDIS="arm64" 136 | elif [[ "$ARCH" == *"mips64le"* ]]; then 137 | VDIS="mips64le" 138 | elif [[ "$ARCH" == *"mips64"* ]]; then 139 | VDIS="mips64" 140 | elif [[ "$ARCH" == *"mipsle"* ]]; then 141 | VDIS="mipsle" 142 | elif [[ "$ARCH" == *"mips"* ]]; then 143 | VDIS="mips" 144 | elif [[ "$ARCH" == *"s390x"* ]]; then 145 | VDIS="s390x" 146 | elif [[ "$ARCH" == "ppc64le" ]]; then 147 | VDIS="ppc64le" 148 | elif [[ "$ARCH" == "ppc64" ]]; then 149 | VDIS="ppc64" 150 | fi 151 | return 0 152 | } 153 | 154 | downloadV2Ray(){ 155 | rm -rf /tmp/v2ray 156 | mkdir -p /tmp/v2ray 157 | colorEcho ${BLUE} "Downloading V2Ray." 158 | DOWNLOAD_LINK="https://github.com/v2ray/v2ray-core/releases/download/v4.19.1/v2ray-linux-64.zip" 159 | curl ${PROXY} -L -H "Cache-Control: no-cache" -o ${ZIPFILE} ${DOWNLOAD_LINK} 160 | if [ $? != 0 ];then 161 | colorEcho ${RED} "Failed to download! Please check your network or try again." 162 | return 3 163 | fi 164 | return 0 165 | } 166 | 167 | installSoftware(){ 168 | COMPONENT=$1 169 | if [[ -n `command -v $COMPONENT` ]]; then 170 | return 0 171 | fi 172 | 173 | getPMT 174 | if [[ $? -eq 1 ]]; then 175 | colorEcho ${RED} "The system package manager tool isn't APT or YUM, please install ${COMPONENT} manually." 176 | return 1 177 | fi 178 | if [[ $SOFTWARE_UPDATED -eq 0 ]]; then 179 | colorEcho ${BLUE} "Updating software repo" 180 | $CMD_UPDATE 181 | SOFTWARE_UPDATED=1 182 | fi 183 | 184 | colorEcho ${BLUE} "Installing ${COMPONENT}" 185 | $CMD_INSTALL $COMPONENT 186 | if [[ $? -ne 0 ]]; then 187 | colorEcho ${RED} "Failed to install ${COMPONENT}. Please install it manually." 188 | return 1 189 | fi 190 | return 0 191 | } 192 | 193 | # return 1: not apt, yum, or zypper 194 | getPMT(){ 195 | if [[ -n `command -v apt-get` ]];then 196 | CMD_INSTALL="apt-get -y -qq install" 197 | CMD_UPDATE="apt-get -qq update" 198 | elif [[ -n `command -v yum` ]]; then 199 | CMD_INSTALL="yum -y -q install" 200 | CMD_UPDATE="yum -q makecache" 201 | elif [[ -n `command -v zypper` ]]; then 202 | CMD_INSTALL="zypper -y install" 203 | CMD_UPDATE="zypper ref" 204 | else 205 | return 1 206 | fi 207 | return 0 208 | } 209 | 210 | extract(){ 211 | colorEcho ${BLUE}"Extracting V2Ray package to /tmp/v2ray." 212 | mkdir -p /tmp/v2ray 213 | unzip $1 -d ${VSRC_ROOT} 214 | if [[ $? -ne 0 ]]; then 215 | colorEcho ${RED} "Failed to extract V2Ray." 216 | return 2 217 | fi 218 | if [[ -d "/tmp/v2ray/v2ray-${NEW_VER}-linux-${VDIS}" ]]; then 219 | VSRC_ROOT="/tmp/v2ray/v2ray-${NEW_VER}-linux-${VDIS}" 220 | fi 221 | return 0 222 | } 223 | 224 | 225 | # 1: new V2Ray. 0: no. 2: not installed. 3: check failed. 4: don't check. 226 | getVersion(){ 227 | if [[ -n "$VERSION" ]]; then 228 | NEW_VER="$VERSION" 229 | if [[ ${NEW_VER} != v* ]]; then 230 | NEW_VER=v${NEW_VER} 231 | fi 232 | return 4 233 | else 234 | VER=`/usr/bin/v2ray/v2ray -version 2>/dev/null` 235 | RETVAL="$?" 236 | CUR_VER=`echo $VER | head -n 1 | cut -d " " -f2` 237 | if [[ ${CUR_VER} != v* ]]; then 238 | CUR_VER=v${CUR_VER} 239 | fi 240 | TAG_URL="https://api.github.com/repos/githubphone/pay-v2ray-sspanel-v3-mod_Uim-plugin/releases/latest" 241 | NEW_VER=`curl ${PROXY} -s ${TAG_URL} --connect-timeout 10| grep 'tag_name' | cut -d\" -f4` 242 | if [[ ${NEW_VER} != v* ]]; then 243 | NEW_VER=v${NEW_VER} 244 | fi 245 | if [[ $? -ne 0 ]] || [[ $NEW_VER == "" ]]; then 246 | colorEcho ${RED} "Failed to fetch release information. Please check your network or try again." 247 | return 3 248 | elif [[ $RETVAL -ne 0 ]];then 249 | return 2 250 | elif [[ `echo $NEW_VER | cut -d. -f-2` != `echo $CUR_VER | cut -d. -f-2` ]];then 251 | return 1 252 | fi 253 | return 0 254 | fi 255 | } 256 | 257 | stopV2ray(){ 258 | colorEcho ${BLUE} "Shutting down V2Ray service." 259 | if [[ -n "${SYSTEMCTL_CMD}" ]] || [[ -f "/lib/systemd/system/v2ray.service" ]] || [[ -f "/etc/systemd/system/v2ray.service" ]]; then 260 | ${SYSTEMCTL_CMD} stop v2ray 261 | elif [[ -n "${SERVICE_CMD}" ]] || [[ -f "/etc/init.d/v2ray" ]]; then 262 | ${SERVICE_CMD} v2ray stop 263 | fi 264 | if [[ $? -ne 0 ]]; then 265 | colorEcho ${YELLOW} "Failed to shutdown V2Ray service." 266 | return 2 267 | fi 268 | return 0 269 | } 270 | 271 | startV2ray(){ 272 | if [ -n "${SYSTEMCTL_CMD}" ] && [ -f "/lib/systemd/system/v2ray.service" ]; then 273 | ${SYSTEMCTL_CMD} start v2ray 274 | elif [ -n "${SYSTEMCTL_CMD}" ] && [ -f "/etc/systemd/system/v2ray.service" ]; then 275 | ${SYSTEMCTL_CMD} start v2ray 276 | elif [ -n "${SERVICE_CMD}" ] && [ -f "/etc/init.d/v2ray" ]; then 277 | ${SERVICE_CMD} v2ray start 278 | fi 279 | if [[ $? -ne 0 ]]; then 280 | colorEcho ${YELLOW} "Failed to start V2Ray service." 281 | return 2 282 | fi 283 | return 0 284 | } 285 | 286 | copyFile() { 287 | NAME=$1 288 | ERROR=`cp "${VSRC_ROOT}/${NAME}" "/usr/bin/v2ray/${NAME}" 2>&1` 289 | if [[ $? -ne 0 ]]; then 290 | colorEcho ${YELLOW} "${ERROR}" 291 | return 1 292 | fi 293 | return 0 294 | } 295 | 296 | makeExecutable() { 297 | chmod +x "/usr/bin/v2ray/$1" 298 | } 299 | 300 | installV2Ray(){ 301 | # Install V2Ray binary to /usr/bin/v2ray 302 | mkdir -p /usr/bin/v2ray 303 | copyFile v2ray 304 | if [[ $? -ne 0 ]]; then 305 | colorEcho ${RED} "Failed to copy V2Ray binary and resources." 306 | return 1 307 | fi 308 | makeExecutable v2ray 309 | copyFile v2ctl && makeExecutable v2ctl 310 | copyFile geoip.dat 311 | copyFile geosite.dat 312 | colorEcho ${BLUE} "Setting TimeZone to Shanghai" 313 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 314 | date -s "$(curl -sI g.cn | grep Date | cut -d' ' -f3-6)Z" 315 | 316 | # Install V2Ray server config to /etc/v2ray 317 | if [[ ! -f "/etc/v2ray/config.json" ]]; then 318 | mkdir -p /etc/v2ray 319 | mkdir -p /var/log/v2ray 320 | cp "${VSRC_ROOT}/vpoint_vmess_freedom.json" "/etc/v2ray/config.json" 321 | if [[ $? -ne 0 ]]; then 322 | colorEcho ${YELLOW} "Failed to create V2Ray configuration file. Please create it manually." 323 | return 1 324 | fi 325 | 326 | if [ ! -z "${PANELURL}" ] 327 | then 328 | sed -i "s|"https://google.com"|"${PANELURL}"|g" "/etc/v2ray/config.json" 329 | colorEcho ${BLUE} "PANELURL:${PANELURL}" 330 | fi 331 | if [ ! -z "${PANELKEY}" ] 332 | then 333 | sed -i "s/"55fUxDGFzH3n"/"${PANELKEY}"/g" "/etc/v2ray/config.json" 334 | colorEcho ${BLUE} "PANELKEY:${PANELKEY}" 335 | 336 | fi 337 | if [ ! -z "${NODEID}" ] 338 | then 339 | sed -i "s/123456,/${NODEID},/g" "/etc/v2ray/config.json" 340 | colorEcho ${BLUE} "NODEID:${NODEID}" 341 | 342 | fi 343 | 344 | if [ ! -z "${DOWNWITHPANEL}" ] 345 | then 346 | sed -i "s|\"downWithPanel\": 1|\"downWithPanel\": ${DOWNWITHPANEL}|g" "/etc/v2ray/config.json" 347 | colorEcho ${BLUE} "DOWNWITHPANEL:${DOWNWITHPANEL}" 348 | fi 349 | 350 | if [ ! -z "${MYSQLHOST}" ] 351 | then 352 | sed -i "s|"https://bing.com"|"${MYSQLHOST}"|g" "/etc/v2ray/config.json" 353 | colorEcho ${BLUE} "MYSQLHOST:${MYSQLHOST}" 354 | 355 | fi 356 | if [ ! -z "${MYSQLDBNAME}" ] 357 | then 358 | sed -i "s/"demo_dbname"/"${MYSQLDBNAME}"/g" "/etc/v2ray/config.json" 359 | colorEcho ${BLUE} "MYSQLDBNAME:${MYSQLDBNAME}" 360 | 361 | fi 362 | if [ ! -z "${MYSQLUSR}" ] 363 | then 364 | sed -i "s|\"demo_user\"|\"${MYSQLUSR}\"|g" "/etc/v2ray/config.json" 365 | colorEcho ${BLUE} "MYSQLUSR:${MYSQLUSR}" 366 | fi 367 | if [ ! -z "${MYSQLPASSWD}" ] 368 | then 369 | sed -i "s/"demo_dbpassword"/"${MYSQLPASSWD}"/g" "/etc/v2ray/config.json" 370 | colorEcho ${BLUE} "MYSQLPASSWD:${MYSQLPASSWD}" 371 | 372 | fi 373 | if [ ! -z "${MYSQLPORT}" ] 374 | then 375 | sed -i "s/3306,/${MYSQLPORT},/g" "/etc/v2ray/config.json" 376 | colorEcho ${BLUE} "MYSQLPORT:${MYSQLPORT}" 377 | 378 | fi 379 | 380 | if [ ! -z "${SPEEDTESTRATE}" ] 381 | then 382 | sed -i "s|\"SpeedTestCheckRate\": 6|\"SpeedTestCheckRate\": ${SPEEDTESTRATE}|g" "/etc/v2ray/config.json" 383 | colorEcho ${BLUE} "SPEEDTESTRATE:${SPEEDTESTRATE}" 384 | 385 | fi 386 | if [ ! -z "${PANELTYPE}" ] 387 | then 388 | sed -i "s|\"paneltype\": 0|\"paneltype\": ${PANELTYPE}|g" "/etc/v2ray/config.json" 389 | colorEcho ${BLUE} "PANELTYPE:${PANELTYPE}" 390 | 391 | fi 392 | if [ ! -z "${USEMYSQL}" ] 393 | then 394 | sed -i "s|\"usemysql\": 0|\"usemysql\": ${USEMYSQL}|g" "/etc/v2ray/config.json" 395 | colorEcho ${BLUE} "USEMYSQL:${USEMYSQL}" 396 | 397 | fi 398 | 399 | 400 | fi 401 | return 0 402 | } 403 | 404 | 405 | installInitScript(){ 406 | if [[ -n "${SYSTEMCTL_CMD}" ]];then 407 | if [[ ! -f "/etc/systemd/system/v2ray.service" ]]; then 408 | if [[ ! -f "/lib/systemd/system/v2ray.service" ]]; then 409 | cp "${VSRC_ROOT}/systemd/v2ray.service" "/etc/systemd/system/" 410 | systemctl enable v2ray.service 411 | fi 412 | fi 413 | return 414 | elif [[ -n "${SERVICE_CMD}" ]] && [[ ! -f "/etc/init.d/v2ray" ]]; then 415 | installSoftware "daemon" || return $? 416 | cp "${VSRC_ROOT}/systemv/v2ray" "/etc/init.d/v2ray" 417 | chmod +x "/etc/init.d/v2ray" 418 | update-rc.d v2ray defaults 419 | fi 420 | return 421 | } 422 | 423 | Help(){ 424 | echo "./install-release.sh [-h] [-c] [--remove] [-p proxy] [-f] [--version vx.y.z] [-l file]" 425 | echo " -h, --help Show help" 426 | echo " -p, --proxy To download through a proxy server, use -p socks5://127.0.0.1:1080 or -p http://127.0.0.1:3128 etc" 427 | echo " -f, --force Force install" 428 | echo " --version Install a particular version, use --version v3.15" 429 | echo " -l, --local Install from a local file" 430 | echo " --remove Remove installed V2Ray" 431 | echo " -c, --check Check for update" 432 | return 0 433 | } 434 | 435 | remove(){ 436 | if [[ -n "${SYSTEMCTL_CMD}" ]] && [[ -f "/etc/systemd/system/v2ray.service" ]];then 437 | if pgrep "v2ray" > /dev/null ; then 438 | stopV2ray 439 | fi 440 | systemctl disable v2ray.service 441 | rm -rf "/usr/bin/v2ray" "/etc/systemd/system/v2ray.service" 442 | if [[ $? -ne 0 ]]; then 443 | colorEcho ${RED} "Failed to remove V2Ray." 444 | return 0 445 | else 446 | colorEcho ${GREEN} "Removed V2Ray successfully." 447 | colorEcho ${BLUE} "If necessary, please remove configuration file and log file manually." 448 | return 0 449 | fi 450 | elif [[ -n "${SYSTEMCTL_CMD}" ]] && [[ -f "/lib/systemd/system/v2ray.service" ]];then 451 | if pgrep "v2ray" > /dev/null ; then 452 | stopV2ray 453 | fi 454 | systemctl disable v2ray.service 455 | rm -rf "/usr/bin/v2ray" "/lib/systemd/system/v2ray.service" 456 | if [[ $? -ne 0 ]]; then 457 | colorEcho ${RED} "Failed to remove V2Ray." 458 | return 0 459 | else 460 | colorEcho ${GREEN} "Removed V2Ray successfully." 461 | colorEcho ${BLUE} "If necessary, please remove configuration file and log file manually." 462 | return 0 463 | fi 464 | elif [[ -n "${SERVICE_CMD}" ]] && [[ -f "/etc/init.d/v2ray" ]]; then 465 | if pgrep "v2ray" > /dev/null ; then 466 | stopV2ray 467 | fi 468 | rm -rf "/usr/bin/v2ray" "/etc/init.d/v2ray" 469 | if [[ $? -ne 0 ]]; then 470 | colorEcho ${RED} "Failed to remove V2Ray." 471 | return 0 472 | else 473 | colorEcho ${GREEN} "Removed V2Ray successfully." 474 | colorEcho ${BLUE} "If necessary, please remove configuration file and log file manually." 475 | return 0 476 | fi 477 | else 478 | colorEcho ${YELLOW} "V2Ray not found." 479 | return 0 480 | fi 481 | } 482 | 483 | checkUpdate(){ 484 | echo "Checking for update." 485 | VERSION="" 486 | getVersion 487 | RETVAL="$?" 488 | if [[ $RETVAL -eq 1 ]]; then 489 | colorEcho ${BLUE} "Found new version ${NEW_VER} for V2Ray.(Current version:$CUR_VER)" 490 | elif [[ $RETVAL -eq 0 ]]; then 491 | colorEcho ${BLUE} "No new version. Current version is ${NEW_VER}." 492 | elif [[ $RETVAL -eq 2 ]]; then 493 | colorEcho ${YELLOW} "No V2Ray installed." 494 | colorEcho ${BLUE} "The newest version for V2Ray is ${NEW_VER}." 495 | fi 496 | return 0 497 | } 498 | 499 | main(){ 500 | #helping information 501 | [[ "$HELP" == "1" ]] && Help && return 502 | [[ "$CHECK" == "1" ]] && checkUpdate && return 503 | [[ "$REMOVE" == "1" ]] && remove && return 504 | 505 | sysArch 506 | # extract local file 507 | if [[ $LOCAL_INSTALL -eq 1 ]]; then 508 | colorEcho ${YELLOW} "Installing V2Ray via local file. Please make sure the file is a valid V2Ray package, as we are not able to determine that." 509 | NEW_VER=local 510 | installSoftware unzip || return $? 511 | installSoftware "socat" || return $? 512 | colorEcho ${YELLOW} "Downloading acme.sh" 513 | curl https://get.acme.sh | sh 514 | rm -rf /tmp/v2ray 515 | extract $LOCAL || return $? 516 | #FILEVDIS=`ls /tmp/v2ray |grep v2ray-v |cut -d "-" -f4` 517 | #SYSTEM=`ls /tmp/v2ray |grep v2ray-v |cut -d "-" -f3` 518 | #if [[ ${SYSTEM} != "linux" ]]; then 519 | # colorEcho ${RED} "The local V2Ray can not be installed in linux." 520 | # return 1 521 | #elif [[ ${FILEVDIS} != ${VDIS} ]]; then 522 | # colorEcho ${RED} "The local V2Ray can not be installed in ${ARCH} system." 523 | # return 1 524 | #else 525 | # NEW_VER=`ls /tmp/v2ray |grep v2ray-v |cut -d "-" -f2` 526 | #fi 527 | else 528 | # download via network and extract 529 | installSoftware "curl" || return $? 530 | installSoftware "socat" || return $? 531 | colorEcho ${YELLOW} "Downloading acme.sh" 532 | curl https://get.acme.sh | sh 533 | getVersion 534 | RETVAL="$?" 535 | if [[ $RETVAL == 0 ]] && [[ "$FORCE" != "1" ]]; then 536 | colorEcho ${BLUE} "Latest version ${NEW_VER} is already installed." 537 | if [[ "${ERROR_IF_UPTODATE}" == "1" ]]; then 538 | return 10 539 | fi 540 | return 541 | elif [[ $RETVAL == 3 ]]; then 542 | return 3 543 | else 544 | colorEcho ${BLUE} "Installing V2Ray ${NEW_VER} on ${ARCH}" 545 | downloadV2Ray || return $? 546 | installSoftware unzip || return $? 547 | extract ${ZIPFILE} || return $? 548 | fi 549 | fi 550 | 551 | if [[ "${EXTRACT_ONLY}" == "1" ]]; then 552 | colorEcho ${GREEN} "V2Ray extracted to ${VSRC_ROOT}, and exiting..." 553 | return 0 554 | fi 555 | 556 | if pgrep "v2ray" > /dev/null ; then 557 | V2RAY_RUNNING=1 558 | stopV2ray 559 | fi 560 | installV2Ray || return $? 561 | installInitScript || return $? 562 | if [[ ${V2RAY_RUNNING} -eq 1 ]];then 563 | colorEcho ${BLUE} "Restarting V2Ray service." 564 | startV2ray 565 | fi 566 | colorEcho ${GREEN} "V2Ray ${NEW_VER} is installed." 567 | rm -rf /tmp/v2ray 568 | return 0 569 | } 570 | 571 | main 572 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin 3 | export PATH 4 | 5 | # Current folder 6 | cur_dir=`pwd` 7 | # Color 8 | red='\033[0;31m' 9 | green='\033[0;32m' 10 | yellow='\033[0;33m' 11 | plain='\033[0m' 12 | software=(Docker_Caddy Docker_Caddy_cloudflare Docker) 13 | operation=(install update_config update_image logs) 14 | # Make sure only root can run our script 15 | [[ $EUID -ne 0 ]] && echo -e "[${red}Error${plain}] This script must be run as root!" && exit 1 16 | 17 | #Check system 18 | check_sys(){ 19 | local checkType=$1 20 | local value=$2 21 | 22 | local release='' 23 | local systemPackage='' 24 | 25 | if [[ -f /etc/redhat-release ]]; then 26 | release="centos" 27 | systemPackage="yum" 28 | elif grep -Eqi "debian|raspbian" /etc/issue; then 29 | release="debian" 30 | systemPackage="apt" 31 | elif grep -Eqi "ubuntu" /etc/issue; then 32 | release="ubuntu" 33 | systemPackage="apt" 34 | elif grep -Eqi "centos|red hat|redhat" /etc/issue; then 35 | release="centos" 36 | systemPackage="yum" 37 | elif grep -Eqi "debian|raspbian" /proc/version; then 38 | release="debian" 39 | systemPackage="apt" 40 | elif grep -Eqi "ubuntu" /proc/version; then 41 | release="ubuntu" 42 | systemPackage="apt" 43 | elif grep -Eqi "centos|red hat|redhat" /proc/version; then 44 | release="centos" 45 | systemPackage="yum" 46 | fi 47 | 48 | if [[ "${checkType}" == "sysRelease" ]]; then 49 | if [ "${value}" == "${release}" ]; then 50 | return 0 51 | else 52 | return 1 53 | fi 54 | elif [[ "${checkType}" == "packageManager" ]]; then 55 | if [ "${value}" == "${systemPackage}" ]; then 56 | return 0 57 | else 58 | return 1 59 | fi 60 | fi 61 | } 62 | 63 | # Get version 64 | getversion(){ 65 | if [[ -s /etc/redhat-release ]]; then 66 | grep -oE "[0-9.]+" /etc/redhat-release 67 | else 68 | grep -oE "[0-9.]+" /etc/issue 69 | fi 70 | } 71 | 72 | # CentOS version 73 | centosversion(){ 74 | if check_sys sysRelease centos; then 75 | local code=$1 76 | local version="$(getversion)" 77 | local main_ver=${version%%.*} 78 | if [ "$main_ver" == "$code" ]; then 79 | return 0 80 | else 81 | return 1 82 | fi 83 | else 84 | return 1 85 | fi 86 | } 87 | 88 | get_char(){ 89 | SAVEDSTTY=`stty -g` 90 | stty -echo 91 | stty cbreak 92 | dd if=/dev/tty bs=1 count=1 2> /dev/null 93 | stty -raw 94 | stty echo 95 | stty $SAVEDSTTY 96 | } 97 | error_detect_depends(){ 98 | local command=$1 99 | local depend=`echo "${command}" | awk '{print $4}'` 100 | echo -e "[${green}Info${plain}] Starting to install package ${depend}" 101 | ${command} > /dev/null 2>&1 102 | if [ $? -ne 0 ]; then 103 | echo -e "[${red}Error${plain}] Failed to install ${red}${depend}${plain}" 104 | echo "Please visit: https://teddysun.com/486.html and contact." 105 | exit 1 106 | fi 107 | } 108 | 109 | # Pre-installation settings 110 | pre_install_docker_compose(){ 111 | echo "Which Panel Do you use SSpanel 0, SSRpanel 1" 112 | read -p "(v2ray_paneltype (Default 0):" v2ray_paneltype 113 | [ -z "${v2ray_paneltype}" ] && v2ray_paneltype=0 114 | echo 115 | echo "---------------------------" 116 | echo "v2ray_paneltype = ${v2ray_paneltype}" 117 | echo "---------------------------" 118 | echo 119 | # Set ssrpanel node_id 120 | echo "node_id" 121 | read -p "(Default value: 0 ):" ssrpanel_node_id 122 | [ -z "${ssrpanel_node_id}" ] && ssrpanel_node_id=0 123 | echo 124 | echo "---------------------------" 125 | echo "node_id = ${ssrpanel_node_id}" 126 | echo "---------------------------" 127 | echo 128 | 129 | 130 | echo "Which connection do you prefer 0 for webapi and 1 for mysql" 131 | read -p "(v2ray_usemysql (Default 0):" v2ray_usemysql 132 | [ -z "${v2ray_usemysql}" ] && v2ray_usemysql=0 133 | echo 134 | echo "---------------------------" 135 | echo "v2ray_usemysql = ${v2ray_usemysql}" 136 | echo "---------------------------" 137 | echo 138 | 139 | # Set ssrpanel_url 140 | echo "Please sspanel_url, u can pass this setting if u use mysql" 141 | read -p "(There is no default value please make sure you input the right thing):" ssrpanel_url 142 | [ -z "${ssrpanel_url}" ] 143 | echo 144 | echo "---------------------------" 145 | echo "ssrpanel_url = ${ssrpanel_url}" 146 | echo "---------------------------" 147 | echo 148 | # Set ssrpanel key 149 | echo "sspanel key u can pass this setting if u use mysql" 150 | read -p "(There is no default value please make sure you input the right thing):" ssrpanel_key 151 | [ -z "${ssrpanel_key}" ] 152 | echo 153 | echo "---------------------------" 154 | echo "ssrpanel_key = ${ssrpanel_key}" 155 | echo "---------------------------" 156 | echo 157 | # Set Setting if the node go downwith panel 158 | echo "Setting Myqlhost, u can pass this setting u chosen use webapi" 159 | read -p "(v2ray_downWithPanel :" v2ray_mysqlhost 160 | [ -z "${v2ray_mysqlhost}" ] && v2ray_mysqlhost="" 161 | echo 162 | echo "---------------------------" 163 | echo "v2ray_mysqlhost = ${v2ray_mysqlhost}" 164 | echo "---------------------------" 165 | echo 166 | # Set Setting if the node go downwith panel 167 | echo "Setting MysqlPort u can pass this setting u chosen use webapi" 168 | read -p "(v2ray_mysqlport (Default 3306):" v2ray_mysqlport 169 | [ -z "${v2ray_mysqlport}" ] && v2ray_mysqlport=3306 170 | echo 171 | echo "---------------------------" 172 | echo "v2ray_mysqlport = ${v2ray_mysqlport}" 173 | echo "---------------------------" 174 | echo 175 | # Set Setting if the node go downwith panel 176 | echo "Setting MysqlUser u can pass this setting if u use webapi" 177 | read -p "(v2ray_myqluser (Default root):" v2ray_myqluser 178 | [ -z "${v2ray_myqluser}" ] && v2ray_myqluser="root" 179 | echo 180 | echo "---------------------------" 181 | echo "v2ray_myqluser = ${v2ray_myqluser}" 182 | echo "---------------------------" 183 | echo 184 | # Set Setting if the node go downwith panel 185 | echo "Setting MysqlPassword u can pass this setting if u use webapi" 186 | read -p "(v2ray_mysqlpassword (Default 1):" v2ray_mysqlpassword 187 | [ -z "${v2ray_mysqlpassword}" ] && v2ray_mysqlpassword=1 188 | echo 189 | echo "---------------------------" 190 | echo "v2ray_mysqlpassword = ${v2ray_mysqlpassword}" 191 | echo "---------------------------" 192 | echo 193 | # Set Setting if the node go downwith panel 194 | echo "Setting MysqlDbname u can pass this setting u chosen use webapi" 195 | read -p "(v2ray_mysqldbname (Default sspanel):" v2ray_mysqldbname 196 | [ -z "${v2ray_mysqldbname}" ] && v2ray_mysqldbname="sspanel" 197 | echo 198 | echo "---------------------------" 199 | echo "v2ray_mysqldbname = ${v2ray_mysqldbname}" 200 | echo "---------------------------" 201 | echo 202 | 203 | 204 | # Set ssrpanel speedtest function 205 | echo "use sspanel speedtest" 206 | read -p "(sspanel speedtest: Default (6) hours every time):" ssrpanel_speedtest 207 | [ -z "${ssrpanel_speedtest}" ] && ssrpanel_speedtest=6 208 | echo 209 | echo "---------------------------" 210 | echo "ssrpanel_speedtest = ${ssrpanel_speedtest}" 211 | echo "---------------------------" 212 | echo 213 | 214 | # Set V2ray backend API Listen port 215 | echo "Setting V2ray Grpc API Listen port" 216 | read -p "(V2ray Grpc API Listen port(Default 2333):" v2ray_api_port 217 | [ -z "${v2ray_api_port}" ] && v2ray_api_port=2333 218 | echo 219 | echo "---------------------------" 220 | echo "V2ray Grpc API Listen port = ${v2ray_api_port}" 221 | echo "---------------------------" 222 | echo 223 | 224 | # Set Setting if the node go downwith panel 225 | echo "Setting if the node go downwith panel" 226 | read -p "(v2ray_downWithPanel (Default 1):" v2ray_downWithPanel 227 | [ -z "${v2ray_downWithPanel}" ] && v2ray_downWithPanel=1 228 | echo 229 | echo "---------------------------" 230 | echo "v2ray_downWithPanel = ${v2ray_downWithPanel}" 231 | echo "---------------------------" 232 | echo 233 | 234 | # Set Setting if the node go downwith panel 235 | 236 | } 237 | 238 | pre_install_caddy(){ 239 | 240 | # Set caddy v2ray domain 241 | echo "caddy v2ray domain" 242 | read -p "(There is no default value please make sure you input the right thing):" v2ray_domain 243 | [ -z "${v2ray_domain}" ] 244 | echo 245 | echo "---------------------------" 246 | echo "v2ray_domain = ${v2ray_domain}" 247 | echo "---------------------------" 248 | echo 249 | 250 | 251 | # Set caddy v2ray path 252 | echo "caddy v2ray path" 253 | read -p "(Default path: /v2ray):" v2ray_path 254 | [ -z "${v2ray_path}" ] && v2ray_path="/v2ray" 255 | echo 256 | echo "---------------------------" 257 | echo "v2ray_path = ${v2ray_path}" 258 | echo "---------------------------" 259 | echo 260 | 261 | # Set caddy v2ray tls email 262 | echo "caddy v2ray tls email" 263 | read -p "(No default ):" v2ray_email 264 | [ -z "${v2ray_email}" ] 265 | echo 266 | echo "---------------------------" 267 | echo "v2ray_email = ${v2ray_email}" 268 | echo "---------------------------" 269 | echo 270 | 271 | # Set Caddy v2ray listen port 272 | echo "caddy v2ray local listen port" 273 | read -p "(Default port: 10550):" v2ray_local_port 274 | [ -z "${v2ray_local_port}" ] && v2ray_local_port=10550 275 | echo 276 | echo "---------------------------" 277 | echo "v2ray_local_port = ${v2ray_local_port}" 278 | echo "---------------------------" 279 | echo 280 | 281 | # Set Caddy listen port 282 | echo "caddy listen port" 283 | read -p "(Default port: 443):" caddy_listen_port 284 | [ -z "${caddy_listen_port}" ] && caddy_listen_port=443 285 | echo 286 | echo "---------------------------" 287 | echo "caddy_listen_port = ${caddy_listen_port}" 288 | echo "---------------------------" 289 | echo 290 | 291 | 292 | } 293 | 294 | # Config docker 295 | config_docker(){ 296 | echo "Press any key to start...or Press Ctrl+C to cancel" 297 | char=`get_char` 298 | cd ${cur_dir} 299 | echo "install curl" 300 | install_dependencies 301 | echo "Writing docker-compose.yml" 302 | curl -L https://raw.githubusercontent.com/githubphone/pay-v2ray-sspanel-v3-mod_Uim-plugin/master/Docker/V2ray/docker-compose.yml > docker-compose.yml 303 | sed -i "s|node_id:.*|node_id: ${ssrpanel_node_id}|" ./docker-compose.yml 304 | sed -i "s|sspanel_url:.*|sspanel_url: '${ssrpanel_url}'|" ./docker-compose.yml 305 | sed -i "s|key:.*|key: '${ssrpanel_key}'|" ./docker-compose.yml 306 | sed -i "s|speedtest:.*|speedtest: ${ssrpanel_speedtest}|" ./docker-compose.yml 307 | sed -i "s|api_port:.*|api_port: ${v2ray_api_port}|" ./docker-compose.yml 308 | sed -i "s|downWithPanel:.*|downWithPanel: ${v2ray_downWithPanel}|" ./docker-compose.yml 309 | sed -i "s|usemysql:.*|usemysql: ${v2ray_usemysql}|" ./docker-compose.yml 310 | sed -i "s|PANELTYPE:.*|PANELTYPE: ${v2ray_paneltype}|" ./docker-compose.yml 311 | sed -i "s|MYSQLHOST:.*|MYSQLHOST: ${v2ray_mysqlhost}|" ./docker-compose.yml 312 | sed -i "s|MYSQLPORT:.*|MYSQLPORT: ${v2ray_mysqlport}|" ./docker-compose.yml 313 | sed -i "s|MYSQLUSR:.*|MYSQLUSR: ${v2ray_myqluser}|" ./docker-compose.yml 314 | sed -i "s|MYSQLPASSWD:.*|MYSQLPASSWD: ${v2ray_mysqlpassword}|" ./docker-compose.yml 315 | sed -i "s|MYSQLDBNAME:.*|MYSQLDBNAME: ${v2ray_mysqldbname}|" ./docker-compose.yml 316 | } 317 | 318 | 319 | # Config caddy_docker 320 | config_caddy_docker(){ 321 | echo "Press any key to start...or Press Ctrl+C to cancel" 322 | char=`get_char` 323 | cd ${cur_dir} 324 | echo "install curl" 325 | install_dependencies 326 | curl -L https://raw.githubusercontent.com/githubphone/pay-v2ray-sspanel-v3-mod_Uim-plugin/master/Docker/Caddy_V2ray/Caddyfile > Caddyfile 327 | echo "Writing docker-compose.yml" 328 | curl -L https://raw.githubusercontent.com/githubphone/pay-v2ray-sspanel-v3-mod_Uim-plugin/master/Docker/Caddy_V2ray/docker-compose.yml > docker-compose.yml 329 | sed -i "s|node_id:.*|node_id: ${ssrpanel_node_id}|" ./docker-compose.yml 330 | sed -i "s|sspanel_url:.*|sspanel_url: '${ssrpanel_url}'|" ./docker-compose.yml 331 | sed -i "s|key:.*|key: '${ssrpanel_key}'|" ./docker-compose.yml 332 | sed -i "s|speedtest:.*|speedtest: ${ssrpanel_speedtest}|" ./docker-compose.yml 333 | sed -i "s|api_port:.*|api_port: ${v2ray_api_port}|" ./docker-compose.yml 334 | sed -i "s|downWithPanel:.*|downWithPanel: ${v2ray_downWithPanel}|" ./docker-compose.yml 335 | sed -i "s|usemysql:.*|usemysql: ${v2ray_usemysql}|" ./docker-compose.yml 336 | sed -i "s|PANELTYPE:.*|PANELTYPE: ${v2ray_paneltype}|" ./docker-compose.yml 337 | sed -i "s|MYSQLHOST:.*|MYSQLHOST: ${v2ray_mysqlhost}|" ./docker-compose.yml 338 | sed -i "s|MYSQLPORT:.*|MYSQLPORT: ${v2ray_mysqlport}|" ./docker-compose.yml 339 | sed -i "s|MYSQLUSR:.*|MYSQLUSR: ${v2ray_myqluser}|" ./docker-compose.yml 340 | sed -i "s|MYSQLPASSWD:.*|MYSQLPASSWD: ${v2ray_mysqlpassword}|" ./docker-compose.yml 341 | sed -i "s|MYSQLDBNAME:.*|MYSQLDBNAME: ${v2ray_mysqldbname}|" ./docker-compose.yml 342 | sed -i "s|V2RAY_DOMAIN=xxxx.com|V2RAY_DOMAIN=${v2ray_domain}|" ./docker-compose.yml 343 | sed -i "s|V2RAY_PATH=/v2ray|V2RAY_PATH=${v2ray_path}|" ./docker-compose.yml 344 | sed -i "s|V2RAY_EMAIL=xxxx@outlook.com|V2RAY_EMAIL=${v2ray_email}|" ./docker-compose.yml 345 | sed -i "s|V2RAY_PORT=10550|V2RAY_PORT=${v2ray_local_port}|" ./docker-compose.yml 346 | sed -i "s|V2RAY_OUTSIDE_PORT=443|V2RAY_OUTSIDE_PORT=${caddy_listen_port}|" ./docker-compose.yml 347 | } 348 | 349 | # Config caddy_docker 350 | config_caddy_docker_cloudflare(){ 351 | 352 | # Set caddy cloudflare ddns email 353 | echo "caddy cloudflare ddns email" 354 | read -p "(No default ):" cloudflare_email 355 | [ -z "${cloudflare_email}" ] 356 | echo 357 | echo "---------------------------" 358 | echo "cloudflare_email = ${cloudflare_email}" 359 | echo "---------------------------" 360 | echo 361 | 362 | # Set caddy cloudflare ddns key 363 | echo "caddy cloudflare ddns key" 364 | read -p "(No default ):" cloudflare_key 365 | [ -z "${cloudflare_email}" ] 366 | echo 367 | echo "---------------------------" 368 | echo "cloudflare_email = ${cloudflare_key}" 369 | echo "---------------------------" 370 | echo 371 | echo 372 | 373 | echo "Press any key to start...or Press Ctrl+C to cancel" 374 | char=`get_char` 375 | cd ${cur_dir} 376 | echo "install curl first " 377 | install_dependencies 378 | echo "Starting Writing Caddy file and docker-compose.yml" 379 | curl -L https://raw.githubusercontent.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/master/Docker/Caddy_V2ray/Caddyfile >Caddyfile 380 | epcho "Writing docker-compose.yml" 381 | curl -L https://raw.githubusercontent.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/master/Docker/Caddy_V2ray/docker-compose.yml >docker-compose.yml 382 | sed -i "s|node_id:.*|node_id: ${ssrpanel_node_id}|" ./docker-compose.yml 383 | sed -i "s|sspanel_url:.*|sspanel_url: '${ssrpanel_url}'|" ./docker-compose.yml 384 | sed -i "s|key:.*|key: '${ssrpanel_key}'|" ./docker-compose.yml 385 | sed -i "s|speedtest:.*|speedtest: ${ssrpanel_speedtest}|" ./docker-compose.yml 386 | sed -i "s|api_port:.*|api_port: ${v2ray_api_port}|" ./docker-compose.yml 387 | sed -i "s|downWithPanel:.*|downWithPanel: ${v2ray_downWithPanel}|" ./docker-compose.yml 388 | sed -i "s|usemysql:.*|usemysql: ${v2ray_usemysql}|" ./docker-compose.yml 389 | sed -i "s|PANELTYPE:.*|PANELTYPE: ${v2ray_paneltype}|" ./docker-compose.yml 390 | sed -i "s|MYSQLHOST:.*|MYSQLHOST: ${v2ray_mysqlhost}|" ./docker-compose.yml 391 | sed -i "s|MYSQLPORT:.*|MYSQLPORT: ${v2ray_mysqlport}|" ./docker-compose.yml 392 | sed -i "s|MYSQLUSR:.*|MYSQLUSR: ${v2ray_myqluser}|" ./docker-compose.yml 393 | sed -i "s|MYSQLPASSWD:.*|MYSQLPASSWD: ${v2ray_mysqlpassword}|" ./docker-compose.yml 394 | sed -i "s|MYSQLDBNAME:.*|MYSQLDBNAME: ${v2ray_mysqldbname}|" ./docker-compose.yml 395 | sed -i "s|V2RAY_DOMAIN=xxxx.com|V2RAY_DOMAIN=${v2ray_domain}|" ./docker-compose.yml 396 | sed -i "s|V2RAY_PATH=/v2ray|V2RAY_PATH=${v2ray_path}|" ./docker-compose.yml 397 | sed -i "s|V2RAY_EMAIL=xxxx@outlook.com|V2RAY_EMAIL=${v2ray_email}|" ./docker-compose.yml 398 | sed -i "s|V2RAY_PORT=10550|V2RAY_PORT=${v2ray_local_port}|" ./docker-compose.yml 399 | sed -i "s|V2RAY_OUTSIDE_PORT=443|V2RAY_OUTSIDE_PORT=${caddy_listen_port}|" ./docker-compose.yml 400 | sed -i "s|# - CLOUDFLARE_EMAIL=xxxxxx@out.look.com| - CLOUDFLARE_EMAIL=${cloudflare_email}|" ./docker-compose.yml 401 | sed -i "s|# - CLOUDFLARE_API_KEY=xxxxxxx| - CLOUDFLARE_API_KEY=${cloudflare_key}|" ./docker-compose.yml 402 | sed -i "s|# dns cloudflare|dns cloudflare|" ./Caddyfile 403 | 404 | } 405 | 406 | # Install docker and docker compose 407 | install_docker(){ 408 | echo -e "Starting installing Docker " 409 | curl -fsSL https://get.docker.com -o get-docker.sh 410 | bash get-docker.sh 411 | echo -e "Starting installing Docker Compose " 412 | curl -L https://github.com/docker/compose/releases/download/1.17.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose 413 | chmod +x /usr/local/bin/docker-compose 414 | curl -L https://raw.githubusercontent.com/docker/compose/1.8.0/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose 415 | clear 416 | echo "Start Docker " 417 | service docker start 418 | echo "Start Docker-Compose " 419 | docker-compose pull 420 | docker-compose up -d 421 | echo 422 | echo -e "Congratulations, V2ray server install completed!" 423 | echo 424 | echo "Enjoy it!" 425 | echo 426 | } 427 | 428 | install_check(){ 429 | if check_sys packageManager yum || check_sys packageManager apt; then 430 | if centosversion 5; then 431 | return 1 432 | fi 433 | return 0 434 | else 435 | return 1 436 | fi 437 | } 438 | 439 | install_select(){ 440 | clear 441 | while true 442 | do 443 | echo "Which v2ray Docker you'd select:" 444 | for ((i=1;i<=${#software[@]};i++ )); do 445 | hint="${software[$i-1]}" 446 | echo -e "${green}${i}${plain}) ${hint}" 447 | done 448 | read -p "Please enter a number (Default ${software[0]}):" selected 449 | [ -z "${selected}" ] && selected="1" 450 | case "${selected}" in 451 | 1|2|3|4) 452 | echo 453 | echo "You choose = ${software[${selected}-1]}" 454 | echo 455 | break 456 | ;; 457 | *) 458 | echo -e "[${red}Error${plain}] Please only enter a number [1-4]" 459 | ;; 460 | esac 461 | done 462 | } 463 | install_dependencies(){ 464 | if check_sys packageManager yum; then 465 | echo -e "[${green}Info${plain}] Checking the EPEL repository..." 466 | if [ ! -f /etc/yum.repos.d/epel.repo ]; then 467 | yum install -y epel-release > /dev/null 2>&1 468 | fi 469 | [ ! -f /etc/yum.repos.d/epel.repo ] && echo -e "[${red}Error${plain}] Install EPEL repository failed, please check it." && exit 1 470 | [ ! "$(command -v yum-config-manager)" ] && yum install -y yum-utils > /dev/null 2>&1 471 | [ x"$(yum-config-manager epel | grep -w enabled | awk '{print $3}')" != x"True" ] && yum-config-manager --enable epel > /dev/null 2>&1 472 | echo -e "[${green}Info${plain}] Checking the EPEL repository complete..." 473 | 474 | yum_depends=( 475 | curl 476 | ) 477 | for depend in ${yum_depends[@]}; do 478 | error_detect_depends "yum -y install ${depend}" 479 | done 480 | elif check_sys packageManager apt; then 481 | apt_depends=( 482 | curl 483 | ) 484 | apt-get -y update 485 | for depend in ${apt_depends[@]}; do 486 | error_detect_depends "apt-get -y install ${depend}" 487 | done 488 | fi 489 | echo -e "[${green}Info${plain}] Setting TimeZone to Shanghai" 490 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 491 | date -s "$(curl -sI g.cn | grep Date | cut -d' ' -f3-6)Z" 492 | 493 | } 494 | #update_image 495 | update_image_v2ray(){ 496 | echo "Shut down the current service" 497 | docker-compose down 498 | echo "Pulling Images" 499 | docker-compose pull 500 | echo "Start Service" 501 | docker-compose up -d 502 | } 503 | 504 | #show last 100 line log 505 | 506 | logs_v2ray(){ 507 | echo "Last 100 line logs" 508 | docker-compose logs --tail 100 509 | } 510 | 511 | # Update config 512 | update_config_v2ray(){ 513 | cd ${cur_dir} 514 | echo "Shut down the current service" 515 | docker-compose down 516 | install_select 517 | case "${selected}" in 518 | 1) 519 | pre_install_docker_compose 520 | pre_install_caddy 521 | config_caddy_docker 522 | ;; 523 | 2) 524 | pre_install_docker_compose 525 | pre_install_caddy 526 | config_caddy_docker_cloudflare 527 | ;; 528 | 3) 529 | pre_install_docker_compose 530 | config_docker 531 | ;; 532 | *) 533 | echo "Wrong number" 534 | ;; 535 | esac 536 | 537 | echo "Start Service" 538 | docker-compose pull 539 | docker-compose up -d 540 | 541 | } 542 | # remove config 543 | # Install v2ray 544 | install_v2ray(){ 545 | install_select 546 | case "${selected}" in 547 | 1) 548 | pre_install_docker_compose 549 | pre_install_caddy 550 | config_caddy_docker 551 | ;; 552 | 2) 553 | pre_install_docker_compose 554 | pre_install_caddy 555 | config_caddy_docker_cloudflare 556 | ;; 557 | 3) 558 | pre_install_docker_compose 559 | config_docker 560 | ;; 561 | *) 562 | echo "Wrong number" 563 | ;; 564 | esac 565 | install_docker 566 | } 567 | 568 | # Initialization step 569 | clear 570 | while true 571 | do 572 | echo "Which operation you'd select:" 573 | for ((i=1;i<=${#operation[@]};i++ )); do 574 | hint="${operation[$i-1]}" 575 | echo -e "${green}${i}${plain}) ${hint}" 576 | done 577 | read -p "Please enter a number (Default ${operation[0]}):" selected 578 | [ -z "${selected}" ] && selected="1" 579 | case "${selected}" in 580 | 1|2|3|4) 581 | echo 582 | echo "You choose = ${operation[${selected}-1]}" 583 | echo 584 | ${operation[${selected}-1]}_v2ray 585 | break 586 | ;; 587 | *) 588 | echo -e "[${red}Error${plain}] Please only enter a number [1-4]" 589 | ;; 590 | esac 591 | done 592 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type UserModel struct { 4 | UserID uint `json:"id"` 5 | Uuid string `json:"uuid"` 6 | Email string `json:"email"` 7 | Passwd string `json:"passwd"` 8 | Method string `json:"method"` 9 | Port uint16 `json:"port"` 10 | NodeSpeedlimit uint `json:"node_speedlimit"` 11 | Obfs string `json:"obfs"` 12 | Protocol string `json:"protocol"` 13 | Rate uint32 14 | AlterId uint32 15 | PrefixedId string 16 | Muhost string 17 | } 18 | 19 | type UserTrafficLog struct { 20 | UserID uint `json:"user_id"` 21 | Uplink uint64 `json:"u"` 22 | Downlink uint64 `json:"d"` 23 | } 24 | 25 | type NodeInfo struct { 26 | NodeID uint 27 | NodeSpeedlimit uint `json:"node_speedlimit"` 28 | Server_raw string `json:"server"` 29 | Sort uint `json:"sort"` 30 | Server map[string]interface{} 31 | TrafficRate float64 32 | } 33 | 34 | type UserOnLineIP struct { 35 | UserId uint `json:"user_id"` 36 | Ip string `json:"ip"` 37 | } 38 | 39 | type DisNodeInfo struct { 40 | Server_raw string `json:"dist_node_server"` 41 | Sort uint `json:"dist_node_sort"` 42 | Port uint16 `json:"port"` 43 | Server map[string]interface{} 44 | UserId uint `json:"user_id"` 45 | } 46 | -------------------------------------------------------------------------------- /panel.go: -------------------------------------------------------------------------------- 1 | package v2ray_sspanel_v3_mod_Uim_plugin 2 | 3 | import ( 4 | "fmt" 5 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/Manager" 6 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/client" 7 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/config" 8 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/db" 9 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/model" 10 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/speedtest" 11 | "github.com/robfig/cron" 12 | "google.golang.org/grpc" 13 | "reflect" 14 | "runtime" 15 | ) 16 | 17 | type Panel struct { 18 | db db.Db 19 | manager *Manager.Manager 20 | speedtestClient speedtest.Client 21 | downwithpanel int 22 | } 23 | 24 | func NewPanel(gRPCConn *grpc.ClientConn, db db.Db, cfg *config.Config) (*Panel, error) { 25 | opts := speedtest.NewOpts() 26 | speedtestClient := speedtest.NewClient(opts) 27 | var newpanel = Panel{ 28 | speedtestClient: speedtestClient, 29 | db: db, 30 | downwithpanel: cfg.DownWithPanel, 31 | manager: &Manager.Manager{ 32 | HandlerServiceClient: client.NewHandlerServiceClient(gRPCConn, "MAIN_INBOUND"), 33 | StatsServiceClient: client.NewStatsServiceClient(gRPCConn), 34 | RuleServiceClient: client.NewRuleServerClient(gRPCConn), 35 | NodeID: cfg.NodeID, 36 | CheckRate: cfg.CheckRate, 37 | SpeedTestCheckRate: cfg.SpeedTestCheckRate, 38 | CurrentNodeInfo: &model.NodeInfo{}, 39 | NextNodeInfo: &model.NodeInfo{}, 40 | Users: map[string]model.UserModel{}, 41 | UserToBeMoved: map[string]model.UserModel{}, 42 | UserToBeAdd: map[string]model.UserModel{}, 43 | Id2PrefixedIdmap: map[uint]string{}, 44 | Id2DisServer: map[uint]string{}, 45 | }, 46 | } 47 | return &newpanel, nil 48 | } 49 | 50 | func (p *Panel) Start() { 51 | doFunc := func() { 52 | if err := p.do(); err != nil { 53 | newError("panel#do").Base(err).AtError().WriteToLog() 54 | } 55 | // Explicitly triggering GC to remove garbage 56 | runtime.GC() 57 | } 58 | doFunc() 59 | 60 | speedTestFunc := func() { 61 | result, err := speedtest.GetSpeedtest(p.speedtestClient) 62 | if err != nil { 63 | newError("panel#speedtest").Base(err).AtError().WriteToLog() 64 | } 65 | newError(result).AtInfo().WriteToLog() 66 | if p.db.UploadSpeedTest(p.manager.NodeID, result) { 67 | newError("succesfully upload speedtest result").AtInfo().WriteToLog() 68 | } else { 69 | newError("failed to upload speedtest result").AtInfo().WriteToLog() 70 | } 71 | // Explicitly triggering GC to remove garbage 72 | runtime.GC() 73 | } 74 | c := cron.New() 75 | err := c.AddFunc(fmt.Sprintf("@every %ds", p.manager.CheckRate), doFunc) 76 | if err != nil { 77 | fatal(err) 78 | } 79 | if p.manager.SpeedTestCheckRate > 0 { 80 | newErrorf("SpeedTest @every %dh", p.manager.SpeedTestCheckRate).AtInfo().WriteToLog() 81 | err = c.AddFunc(fmt.Sprintf("@every %dh", p.manager.SpeedTestCheckRate), speedTestFunc) 82 | if err != nil { 83 | newError("Can't add speed test into cron").AtWarning().WriteToLog() 84 | } 85 | } 86 | c.Start() 87 | c.Run() 88 | } 89 | 90 | func (p *Panel) do() error { 91 | if p.updateManager() { 92 | p.updateThroughout() 93 | } 94 | return nil 95 | } 96 | func (p *Panel) initial() { 97 | newError("initial system").AtWarning().WriteToLog() 98 | p.manager.RemoveInbound() 99 | p.manager.CopyUsers() 100 | p.manager.UpdataUsers() 101 | p.manager.RemoveAllUserOutBound() 102 | p.manager.CurrentNodeInfo = &model.NodeInfo{} 103 | p.manager.NextNodeInfo = &model.NodeInfo{} 104 | p.manager.UserToBeAdd = map[string]model.UserModel{} 105 | p.manager.UserToBeMoved = map[string]model.UserModel{} 106 | p.manager.Users = map[string]model.UserModel{} 107 | p.manager.Id2PrefixedIdmap = map[uint]string{} 108 | p.manager.Id2DisServer = map[uint]string{} 109 | 110 | } 111 | 112 | func (p *Panel) updateManager() bool { 113 | newNodeinfo, err := p.db.GetNodeInfo(p.manager.NodeID) 114 | if err != nil { 115 | newError("Can't get nodeinfo").Base(err).AtWarning().WriteToLog() 116 | if p.downwithpanel == 1 { 117 | p.initial() 118 | } 119 | return false 120 | } 121 | if newNodeinfo.Ret != 1 { 122 | newError("please check your setting nodata finded").AtWarning().WriteToLog() 123 | if p.downwithpanel == 1 { 124 | p.initial() 125 | } 126 | return false 127 | } 128 | newErrorf("old node info %s ", p.manager.NextNodeInfo.Server_raw).AtInfo().WriteToLog() 129 | newErrorf("new node info %s", newNodeinfo.Data.Server_raw).AtInfo().WriteToLog() 130 | if p.manager.NextNodeInfo.Server_raw != newNodeinfo.Data.Server_raw { 131 | p.manager.NextNodeInfo = newNodeinfo.Data 132 | if err = p.manager.UpdateServer(); err != nil { 133 | newError(err).AtWarning().WriteToLog() 134 | } 135 | p.manager.UserChanged = true 136 | } 137 | users, err := p.db.GetALLUsers(p.manager.NextNodeInfo) 138 | if err != nil { 139 | newError(err).AtDebug().WriteToLog() 140 | } 141 | newError("now begin to check users").AtInfo().WriteToLog() 142 | current_user := p.manager.GetUsers() 143 | // remove user by prefixed_id 144 | for key, _ := range current_user { 145 | _, ok := users.Data[key] 146 | if !ok { 147 | p.manager.Remove(key) 148 | newErrorf("need to remove client: %s.", key).AtInfo().WriteToLog() 149 | } 150 | } 151 | // add users 152 | for key, value := range users.Data { 153 | current, ok := current_user[key] 154 | if !ok { 155 | p.manager.Add(value) 156 | newErrorf("need to add user email %s", key).AtInfo().WriteToLog() 157 | } else { 158 | if !reflect.DeepEqual(value, current) { 159 | p.manager.Remove(key) 160 | p.manager.Add(value) 161 | newErrorf("need to add user email %s due to method or password changed", key).AtInfo().WriteToLog() 162 | } 163 | } 164 | 165 | } 166 | 167 | if p.manager.UserChanged { 168 | p.manager.UserChanged = false 169 | newErrorf("Before Update, Current Users %d need to be add %d need to be romved %d", len(p.manager.Users), 170 | len(p.manager.UserToBeAdd), len(p.manager.UserToBeMoved)).AtWarning().WriteToLog() 171 | p.manager.UpdataUsers() 172 | newErrorf("After Update, Current Users %d need to be add %d need to be romved %d", len(p.manager.Users), 173 | len(p.manager.UserToBeAdd), len(p.manager.UserToBeMoved)).AtWarning().WriteToLog() 174 | p.manager.CopyNodeinfo() 175 | } else { 176 | newError("check ports finished. No need to update ").AtInfo().WriteToLog() 177 | } 178 | if newNodeinfo.Data.Sort == 12 { 179 | newError("Start to check relay rules ").AtInfo().WriteToLog() 180 | p.updateOutbounds() 181 | } 182 | return true 183 | } 184 | func (p *Panel) updateOutbounds() { 185 | data, err := p.db.GetDisNodeInfo(p.manager.NodeID) 186 | if err != nil { 187 | newError(err).AtWarning().WriteToLog() 188 | if p.downwithpanel == 1 { 189 | p.initial() 190 | } 191 | return 192 | } 193 | if data.Ret != 1 { 194 | newError(data.Data).AtWarning().WriteToLog() 195 | if p.downwithpanel == 1 { 196 | p.initial() 197 | } 198 | return 199 | } 200 | if len(data.Data) > 0 { 201 | newErrorf("Recieve %d User Rules", len(data.Data)).AtInfo().WriteToLog() 202 | globalSettingindex := -1 203 | for index, value := range data.Data { 204 | if value.UserId == 0 { 205 | globalSettingindex = index 206 | break 207 | } 208 | } 209 | if globalSettingindex != -1 { 210 | nextserver := data.Data[globalSettingindex] 211 | newErrorf("Got A Global Rule %s ", nextserver.Server_raw).AtInfo().WriteToLog() 212 | remove_count := 0 213 | add_count := 0 214 | for _, user := range p.manager.Users { 215 | currentserver, find := p.manager.Id2DisServer[user.UserID] 216 | nextserver.UserId = user.UserID 217 | if find { 218 | if currentserver != nextserver.Server_raw { 219 | p.manager.RemoveOutBound(currentserver+fmt.Sprintf("%d", user.UserID), user.UserID) 220 | err := p.manager.AddOuntBound(nextserver) 221 | if err != nil { 222 | newError("ADDOUTBOUND ").Base(err).AtInfo().WriteToLog() 223 | } else { 224 | 225 | remove_count += 1 226 | add_count += 1 227 | } 228 | } 229 | } else { 230 | err := p.manager.AddOuntBound(nextserver) 231 | if err != nil { 232 | newError("ADDOUTBOUND ").Base(err).AtInfo().WriteToLog() 233 | } else { 234 | add_count += 1 235 | } 236 | } 237 | } 238 | p.manager.Id2DisServer = map[uint]string{} 239 | for _, user := range p.manager.Users { 240 | p.manager.Id2DisServer[user.UserID] = nextserver.Server_raw 241 | } 242 | newErrorf("Add %d and REMOVE %d Rules, Current Rules %d", add_count, remove_count, len(p.manager.Id2DisServer)).AtInfo().WriteToLog() 243 | 244 | } else { 245 | remove_count := 0 246 | add_count := 0 247 | for _, value := range data.Data { 248 | _, find := p.manager.Id2DisServer[value.UserId] 249 | if !find { 250 | err := p.manager.AddOuntBound(value) 251 | if err != nil { 252 | newError("ADDOUTBOUND ").Base(err).AtInfo().WriteToLog() 253 | } else { 254 | add_count += 1 255 | } 256 | } 257 | } 258 | for id, currentserver := range p.manager.Id2DisServer { 259 | flag := false 260 | currenttag := currentserver + fmt.Sprintf("%d", id) 261 | for _, nextserver := range data.Data { 262 | if id == nextserver.UserId && currenttag == nextserver.Server_raw+fmt.Sprintf("%d", nextserver.UserId) { 263 | flag = true 264 | break 265 | } else if id == nextserver.UserId && currenttag != nextserver.Server_raw+fmt.Sprintf("%d", nextserver.UserId) { 266 | p.manager.RemoveOutBound(currenttag, id) 267 | err := p.manager.AddOuntBound(nextserver) 268 | if err != nil { 269 | newError("ADDOUTBOUND ").Base(err).AtInfo().WriteToLog() 270 | } else { 271 | remove_count += 1 272 | add_count += 1 273 | } 274 | flag = true 275 | break 276 | } 277 | if !flag { 278 | p.manager.RemoveOutBound(currenttag, id) 279 | remove_count += 1 280 | } 281 | } 282 | 283 | } 284 | 285 | p.manager.Id2DisServer = map[uint]string{} 286 | for _, nextserver := range data.Data { 287 | p.manager.Id2DisServer[nextserver.UserId] = nextserver.Server_raw 288 | } 289 | newErrorf("Add %d and REMOVE %d Rules, Current Rules %d", add_count, remove_count, len(p.manager.Id2DisServer)).AtInfo().WriteToLog() 290 | } 291 | } else { 292 | newErrorf("There is No User Rules, Need To Remove %d RULEs", len(p.manager.Id2DisServer)).AtInfo().WriteToLog() 293 | if len(p.manager.Id2DisServer) > 0 { 294 | remove_count := 0 295 | add_count := 0 296 | for id, currentserver := range p.manager.Id2DisServer { 297 | currenttag := currentserver + fmt.Sprintf("%d", id) 298 | p.manager.RemoveOutBound(currenttag, id) 299 | remove_count += 1 300 | } 301 | p.manager.Id2DisServer = map[uint]string{} 302 | newErrorf("Add %d and REMOVE %d Rules, Current Rules %d", add_count, remove_count, len(p.manager.Id2DisServer)).AtInfo().WriteToLog() 303 | 304 | } 305 | } 306 | 307 | } 308 | 309 | func (p *Panel) updateThroughout() { 310 | current_user := p.manager.GetUsers() 311 | usertraffic := []model.UserTrafficLog{} 312 | userIPS := []model.UserOnLineIP{} 313 | for _, value := range current_user { 314 | current_upload, err := p.manager.StatsServiceClient.GetUserUplink(value.Email) 315 | current_download, err := p.manager.StatsServiceClient.GetUserDownlink(value.Email) 316 | if err != nil { 317 | newError(err).AtDebug().WriteToLog() 318 | } 319 | if current_upload+current_download > 0 { 320 | 321 | newErrorf("USER %s has use %d", value.Email, current_upload+current_download).AtDebug().WriteToLog() 322 | usertraffic = append(usertraffic, model.UserTrafficLog{ 323 | UserID: value.UserID, 324 | Downlink: current_download, 325 | Uplink: current_upload, 326 | }) 327 | current_user_ips, err := p.manager.StatsServiceClient.GetUserIPs(value.Email) 328 | if current_upload+current_download > 1024 { 329 | if err != nil { 330 | newError(err).AtDebug().WriteToLog() 331 | } 332 | for index := range current_user_ips { 333 | userIPS = append(userIPS, model.UserOnLineIP{ 334 | UserId: value.UserID, 335 | Ip: current_user_ips[index], 336 | }) 337 | } 338 | 339 | } 340 | } 341 | } 342 | if p.db.UpLoadUserTraffics(p.manager.NodeID, usertraffic) { 343 | newErrorf("Successfully upload %d users traffics", len(usertraffic)).AtInfo().WriteToLog() 344 | } else { 345 | newError("update trafic failed").AtDebug().WriteToLog() 346 | } 347 | if p.db.UpLoadOnlineIps(p.manager.NodeID, userIPS) { 348 | newErrorf("Successfully upload %d ips", len(userIPS)).AtInfo().WriteToLog() 349 | } else { 350 | newError("update trafic failed").AtDebug().WriteToLog() 351 | } 352 | if p.db.UploadSystemLoad(p.manager.NodeID) { 353 | newError("Uploaded systemLoad successfully").AtInfo().WriteToLog() 354 | } else { 355 | newError("Failed to uploaded systemLoad ").AtDebug().WriteToLog() 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package v2ray_sspanel_v3_mod_Uim_plugin 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "github.com/imroc/req" 8 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/client" 9 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/config" 10 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/db" 11 | "google.golang.org/grpc/status" 12 | "os" 13 | "runtime" 14 | "strings" 15 | "time" 16 | "v2ray.com/core/common/errors" 17 | ) 18 | 19 | func init1() { 20 | go func() { 21 | err := run() 22 | if err != nil { 23 | fatal(err) 24 | } 25 | }() 26 | } 27 | func CheckAuth(url string, params map[string]interface{}) (*db.AuthResponse, error) { 28 | var response = db.AuthResponse{} 29 | parm := req.Param{} 30 | for k, v := range params { 31 | parm[k] = v 32 | } 33 | r, err := req.Get(url, parm) 34 | if err != nil { 35 | return nil, err 36 | } else { 37 | err = r.ToJSON(&response) 38 | if err != nil { 39 | return &response, err 40 | } else if response.Ret != 1 { 41 | return nil, err 42 | } 43 | } 44 | return &response, nil 45 | } 46 | func checkAuth(panelurl string) (bool, error) { 47 | md5Ctx := md5.New() 48 | md5Ctx.Write([]byte(panelurl)) 49 | cipherStr := md5Ctx.Sum(nil) 50 | current_md5 := hex.EncodeToString(cipherStr) 51 | re, err := CheckAuth("https://auth.rico93.com", map[string]interface{}{"md5": current_md5}) 52 | if re != nil { 53 | if re.Token != "" { 54 | auth := config.AESDecodeStr(re.Token, config.Key) 55 | return current_md5 == auth, nil 56 | } else { 57 | return false, newErrorf("Auth failed, current url: %s current md5 %s", panelurl, current_md5) 58 | } 59 | } else { 60 | return false, newErrorf("Can't get data from server or the data is not as expected current url: %s current md5 %s", panelurl, current_md5).Base(err) 61 | } 62 | 63 | } 64 | func run() error { 65 | 66 | err := config.CommandLine.Parse(os.Args[1:]) 67 | 68 | cfg, err := config.GetConfig() 69 | if err != nil || *config.Test || cfg == nil { 70 | return err 71 | } 72 | 73 | // wait v2ray 74 | time.Sleep(3 * time.Second) 75 | 76 | go func() { 77 | var ok bool 78 | var err error 79 | if cfg.Usemysql == 0 { 80 | if strings.HasPrefix(cfg.PanelUrl, "https://") || strings.HasPrefix(cfg.PanelUrl, "http://") { 81 | ok, err = checkAuth(cfg.PanelUrl) 82 | } else { 83 | ok, err = checkAuth(cfg.PanelUrl) 84 | cfg.PanelUrl = "https://" + cfg.PanelUrl 85 | } 86 | 87 | } else { 88 | if cfg.MySQL != nil { 89 | ok, err = checkAuth(cfg.MySQL.Host) 90 | } else { 91 | fatal("Please Add Mysql setting") 92 | } 93 | } 94 | if ok && err == nil { 95 | apiInbound := config.GetInboundConfigByTag(cfg.V2rayConfig.Api.Tag, cfg.V2rayConfig.InboundConfigs) 96 | gRPCAddr := fmt.Sprintf("%s:%d", apiInbound.ListenOn.String(), apiInbound.PortRange.From) 97 | gRPCConn, err := client.ConnectGRPC(gRPCAddr, 10*time.Second) 98 | if err != nil { 99 | if s, ok := status.FromError(err); ok { 100 | err = errors.New(s.Message()) 101 | } 102 | fatal(fmt.Sprintf("connect to gRPC server \"%s\" err: ", gRPCAddr), err) 103 | } 104 | newErrorf("Connected gRPC server \"%s\" ", gRPCAddr).AtWarning().WriteToLog() 105 | var database db.Db 106 | if cfg.Paneltype == 0 { 107 | newError("Using SSpanel").AtInfo().WriteToLog() 108 | if cfg.Usemysql == 1 { 109 | mysql, err := db.NewMySQLConn(cfg.MySQL) 110 | if err != nil { 111 | fmt.Println(err) 112 | } 113 | database = &db.SSpanel{Db: mysql, MU_SUFFIX: cfg.MU_SUFFIX, MU_REGEX: cfg.MU_REGEX} 114 | newError("Using Mysql Now").AtInfo().WriteToLog() 115 | } else { 116 | database = &db.Webapi{ 117 | WebToken: cfg.PanelKey, 118 | WebBaseURl: cfg.PanelUrl, 119 | MU_SUFFIX: cfg.MU_SUFFIX, 120 | MU_REGEX: cfg.MU_REGEX, 121 | } 122 | newError("Using Webapi Now").AtInfo().WriteToLog() 123 | } 124 | } else { 125 | newError("Using SSRpanel").AtInfo().WriteToLog() 126 | if cfg.Usemysql == 1 { 127 | mysql, err := db.NewMySQLConn(cfg.MySQL) 128 | if err != nil { 129 | fmt.Println(err) 130 | } 131 | database = &db.SSRpanel{Db: mysql, MU_SUFFIX: cfg.MU_SUFFIX, MU_REGEX: cfg.MU_REGEX} 132 | newError("Using Mysql Now").AtInfo().WriteToLog() 133 | } else { 134 | fatal("No databese config for ssrpanel") 135 | } 136 | } 137 | 138 | p, err := NewPanel(gRPCConn, database, cfg) 139 | if err != nil { 140 | fatal("new panel error", err) 141 | } 142 | p.Start() 143 | } else { 144 | if err != nil { 145 | fatal(err) 146 | } 147 | } 148 | 149 | }() 150 | 151 | // Explicitly triggering GC to remove garbage 152 | runtime.GC() 153 | 154 | return nil 155 | } 156 | 157 | func newErrorf(format string, a ...interface{}) *errors.Error { 158 | return newError(fmt.Sprintf(format, a...)) 159 | } 160 | 161 | func newError(values ...interface{}) *errors.Error { 162 | values = append([]interface{}{"SSPanelPlugin: "}, values...) 163 | return errors.New(values...) 164 | } 165 | 166 | func fatal(values ...interface{}) { 167 | newError(values...).AtError().WriteToLog() 168 | // Wait log 169 | time.Sleep(1 * time.Second) 170 | os.Exit(-2) 171 | } 172 | -------------------------------------------------------------------------------- /speedtest/client.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "runtime" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | type Client interface { 16 | Log(format string, a ...interface{}) 17 | Config() (*Config, error) 18 | LoadConfig(ret chan ConfigRef) 19 | NewRequest(method string, url string, body io.Reader) (*http.Request, error) 20 | Get(url string) (resp *Response, err error) 21 | Post(url string, bodyType string, body io.Reader) (resp *Response, err error) 22 | AllServers() (*Servers, error) 23 | LoadAllServers(ret chan ServersRef) 24 | ClosestServers() (*Servers, error) 25 | LoadClosestServers(ret chan ServersRef) 26 | } 27 | 28 | type client struct { 29 | http.Client 30 | opts *Opts 31 | mutex sync.Mutex 32 | config chan ConfigRef 33 | allServers chan ServersRef 34 | closestServers chan ServersRef 35 | } 36 | 37 | type Response http.Response 38 | 39 | func NewClient(opts *Opts) Client { 40 | dialer := &net.Dialer{ 41 | Timeout: opts.Timeout, 42 | KeepAlive: opts.Timeout, 43 | } 44 | 45 | if len(opts.Interface) != 0 { 46 | dialer.LocalAddr = &net.IPAddr{IP: net.ParseIP(opts.Interface)} 47 | if dialer.LocalAddr == nil { 48 | newErrorf("Invalid source IP: %s\n", opts.Interface).AtWarning().WriteToLog() 49 | } 50 | } 51 | 52 | transport := &http.Transport{ 53 | Proxy: http.ProxyFromEnvironment, 54 | Dial: dialer.Dial, 55 | TLSHandshakeTimeout: opts.Timeout, 56 | ExpectContinueTimeout: opts.Timeout, 57 | } 58 | 59 | client := &client{ 60 | Client: http.Client{ 61 | Transport: transport, 62 | Timeout: opts.Timeout, 63 | }, 64 | opts: opts, 65 | } 66 | 67 | return client 68 | } 69 | 70 | func (client *client) NewRequest(method string, url string, body io.Reader) (*http.Request, error) { 71 | if strings.HasPrefix(url, ":") { 72 | if client.opts.Secure { 73 | url = "https" + url 74 | } else { 75 | url = "http" + url 76 | } 77 | } 78 | req, err := http.NewRequest(method, url, body) 79 | if err == nil { 80 | req.Header.Set( 81 | "User-Agent", 82 | "Mozilla/5.0 "+ 83 | fmt.Sprintf("(%s; U; %s; en-us)", runtime.GOOS, runtime.GOARCH)+ 84 | fmt.Sprintf("Go/%s", runtime.Version())+ 85 | fmt.Sprintf("(KHTML, like Gecko) speedtest-cli/%s", Version)) 86 | } 87 | return req, err 88 | } 89 | 90 | func (client *client) Get(url string) (resp *Response, err error) { 91 | req, err := client.NewRequest("GET", url, nil) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | htResp, err := client.Client.Do(req) 97 | 98 | return (*Response)(htResp), err 99 | } 100 | 101 | func (client *client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error) { 102 | req, err := client.NewRequest("POST", url, body) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | req.Header.Set("Content-Type", bodyType) 108 | htResp, err := client.Client.Do(req) 109 | 110 | return (*Response)(htResp), err 111 | } 112 | 113 | func (resp *Response) ReadContent() ([]byte, error) { 114 | content, err := ioutil.ReadAll(resp.Body) 115 | cerr := resp.Body.Close() 116 | if err != nil { 117 | return nil, err 118 | } 119 | if cerr != nil { 120 | return content, cerr 121 | } 122 | return content, nil 123 | } 124 | 125 | func (resp *Response) ReadXML(out interface{}) error { 126 | content, err := resp.ReadContent() 127 | if err != nil { 128 | return err 129 | } 130 | return xml.Unmarshal(content, out) 131 | } 132 | -------------------------------------------------------------------------------- /speedtest/config.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "encoding/xml" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type ClientConfig struct { 10 | Coordinates 11 | IP string `xml:"ip,attr"` 12 | ISP string `xml:"isp,attr"` 13 | ISPRating float32 `xml:"isprating,attr"` 14 | ISPDownloadAverage uint32 `xml:"ispdlavg,attr"` 15 | ISPUploadAverage uint32 `xml:"ispulavg,attr"` 16 | Rating float32 `xml:"rating,attr"` 17 | LoggedIn uint8 `xml:"loggedin,attr"` 18 | } 19 | 20 | type ConfigTime struct { 21 | Upload uint32 22 | Download uint32 23 | } 24 | 25 | type ConfigTimes []ConfigTime 26 | 27 | type Config struct { 28 | Client ClientConfig `xml:"client"` 29 | Times ConfigTimes `xml:"times"` 30 | } 31 | 32 | func (client *client) Log(format string, a ...interface{}) { 33 | if !client.opts.Quiet { 34 | newErrorf(format, a...).AtInfo().WriteToLog() 35 | } 36 | } 37 | 38 | type ConfigRef struct { 39 | Config *Config 40 | Error error 41 | } 42 | 43 | func (client *client) Config() (*Config, error) { 44 | configChan := make(chan ConfigRef) 45 | client.LoadConfig(configChan) 46 | configRef := <-configChan 47 | return configRef.Config, configRef.Error 48 | } 49 | 50 | func (client *client) LoadConfig(ret chan ConfigRef) { 51 | client.mutex.Lock() 52 | defer client.mutex.Unlock() 53 | 54 | if client.config == nil { 55 | client.config = make(chan ConfigRef) 56 | go client.loadConfig() 57 | } 58 | 59 | go func() { 60 | result := <-client.config 61 | ret <- result 62 | client.config <- result 63 | }() 64 | } 65 | 66 | func (client *client) loadConfig() { 67 | client.Log("Retrieving speedtest.net configuration...") 68 | 69 | result := ConfigRef{} 70 | 71 | resp, err := client.Get("://www.speedtest.net/speedtest-config.php") 72 | if err != nil { 73 | result.Error = err 74 | } else { 75 | config := &Config{} 76 | err = resp.ReadXML(config) 77 | if err != nil { 78 | result.Error = err 79 | } else { 80 | result.Config = config 81 | } 82 | } 83 | 84 | client.config <- result 85 | } 86 | 87 | func (times ConfigTimes) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 88 | for _, attr := range start.Attr { 89 | name := attr.Name.Local 90 | if dl := strings.HasPrefix(name, "dl"); dl || strings.HasPrefix(name, "ul") { 91 | num, err := strconv.Atoi(name[2:]) 92 | if err != nil { 93 | return err 94 | } 95 | if num > cap(times) { 96 | newTimes := make([]ConfigTime, num) 97 | copy(newTimes, times) 98 | times = newTimes[0:num] 99 | } 100 | 101 | speed, err := strconv.ParseUint(attr.Value, 10, 32) 102 | 103 | if err != nil { 104 | return err 105 | } 106 | if dl { 107 | times[num-1].Download = uint32(speed) 108 | } else { 109 | times[num-1].Upload = uint32(speed) 110 | } 111 | } 112 | } 113 | 114 | return d.Skip() 115 | } 116 | -------------------------------------------------------------------------------- /speedtest/coordinates.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import "math" 4 | 5 | type Coordinates struct { 6 | Latitude float32 `xml:"lat,attr"` 7 | Longitude float32 `xml:"lon,attr"` 8 | } 9 | 10 | const radius = 6371 // km 11 | 12 | func radians32(degrees float32) float64 { 13 | return radians(float64(degrees)) 14 | } 15 | 16 | func radians(degrees float64) float64 { 17 | return degrees * math.Pi / 180 18 | } 19 | 20 | func (org Coordinates) DistanceTo(dest Coordinates) float64 { 21 | dlat := radians32(dest.Latitude - org.Latitude) 22 | dlon := radians32(dest.Longitude - org.Longitude) 23 | a := (math.Sin(dlat/2)*math.Sin(dlat/2) + 24 | math.Cos(radians32(org.Latitude))* 25 | math.Cos(radians32(dest.Latitude))*math.Sin(dlon/2)* 26 | math.Sin(dlon/2)) 27 | c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) 28 | d := radius * c 29 | 30 | return d 31 | } 32 | -------------------------------------------------------------------------------- /speedtest/download.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | ) 8 | 9 | const downloadStreamLimit = 6 10 | const maxDownloadDuration = 10 * time.Second 11 | const downloadBufferSize = 4096 12 | const downloadRepeats = 5 13 | 14 | var downloadImageSizes = []int{350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000} 15 | 16 | func (client *client) downloadFile(url string, start time.Time, ret chan int) { 17 | totalRead := 0 18 | defer func() { 19 | ret <- totalRead 20 | }() 21 | 22 | if time.Since(start) > maxDownloadDuration { 23 | if !client.opts.Quiet { 24 | newErrorf("[%s] Download timeout", url).AtWarning().WriteToLog() 25 | } 26 | 27 | return 28 | } 29 | resp, err := client.Get(url) 30 | if err != nil { 31 | newErrorf("[%s] Download failed: %v", url, err).AtWarning().WriteToLog() 32 | return 33 | } 34 | 35 | defer resp.Body.Close() 36 | 37 | buf := make([]byte, downloadBufferSize) 38 | for time.Since(start) <= maxDownloadDuration { 39 | read, err := resp.Body.Read(buf) 40 | totalRead += read 41 | if err != nil { 42 | if err != io.EOF { 43 | newErrorf("[%s] Download error: %v\n", url, err).AtWarning().WriteToLog() 44 | return 45 | } 46 | break 47 | } 48 | } 49 | } 50 | 51 | func (server *Server) DownloadSpeed() int { 52 | client := server.client.(*client) 53 | 54 | starterChan := make(chan int, downloadStreamLimit) 55 | downloads := downloadRepeats * len(downloadImageSizes) 56 | resultChan := make(chan int, downloadStreamLimit) 57 | start := time.Now() 58 | 59 | go func() { 60 | for _, size := range downloadImageSizes { 61 | for i := 0; i < downloadRepeats; i++ { 62 | url := server.RelativeURL(fmt.Sprintf("random%dx%d.jpg", size, size)) 63 | starterChan <- 1 64 | go func() { 65 | client.downloadFile(url, start, resultChan) 66 | <-starterChan 67 | }() 68 | } 69 | } 70 | close(starterChan) 71 | }() 72 | 73 | var totalSize int64 = 0 74 | 75 | for i := 0; i < downloads; i++ { 76 | totalSize += int64(<-resultChan) 77 | } 78 | 79 | duration := time.Since(start) 80 | 81 | return int(totalSize * int64(time.Second) / int64(duration)) 82 | } 83 | -------------------------------------------------------------------------------- /speedtest/latency.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | const DefaultLatencyMeasureTimes = 4 10 | const DefaultErrorLatency = time.Hour 11 | 12 | // Measures latencies for each server. 13 | // Returns server list sorted by latencies. 14 | // This is synchronous operation, because multiple simultaneous requests may affect results. 15 | func (servers *Servers) MeasureLatencies(times uint, errorLatency time.Duration) *Servers { 16 | first := true 17 | for _, server := range servers.List { 18 | if first { 19 | first = false 20 | server.client.Log("Measuring server latencies...") 21 | } 22 | server.doMeasureLatency(times, errorLatency) 23 | } 24 | 25 | latencies := &serverLatencies{List: make([]*Server, servers.Len())} 26 | copy(latencies.List, servers.List) 27 | sort.Sort(latencies) 28 | 29 | return (*Servers)(latencies) 30 | } 31 | 32 | type serverLatencies Servers 33 | 34 | func (servers *serverLatencies) Len() int { 35 | return len(servers.List) 36 | } 37 | 38 | func (servers *serverLatencies) Less(i, j int) bool { 39 | return servers.List[i].Latency < servers.List[j].Latency 40 | } 41 | 42 | func (servers *serverLatencies) Swap(i, j int) { 43 | temp := servers.List[i] 44 | servers.List[i] = servers.List[j] 45 | servers.List[j] = temp 46 | } 47 | 48 | func (server *Server) MeasureLatency(times uint, errorLatency time.Duration) time.Duration { 49 | server.client.Log("Measuring server latency...\n") 50 | return server.doMeasureLatency(times, errorLatency) 51 | } 52 | 53 | func (server *Server) doMeasureLatency(times uint, errorLatency time.Duration) time.Duration { 54 | 55 | var results time.Duration = 0 56 | var i uint 57 | 58 | for i = 0; i < times; i++ { 59 | results += server.measureLatency(errorLatency) 60 | } 61 | 62 | server.Latency = time.Duration(results / time.Duration(times)) 63 | 64 | return server.Latency 65 | } 66 | 67 | func (server *Server) measureLatency(errorLatency time.Duration) time.Duration { 68 | url := server.RelativeURL("latency.txt") 69 | start := time.Now() 70 | resp, err := server.client.Get(url) 71 | duration := time.Since(start) 72 | if resp != nil { 73 | url = resp.Request.URL.String() 74 | } 75 | if err != nil { 76 | server.client.Log("[%s] Failed to detect latency: %v\n", url, err) 77 | return errorLatency 78 | } 79 | if resp.StatusCode != 200 { 80 | server.client.Log("[%s] Invalid latency detection HTTP status: %d\n", url, resp.StatusCode) 81 | duration = errorLatency 82 | } 83 | content, err := resp.ReadContent() 84 | if err != nil { 85 | server.client.Log("[%s] Failed to read latency response: %v\n", url, err) 86 | duration = errorLatency 87 | } 88 | if !strings.HasPrefix(string(content), "test=test") { 89 | server.client.Log("[%s] Invalid latency response: %s\n", url, content) 90 | duration = errorLatency 91 | } 92 | return duration 93 | } 94 | -------------------------------------------------------------------------------- /speedtest/latency_test.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func Test_measureLatency(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | client Client 15 | input time.Duration 16 | want time.Duration 17 | }{ 18 | { 19 | name: "Client.Get() error with DefaultErrorLatency input", 20 | client: &latencyErrorClient{}, 21 | input: DefaultErrorLatency, 22 | want: DefaultErrorLatency, 23 | }, 24 | { 25 | name: "Client.Get() error with 10 Second input", 26 | client: &latencyErrorClient{}, 27 | input: 10 * time.Second, 28 | want: 10 * time.Second, 29 | }, 30 | } 31 | 32 | for _, tc := range tests { 33 | tc := tc 34 | t.Run(tc.name, func(t *testing.T) { 35 | s := &Server{client: tc.client} 36 | if got, want := s.measureLatency(tc.input), tc.want; got != want { 37 | t.Fatalf("unexpected result:\n- want: %v\n- got: %v", 38 | want, got) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | // latencyErrorClient is a client returns error from most of the methods. 45 | type latencyErrorClient struct{} 46 | 47 | func (c *latencyErrorClient) Log(_ string, _ ...interface{}) {} 48 | func (c *latencyErrorClient) Config() (*Config, error) { 49 | return nil, errors.New("Config()") 50 | } 51 | func (c *latencyErrorClient) LoadConfig(_ chan ConfigRef) {} 52 | func (c *latencyErrorClient) NewRequest(_ string, _ string, _ io.Reader) (*http.Request, error) { 53 | return nil, errors.New("NewRequest()") 54 | } 55 | func (c *latencyErrorClient) Get(_ string) (resp *Response, err error) { 56 | return nil, errors.New("Get()") 57 | } 58 | func (c *latencyErrorClient) Post(_ string, _ string, _ io.Reader) (*Response, error) { 59 | return nil, errors.New("Post()") 60 | } 61 | func (c *latencyErrorClient) AllServers() (*Servers, error) { 62 | return nil, errors.New("AllServers()") 63 | } 64 | func (c *latencyErrorClient) LoadAllServers(_ chan ServersRef) {} 65 | func (c *latencyErrorClient) ClosestServers() (*Servers, error) { 66 | return nil, errors.New("ClosestServers()") 67 | } 68 | func (c *latencyErrorClient) LoadClosestServers(_ chan ServersRef) {} 69 | -------------------------------------------------------------------------------- /speedtest/opts.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Opts struct { 8 | SpeedInBytes bool 9 | Quiet bool 10 | List bool 11 | Server ServerID 12 | Interface string 13 | Timeout time.Duration 14 | Secure bool 15 | Help bool 16 | Version bool 17 | } 18 | 19 | func NewOpts() *Opts { 20 | 21 | return &Opts{ 22 | Quiet: true, 23 | Server: 0, 24 | Interface: "", 25 | Timeout: 10 * time.Second, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /speedtest/server.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/url" 8 | "sort" 9 | "time" 10 | ) 11 | 12 | type ServerID uint64 13 | 14 | type Server struct { 15 | Coordinates 16 | URL string `xml:"url,attr"` 17 | Name string `xml:"name,attr"` 18 | Country string `xml:"country,attr"` 19 | CC string `xml:"cc,attr"` 20 | Sponsor string `xml:"sponsor,attr"` 21 | ID ServerID `xml:"id,attr"` 22 | URL2 string `xml:"url2,attr"` 23 | Host string `xml:"host,attr"` 24 | client Client `xml:"-"` 25 | Distance float64 `xml:"-"` 26 | Latency time.Duration `xml:"-"` 27 | } 28 | 29 | func (s *Server) String() string { 30 | return fmt.Sprintf("%8d: %s (%s, %s) [%.2f km] %s", s.ID, s.Sponsor, s.Name, s.Country, s.Distance, s.URL) 31 | } 32 | 33 | func (s *Server) RelativeURL(local string) string { 34 | u, err := url.Parse(s.URL) 35 | if err != nil { 36 | log.Fatalf("[%s] Failed to parse server URL: %v\n", s.URL, err) 37 | return "" 38 | } 39 | localURL, err := url.Parse(local) 40 | if err != nil { 41 | log.Fatalf("Failed to parse local URL `%s`: %v\n", local, err) 42 | } 43 | return u.ResolveReference(localURL).String() 44 | } 45 | 46 | type Servers struct { 47 | List []*Server `xml:"servers>server"` 48 | } 49 | 50 | type ServersRef struct { 51 | Servers *Servers 52 | Error error 53 | } 54 | 55 | func (servers *Servers) First() *Server { 56 | if len(servers.List) == 0 { 57 | return nil 58 | } 59 | return servers.List[0] 60 | } 61 | 62 | func (servers *Servers) Find(id ServerID) *Server { 63 | for _, server := range servers.List { 64 | if server.ID == id { 65 | return server 66 | } 67 | } 68 | return nil 69 | } 70 | 71 | func (servers *Servers) Len() int { 72 | return len(servers.List) 73 | } 74 | 75 | func (servers *Servers) Less(i, j int) bool { 76 | server1 := servers.List[i] 77 | server2 := servers.List[j] 78 | if server1.ID == server2.ID { 79 | return false 80 | } 81 | if server1.Distance < server2.Distance { 82 | return true 83 | } 84 | if server1.Distance > server2.Distance { 85 | return false 86 | } 87 | return server1.ID < server2.ID 88 | } 89 | 90 | func (servers *Servers) Swap(i, j int) { 91 | temp := servers.List[i] 92 | servers.List[i] = servers.List[j] 93 | servers.List[j] = temp 94 | } 95 | 96 | func (servers *Servers) truncate(max int) *Servers { 97 | size := servers.Len() 98 | if size <= max { 99 | return servers 100 | } 101 | return &Servers{servers.List[:max]} 102 | } 103 | 104 | func (servers *Servers) String() string { 105 | out := "" 106 | for _, server := range servers.List { 107 | out += server.String() + "\n" 108 | } 109 | return out 110 | } 111 | 112 | func (servers *Servers) append(other *Servers) *Servers { 113 | if servers == nil { 114 | return other 115 | } 116 | servers.List = append(servers.List, other.List...) 117 | return servers 118 | } 119 | 120 | func (servers *Servers) sort(client Client, config *Config) { 121 | for _, server := range servers.List { 122 | server.client = client 123 | server.Distance = server.DistanceTo(config.Client.Coordinates) 124 | } 125 | sort.Sort(servers) 126 | } 127 | 128 | func (servers *Servers) deduplicate() { 129 | dedup := make([]*Server, 0, len(servers.List)) 130 | var prevId ServerID = 0 131 | for _, server := range servers.List { 132 | if prevId != server.ID { 133 | prevId = server.ID 134 | dedup = append(dedup, server) 135 | } 136 | } 137 | servers.List = dedup 138 | } 139 | 140 | var serverURLs = [...]string{ 141 | "://www.speedtest.net/speedtest-servers-static.php", 142 | "://c.speedtest.net/speedtest-servers-static.php", 143 | "://www.speedtest.net/speedtest-servers.php", 144 | "://c.speedtest.net/speedtest-servers.php", 145 | } 146 | 147 | var NoServersError error = errors.New("No servers available") 148 | 149 | func (client *client) AllServers() (*Servers, error) { 150 | serversChan := make(chan ServersRef) 151 | client.LoadAllServers(serversChan) 152 | serversRef := <-serversChan 153 | return serversRef.Servers, serversRef.Error 154 | } 155 | 156 | func (client *client) LoadAllServers(ret chan ServersRef) { 157 | client.mutex.Lock() 158 | defer client.mutex.Unlock() 159 | 160 | if client.allServers == nil { 161 | client.allServers = make(chan ServersRef) 162 | go client.loadServers() 163 | } 164 | 165 | go func() { 166 | result := <-client.allServers 167 | ret <- result 168 | client.allServers <- result // Make it available again 169 | }() 170 | } 171 | 172 | func (client *client) loadServers() { 173 | configChan := make(chan ConfigRef) 174 | client.LoadConfig(configChan) 175 | 176 | client.Log("Retrieving speedtest.net server list...") 177 | 178 | serversChan := make(chan *Servers, len(serverURLs)) 179 | for _, url := range serverURLs { 180 | go client.loadServersFrom(url, serversChan) 181 | } 182 | 183 | var servers *Servers 184 | 185 | for range serverURLs { 186 | servers = servers.append(<-serversChan) 187 | } 188 | 189 | result := ServersRef{} 190 | 191 | if servers.Len() == 0 { 192 | result.Error = NoServersError 193 | } else { 194 | configRef := <-configChan 195 | if configRef.Error != nil { 196 | result.Error = configRef.Error 197 | } else { 198 | servers.sort(client, configRef.Config) 199 | servers.deduplicate() 200 | result.Servers = servers 201 | } 202 | } 203 | 204 | client.allServers <- result 205 | } 206 | 207 | func (client *client) loadServersFrom(url string, ret chan *Servers) { 208 | resp, err := client.Get(url) 209 | if resp != nil { 210 | url = resp.Request.URL.String() 211 | } 212 | if err != nil { 213 | client.Log("[%s] Failed to retrieve server list: %v", url, err) 214 | } 215 | 216 | servers := &Servers{} 217 | if err = resp.ReadXML(servers); err != nil { 218 | client.Log("[%s] Failed to read server list: %v", url, err) 219 | } 220 | ret <- servers 221 | } 222 | 223 | func (client *client) ClosestServers() (*Servers, error) { 224 | serversChan := make(chan ServersRef) 225 | client.LoadClosestServers(serversChan) 226 | serversRef := <-serversChan 227 | return serversRef.Servers, serversRef.Error 228 | } 229 | 230 | func (client *client) LoadClosestServers(ret chan ServersRef) { 231 | client.mutex.Lock() 232 | defer client.mutex.Unlock() 233 | 234 | if client.closestServers == nil { 235 | client.closestServers = make(chan ServersRef) 236 | go client.loadClosestServers() 237 | } 238 | 239 | go func() { 240 | result := <-client.closestServers 241 | ret <- result 242 | client.closestServers <- result // Make it available again 243 | }() 244 | } 245 | 246 | func (client *client) loadClosestServers() { 247 | serversChan := make(chan ServersRef) 248 | client.LoadAllServers(serversChan) 249 | serversRef := <-serversChan 250 | if serversRef.Error != nil { 251 | client.closestServers <- serversRef 252 | } else { 253 | client.closestServers <- ServersRef{serversRef.Servers.truncate(5), nil} 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /speedtest/speedtest_thread.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | // the go speedtest-cli code is from https://github.com/surol/speedtest-cli 4 | import ( 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | "v2ray.com/core/common/errors" 10 | ) 11 | 12 | func newErrorf(format string, a ...interface{}) *errors.Error { 13 | return newError(fmt.Sprintf(format, a...)) 14 | } 15 | 16 | func newError(values ...interface{}) *errors.Error { 17 | values = append([]interface{}{"SSPanelPlugin: "}, values...) 18 | return errors.New(values...) 19 | } 20 | 21 | func fatal(values ...interface{}) { 22 | newError(values...).AtError().WriteToLog() 23 | // Wait log 24 | time.Sleep(1 * time.Second) 25 | os.Exit(-2) 26 | } 27 | 28 | type Speedresult struct { 29 | CTPing string `json:"telecomping"` 30 | CTUpSpeed string `json:"telecomeupload"` 31 | CTDLSpeed string `json:"telecomedownload"` 32 | CUPing string `json:"unicomping"` 33 | CUUpSpeed string `json:"unicomupload"` 34 | CUDLSpeed string `json:"unicomdownload"` 35 | CMPing string `json:"cmccping"` 36 | CMUpSpeed string `json:"cmccupload"` 37 | CMDLSpeed string `json:"cmccdownload"` 38 | } 39 | 40 | func GetSpeedtest(client Client) ([]Speedresult, error) { 41 | config, err := client.Config() 42 | if err != nil { 43 | return nil, newError(err) 44 | } 45 | newErrorf("Testing from %s (%s)...\n", config.Client.ISP, config.Client.IP).AtInfo().WriteToLog() 46 | final_result := []Speedresult{} 47 | result := Speedresult{ 48 | CTPing: "0.000 ms", 49 | CUPing: "0.000 ms", 50 | CMPing: "0.000 ms", 51 | CTDLSpeed: "0.00 Mib/s", 52 | CTUpSpeed: "0.00 Mib/s", 53 | CUDLSpeed: "0.00 Mib/s", 54 | CUUpSpeed: "0.00 Mib/s", 55 | CMDLSpeed: "0.00 Mib/s", 56 | CMUpSpeed: "0.00 Mib/s"} 57 | server := selectServer("Telecom", client) 58 | if server != nil { 59 | result.CTPing = fmt.Sprintf("%.3f ms", server.Latency.Seconds()*1e3) 60 | result.CTDLSpeed = fmt.Sprintf("%.2f Mib/s", float64(server.DownloadSpeed()/(1<<17))) 61 | result.CTUpSpeed = fmt.Sprintf("%.2f Mib/s", float64(server.UploadSpeed()/(1<<17))) 62 | } 63 | server = selectServer("Mobile", client) 64 | if server != nil { 65 | result.CMPing = fmt.Sprintf("%.3f ms", server.Latency.Seconds()*1e3) 66 | result.CMDLSpeed = fmt.Sprintf("%.2f Mib/s", float64(server.DownloadSpeed()/(1<<17))) 67 | result.CMUpSpeed = fmt.Sprintf("%.2f Mib/s", float64(server.UploadSpeed()/(1<<17))) 68 | } 69 | 70 | server = selectServer("Unicom", client) 71 | if server != nil { 72 | result.CUPing = fmt.Sprintf("%.3f ms", server.Latency.Seconds()*1e3) 73 | result.CUDLSpeed = fmt.Sprintf("%.2f Mib/s", float64(server.DownloadSpeed()/(1<<17))) 74 | result.CUUpSpeed = fmt.Sprintf("%.2f Mib/s", float64(server.UploadSpeed()/(1<<17))) 75 | } 76 | return append(final_result, result), nil 77 | } 78 | 79 | func selectServer(sponsor string, client Client) (selected *Server) { 80 | servers, err := client.AllServers() 81 | if err != nil { 82 | newError("Failed to load server list: %v", err).AtWarning().WriteToLog() 83 | return nil 84 | } 85 | sponsor_servers := new(Servers) 86 | for _, server := range servers.List { 87 | if (server.Country == "China" || server.Country == "CN") && strings.Contains(server.Sponsor, sponsor) { 88 | sponsor_servers.List = append(sponsor_servers.List, server) 89 | } 90 | } 91 | if len(sponsor_servers.List) > 0 { 92 | 93 | selected = sponsor_servers.MeasureLatencies( 94 | DefaultLatencyMeasureTimes, 95 | DefaultErrorLatency).First() 96 | return selected 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /speedtest/upload.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | const maxUploadDuration = maxDownloadDuration 11 | const uploadStreamLimit = downloadStreamLimit 12 | const uploadRepeats = downloadRepeats 13 | 14 | var uploadSizes []int 15 | 16 | func init() { 17 | 18 | var uploadSizeSizes = []int{int(1000 * 1000 / 4), int(1000 * 1000 / 2)} 19 | 20 | uploadSizes = make([]int, len(uploadSizeSizes)*25) 21 | for _, size := range uploadSizeSizes { 22 | for i := 0; i < 25; i++ { 23 | uploadSizes[i] = size 24 | } 25 | } 26 | } 27 | 28 | const safeChars = "0123456789abcdefghijklmnopqrstuv" 29 | 30 | type safeReader struct { 31 | in io.Reader 32 | } 33 | 34 | func (r safeReader) Read(p []byte) (n int, err error) { 35 | n, err = r.in.Read(p) 36 | 37 | for i := 0; i < n; i++ { 38 | p[i] = safeChars[p[i]&31] 39 | } 40 | 41 | return n, err 42 | } 43 | 44 | func (client *client) uploadFile(url string, start time.Time, size int, ret chan int) { 45 | totalWrote := 0 46 | defer func() { 47 | ret <- totalWrote 48 | }() 49 | 50 | if time.Since(start) > maxUploadDuration { 51 | return 52 | } 53 | 54 | resp, err := client.Post( 55 | url, 56 | "application/x-www-form-urlencoded", 57 | io.MultiReader( 58 | strings.NewReader("content1="), 59 | io.LimitReader(&safeReader{rand.Reader}, int64(size-9)))) 60 | if err != nil { 61 | if !client.opts.Quiet { 62 | newErrorf("[%s] Upload failed: %v\n", url, err).AtWarning().WriteToLog() 63 | } 64 | return 65 | } 66 | 67 | totalWrote = size 68 | 69 | defer resp.Body.Close() 70 | } 71 | 72 | func (server *Server) UploadSpeed() int { 73 | client := server.client.(*client) 74 | 75 | starterChan := make(chan int, uploadStreamLimit) 76 | uploads := uploadRepeats * len(uploadSizes) 77 | resultChan := make(chan int, uploadStreamLimit) 78 | start := time.Now() 79 | 80 | go func() { 81 | for _, size := range uploadSizes { 82 | size := size // local copy to avoid the data race. 83 | for i := 0; i < uploadRepeats; i++ { 84 | url := server.URL 85 | starterChan <- 1 86 | go func() { 87 | client.uploadFile(url, start, size, resultChan) 88 | <-starterChan 89 | }() 90 | } 91 | } 92 | close(starterChan) 93 | }() 94 | 95 | var totalSize int64 = 0 96 | 97 | for i := 0; i < uploads; i++ { 98 | totalSize += int64(<-resultChan) 99 | } 100 | 101 | duration := time.Since(start) 102 | 103 | return int(totalSize * int64(time.Second) / int64(duration)) 104 | } 105 | -------------------------------------------------------------------------------- /speedtest/upload_test.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestUpload(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | opts Opts 12 | }{ 13 | { 14 | name: "default options", 15 | opts: Opts{}, 16 | }, 17 | { 18 | name: "quiet option", 19 | opts: Opts{Quiet: true}, 20 | }, 21 | } 22 | 23 | for _, tc := range tests { 24 | tc := tc 25 | t.Run(tc.name, func(t *testing.T) { 26 | // set timeout to avoid the longer tests. 27 | tc.opts.Timeout = 10 * time.Second 28 | c := NewClient(&tc.opts) 29 | if _, err := c.Config(); err != nil { 30 | t.Fatalf("unexpected config error: %v", err) 31 | } 32 | s, err := c.ClosestServers() 33 | if err != nil { 34 | t.Fatalf("unexpected server selection error: %v", err) 35 | } 36 | // pick the firstest server to test. 37 | upload := s.MeasureLatencies( 38 | DefaultLatencyMeasureTimes, 39 | DefaultErrorLatency, 40 | ).First().UploadSpeed() 41 | t.Logf("upload %d bps", upload) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /speedtest/version.go: -------------------------------------------------------------------------------- 1 | package speedtest 2 | 3 | const Version = "1.0.0" 4 | -------------------------------------------------------------------------------- /utility/utility.go: -------------------------------------------------------------------------------- 1 | package utility 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "github.com/shirou/gopsutil/host" 7 | "github.com/shirou/gopsutil/load" 8 | "math/rand" 9 | "time" 10 | ) 11 | 12 | func InStr(s string, list []string) bool { 13 | for _, v := range list { 14 | if v == s { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func GetSystemLoad() string { 22 | stat, err := load.Avg() 23 | if err != nil { 24 | return "0.00 0.00 0.00" 25 | } 26 | 27 | return fmt.Sprintf("%.2f %.2f %.2f", stat.Load1, stat.Load5, stat.Load15) 28 | } 29 | func GetSystemUptime() string { 30 | time, err := host.Uptime() 31 | if err != nil { 32 | return "" 33 | } 34 | return fmt.Sprint(time) 35 | 36 | } 37 | func MD5(text string) []byte { 38 | ctx := md5.New() 39 | ctx.Write([]byte(text)) 40 | return ctx.Sum(nil) 41 | } 42 | 43 | func GetRandomString(len1 int) string { 44 | str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 45 | bytes := []byte(str) 46 | result := []byte{} 47 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 48 | for i := 0; i < len1; i++ { 49 | result = append(result, bytes[r.Intn(len(bytes))]) 50 | } 51 | return string(result) 52 | } 53 | -------------------------------------------------------------------------------- /webapi/webapi.go: -------------------------------------------------------------------------------- 1 | package webapi 2 | 3 | import ( 4 | "fmt" 5 | "github.com/imroc/req" 6 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/model" 7 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/speedtest" 8 | "github.com/githubphone/v2ray-sspanel-v3-mod_Uim-plugin/utility" 9 | "log" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type NodeinfoResponse struct { 16 | Ret uint `json:"ret"` 17 | Data *model.NodeInfo `json:"data"` 18 | } 19 | type PostResponse struct { 20 | Ret uint `json:"ret"` 21 | Data string `json:"data"` 22 | } 23 | type UsersResponse struct { 24 | Ret uint `json:"ret"` 25 | Data []model.UserModel `json:"data"` 26 | } 27 | type AllUsers struct { 28 | Ret uint 29 | Data map[string]model.UserModel 30 | } 31 | type Webapi struct { 32 | WebToken string 33 | WebBaseURl string 34 | } 35 | 36 | type DisNodenfoResponse struct { 37 | Ret uint `json:"ret"` 38 | Data []*model.DisNodeInfo `json:"data"` 39 | } 40 | 41 | var id2string = map[uint]string{ 42 | 0: "server_address", 43 | 1: "port", 44 | 2: "alterid", 45 | 3: "protocol", 46 | 4: "protocol_param", 47 | 5: "path", 48 | 6: "host", 49 | 7: "inside_port", 50 | 8: "server", 51 | } 52 | var maps = map[string]interface{}{ 53 | "server_address": "", 54 | "port": "", 55 | "alterid": "16", 56 | "protocol": "tcp", 57 | "protocol_param": "", 58 | "path": "", 59 | "host": "", 60 | "inside_port": "", 61 | "server": "", 62 | } 63 | 64 | func (api *Webapi) GetApi(url string, params map[string]interface{}) (*req.Resp, error) { 65 | req.SetTimeout(50 * time.Second) 66 | parm := req.Param{ 67 | "key": api.WebToken, 68 | } 69 | for k, v := range params { 70 | parm[k] = v 71 | } 72 | 73 | r, err := req.Get(fmt.Sprintf("%s/mod_mu/%s", api.WebBaseURl, url), parm) 74 | return r, err 75 | } 76 | 77 | func (api *Webapi) GetNodeInfo(nodeid uint) (*NodeinfoResponse, error) { 78 | var response = NodeinfoResponse{} 79 | var params map[string]interface{} 80 | 81 | r, err := api.GetApi(fmt.Sprintf("nodes/%d/info", nodeid), params) 82 | if err != nil { 83 | return &response, err 84 | } else { 85 | err = r.ToJSON(&response) 86 | if err != nil { 87 | return &response, err 88 | } else if response.Ret != 1 { 89 | return &response, err 90 | } 91 | } 92 | 93 | if response.Data.Server_raw != "" { 94 | response.Data.Server_raw = strings.ToLower(response.Data.Server_raw) 95 | data := strings.Split(response.Data.Server_raw, ";") 96 | var count uint 97 | count = 0 98 | for v := range data { 99 | if len(data[v]) > 1 { 100 | maps[id2string[count]] = data[v] 101 | } 102 | count += 1 103 | } 104 | var extraArgues []string 105 | if len(data) == 6 { 106 | extraArgues = append(extraArgues, strings.Split(data[5], "|")...) 107 | for item := range extraArgues { 108 | data = strings.Split(extraArgues[item], "=") 109 | if len(data) > 1 { 110 | if len(data[1]) > 1 { 111 | maps[data[0]] = data[1] 112 | } 113 | 114 | } 115 | } 116 | } 117 | 118 | if maps["protocol"] == "tls" { 119 | temp := maps["protocol_param"] 120 | maps["protocol"] = temp 121 | maps["protocol_param"] = "tls" 122 | } 123 | response.Data.Server = maps 124 | } 125 | response.Data.NodeID = nodeid 126 | return &response, nil 127 | } 128 | 129 | func (api *Webapi) GetDisNodeInfo(nodeid uint) (*DisNodenfoResponse, error) { 130 | var response = DisNodenfoResponse{} 131 | var params map[string]interface{} 132 | params = map[string]interface{}{ 133 | "node_id": nodeid, 134 | } 135 | r, err := api.GetApi("func/relay_rules", params) 136 | if err != nil { 137 | return &response, err 138 | } else { 139 | err = r.ToJSON(&response) 140 | if err != nil { 141 | return &response, err 142 | } else if response.Ret != 1 { 143 | return &response, err 144 | } 145 | } 146 | 147 | if len(response.Data) > 0 { 148 | for _, relayrule := range response.Data { 149 | relayrule.Server_raw = strings.ToLower(relayrule.Server_raw) 150 | data := strings.Split(relayrule.Server_raw, ";") 151 | var count uint 152 | count = 0 153 | for v := range data { 154 | if len(data[v]) > 1 { 155 | maps[id2string[count]] = data[v] 156 | } 157 | count += 1 158 | } 159 | var extraArgues []string 160 | if len(data) == 6 { 161 | extraArgues = append(extraArgues, strings.Split(data[5], "|")...) 162 | for item := range extraArgues { 163 | data = strings.Split(extraArgues[item], "=") 164 | if len(data) > 1 { 165 | if len(data[1]) > 1 { 166 | maps[data[0]] = data[1] 167 | } 168 | 169 | } 170 | } 171 | } 172 | 173 | if maps["protocol"] == "tls" { 174 | temp := maps["protocol_param"] 175 | maps["protocol"] = temp 176 | maps["protocol_param"] = "tls" 177 | } 178 | relayrule.Server = maps 179 | } 180 | } 181 | return &response, nil 182 | } 183 | 184 | func (api *Webapi) GetALLUsers(info *model.NodeInfo) (*AllUsers, error) { 185 | sort := info.Sort 186 | var prifix string 187 | var allusers = AllUsers{ 188 | Data: map[string]model.UserModel{}, 189 | } 190 | if sort == 0 { 191 | prifix = "SS_" 192 | } else { 193 | prifix = "Vmess_" 194 | if info.Server["protocol"] == "tcp" { 195 | prifix += "tcp_" 196 | } else if info.Server["protocol"] == "ws" { 197 | if info.Server["protocol_param"] != "" { 198 | prifix += "ws_" + info.Server["protocol_param"].(string) + "_" 199 | } else { 200 | prifix += "ws_" + "none" + "_" 201 | } 202 | } else if info.Server["protocol"] == "kcp" { 203 | if info.Server["protocol_param"] != "" { 204 | prifix += "kcp_" + info.Server["protocol_param"].(string) + "_" 205 | } else { 206 | prifix += "kcp_" + "none" + "_" 207 | } 208 | } 209 | } 210 | var response = UsersResponse{} 211 | params := map[string]interface{}{ 212 | "node_id": info.NodeID, 213 | } 214 | r, err := api.GetApi("users", params) 215 | if err != nil { 216 | return &allusers, err 217 | } else { 218 | err = r.ToJSON(&response) 219 | allusers.Ret = response.Ret 220 | if err != nil { 221 | return &allusers, err 222 | } else if response.Ret != 1 { 223 | return &allusers, err 224 | } 225 | } 226 | for index := range response.Data { 227 | if info.Server["alterid"] == "" { 228 | response.Data[index].AlterId = 16 229 | } else { 230 | alterid, err := strconv.ParseUint(info.Server["alterid"].(string), 10, 0) 231 | if err == nil { 232 | response.Data[index].AlterId = uint32(alterid) 233 | } 234 | } 235 | key := prifix + response.Data[index].Email + fmt.Sprintf("_AlterID_%d_Method_%s_Passwd_%s_Port_%d", 236 | response.Data[index].AlterId, response.Data[index].Method, response.Data[index].Passwd, response.Data[index].Port, 237 | ) 238 | response.Data[index].PrefixedId = key 239 | allusers.Data[key] = response.Data[index] 240 | } 241 | return &allusers, nil 242 | } 243 | 244 | func (api *Webapi) Post(url string, params map[string]interface{}, data map[string]interface{}) (*req.Resp, error) { 245 | parm := req.Param{ 246 | "key": api.WebToken, 247 | } 248 | for k, v := range params { 249 | parm[k] = v 250 | } 251 | r, err := req.Post(fmt.Sprintf("%s/mod_mu/%s", api.WebBaseURl, url), parm, req.BodyJSON(&data)) 252 | return r, err 253 | } 254 | 255 | func (api *Webapi) UploadSystemLoad(nodeid uint) bool { 256 | var postresponse PostResponse 257 | params := map[string]interface{}{ 258 | "node_id": nodeid, 259 | } 260 | upload_systemload := map[string]interface{}{ 261 | "uptime": utility.GetSystemUptime(), 262 | "load": utility.GetSystemLoad(), 263 | } 264 | r, err := api.Post(fmt.Sprintf("nodes/%d/info", nodeid), params, upload_systemload) 265 | if err != nil { 266 | return false 267 | } else { 268 | err = r.ToJSON(&postresponse) 269 | if err != nil { 270 | return false 271 | } else if postresponse.Ret != 1 { 272 | log.Fatal(postresponse.Data) 273 | } 274 | } 275 | return true 276 | } 277 | 278 | func (api *Webapi) UpLoadUserTraffics(nodeid uint, trafficLog []model.UserTrafficLog) bool { 279 | var postresponse PostResponse 280 | params := map[string]interface{}{ 281 | "node_id": nodeid, 282 | } 283 | 284 | data := map[string]interface{}{ 285 | "data": trafficLog, 286 | } 287 | r, err := api.Post("users/traffic", params, data) 288 | if err != nil { 289 | return false 290 | } else { 291 | err = r.ToJSON(&postresponse) 292 | if err != nil { 293 | return false 294 | } else if postresponse.Ret != 1 { 295 | log.Fatal(postresponse.Data) 296 | } 297 | } 298 | return true 299 | } 300 | func (api *Webapi) UploadSpeedTest(nodeid uint, speedresult []speedtest.Speedresult) bool { 301 | var postresponse PostResponse 302 | params := map[string]interface{}{ 303 | "node_id": nodeid, 304 | } 305 | 306 | data := map[string]interface{}{ 307 | "data": speedresult, 308 | } 309 | r, err := api.Post("func/speedtest", params, data) 310 | if err != nil { 311 | return false 312 | } else { 313 | err = r.ToJSON(&postresponse) 314 | if err != nil { 315 | return false 316 | } else if postresponse.Ret != 1 { 317 | log.Fatal(postresponse.Data) 318 | } 319 | } 320 | return true 321 | } 322 | func (api *Webapi) UpLoadOnlineIps(nodeid uint, onlineIPS []model.UserOnLineIP) bool { 323 | var postresponse PostResponse 324 | params := map[string]interface{}{ 325 | "node_id": nodeid, 326 | } 327 | 328 | data := map[string]interface{}{ 329 | "data": onlineIPS, 330 | } 331 | r, err := api.Post("users/aliveip", params, data) 332 | if err != nil { 333 | return false 334 | } else { 335 | err = r.ToJSON(&postresponse) 336 | if err != nil { 337 | return false 338 | } else if postresponse.Ret != 1 { 339 | log.Fatal(postresponse.Data) 340 | } 341 | } 342 | return true 343 | } 344 | --------------------------------------------------------------------------------