update - shadowsocks

Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
This commit is contained in:
MHSanaei 2023-07-18 03:19:01 +03:30
parent 1f78842b70
commit c2e9ee3665
7 changed files with 197 additions and 51 deletions

View File

@ -3,17 +3,18 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"time"
ptime "github.com/yaa110/go-persian-calendar"
"net/url" "net/url"
"strings" "strings"
"time"
"x-ui/database" "x-ui/database"
"x-ui/database/model" "x-ui/database/model"
"x-ui/logger" "x-ui/logger"
"x-ui/util/common"
"x-ui/web/service" "x-ui/web/service"
"x-ui/xray" "x-ui/xray"
"x-ui/util/common"
"github.com/goccy/go-json" "github.com/goccy/go-json"
ptime "github.com/yaa110/go-persian-calendar"
) )
type SubService struct { type SubService struct {
@ -57,7 +58,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, err
} }
for _, client := range clients { for _, client := range clients {
if client.Enable && client.SubID == subId { if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email,client.ExpiryTime) link := s.getLink(inbound, client.Email, client.ExpiryTime)
result = append(result, link) result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email)) clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
} }
@ -143,7 +144,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string, expiryTi
} }
remainedTraffic := s.getRemainedTraffic(email) remainedTraffic := s.getRemainedTraffic(email)
expiryTimeString := getExpiryTime(expiryTime) expiryTimeString := getExpiryTime(expiryTime)
remark := fmt.Sprintf("%s: %s- %s", email, remainedTraffic, expiryTimeString) remark := fmt.Sprintf("%s: %s- %s", email, remainedTraffic, expiryTimeString)
obj := map[string]interface{}{ obj := map[string]interface{}{
@ -456,7 +457,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string, expiryTi
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
remainedTraffic := s.getRemainedTraffic(email) remainedTraffic := s.getRemainedTraffic(email)
expiryTimeString := getExpiryTime(expiryTime) expiryTimeString := getExpiryTime(expiryTime)
remark := fmt.Sprintf("%s: %s- %s", email, remainedTraffic, expiryTimeString) remark := fmt.Sprintf("%s: %s- %s", email, remainedTraffic, expiryTimeString)
@ -668,7 +669,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, expiryT
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
remainedTraffic := s.getRemainedTraffic(email) remainedTraffic := s.getRemainedTraffic(email)
expiryTimeString := getExpiryTime(expiryTime) expiryTimeString := getExpiryTime(expiryTime)
remark := fmt.Sprintf("%s: %s- %s", email, remainedTraffic, expiryTimeString) remark := fmt.Sprintf("%s: %s- %s", email, remainedTraffic, expiryTimeString)
@ -695,6 +696,8 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, ex
if inbound.Protocol != model.Shadowsocks { if inbound.Protocol != model.Shadowsocks {
return "" return ""
} }
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound) clients, _ := s.inboundService.GetClients(inbound)
var settings map[string]interface{} var settings map[string]interface{}
@ -708,13 +711,69 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, ex
break break
} }
} }
streamNetwork := stream["network"].(string)
params := make(map[string]string)
params["type"] = streamNetwork
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
params["seed"] = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
params["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
case "http":
http, _ := stream["httpSettings"].(map[string]interface{})
params["path"] = http["path"].(string)
params["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
params["quicSecurity"] = quic["security"].(string)
params["key"] = quic["key"].(string)
header := quic["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
url, _ := url.Parse(link)
q := url.Query()
remainedTraffic := s.getRemainedTraffic(clients[clientIndex].Email) for k, v := range params {
expiryTimeString := getExpiryTime(expiryTime) q.Add(k, v)
}
remark := fmt.Sprintf("%s: %s- %s", clients[clientIndex].Email, remainedTraffic ,expiryTimeString) // Set the new query values on the URL
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, remark) url.RawQuery = q.Encode()
remainedTraffic := s.getRemainedTraffic(email)
expiryTimeString := getExpiryTime(expiryTime)
remark := fmt.Sprintf("%s: %s- %s", clients[clientIndex].Email, remainedTraffic, expiryTimeString)
url.Fragment = remark
return url.String()
} }
func searchKey(data interface{}, key string) (interface{}, bool) { func searchKey(data interface{}, key string) (interface{}, bool) {
@ -759,26 +818,26 @@ func searchHost(headers interface{}) string {
return "" return ""
} }
func getExpiryTime(expiryTime int64) string{ func getExpiryTime(expiryTime int64) string {
now := time.Now().Unix() now := time.Now().Unix()
expiryString := "" expiryString := ""
timeDifference := expiryTime/1000 - now timeDifference := expiryTime/1000 - now
if expiryTime == 0 { if expiryTime == 0 {
expiryString = "♾ ⏳" expiryString = "♾ ⏳"
} else if timeDifference > 172800 { } else if timeDifference > 172800 {
expiryString = fmt.Sprintf("%s ⏳", ptime.Unix((expiryTime / 1000), 0).Format("yy-MM-dd hh:mm")) expiryString = fmt.Sprintf("%s ⏳", ptime.Unix((expiryTime/1000), 0).Format("yy-MM-dd hh:mm"))
} else if expiryTime < 0 { } else if expiryTime < 0 {
expiryString = fmt.Sprintf("%d ⏳", expiryTime/-86400000) expiryString = fmt.Sprintf("%d ⏳", expiryTime/-86400000)
} else { } else {
expiryString = fmt.Sprintf("%s %d ⏳", "ساعت", timeDifference/3600) expiryString = fmt.Sprintf("%s %d ⏳", "ساعت", timeDifference/3600)
} }
return expiryString return expiryString
} }
func (s *SubService) getRemainedTraffic( email string) string{ func (s *SubService) getRemainedTraffic(email string) string {
traffic, err := s.inboundService.GetClientTrafficByEmail(email) traffic, err := s.inboundService.GetClientTrafficByEmail(email)
if err != nil { if err != nil {
logger.Warning(err) logger.Warning(err)
@ -788,7 +847,7 @@ func (s *SubService) getRemainedTraffic( email string) string{
if traffic.Total == 0 { if traffic.Total == 0 {
remainedTraffic = "♾ 📊" remainedTraffic = "♾ 📊"
} else { } else {
remainedTraffic = fmt.Sprintf("%s%s" ,common.FormatTraffic(traffic.Total-(traffic.Up+traffic.Down)), "📊") remainedTraffic = fmt.Sprintf("%s%s", common.FormatTraffic(traffic.Total-(traffic.Up+traffic.Down)), "📊")
} }
return remainedTraffic return remainedTraffic

View File

@ -16,8 +16,12 @@ const VmessMethods = {
}; };
const SSMethods = { const SSMethods = {
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm', CHACHA20_POLY1305: 'chacha20-poly1305',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm', AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
}; };
const XTLS_FLOW_CONTROL = { const XTLS_FLOW_CONTROL = {
@ -511,7 +515,8 @@ class TlsStreamSettings extends XrayCommonClass {
} }
if (!ObjectUtil.isEmpty(json.settings)) { if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains); } settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains);
}
return new TlsStreamSettings( return new TlsStreamSettings(
json.serverName, json.serverName,
json.minVersion, json.minVersion,
@ -980,7 +985,6 @@ class Inbound extends XrayCommonClass {
} }
} }
//for Reality
get reality() { get reality() {
return this.stream.security === 'reality'; return this.stream.security === 'reality';
} }
@ -1034,6 +1038,9 @@ class Inbound extends XrayCommonClass {
return ""; return "";
} }
} }
get isSSMultiUser() {
return [SSMethods.BLAKE3_AES_128_GCM,SSMethods.BLAKE3_AES_256_GCM].includes(this.method);
}
get serverName() { get serverName() {
if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) { if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) {
@ -1103,7 +1110,7 @@ class Inbound extends XrayCommonClass {
return this.settings.trojans[index].expiryTime < new Date().getTime(); return this.settings.trojans[index].expiryTime < new Date().getTime();
return false return false
case Protocols.SHADOWSOCKS: case Protocols.SHADOWSOCKS:
if(this.settings.shadowsockses[index].expiryTime > 0) if(this.settings.shadowsockses.length > 0 && this.settings.shadowsockses[index].expiryTime > 0)
return this.settings.shadowsockses[index].expiryTime < new Date().getTime(); return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
return false return false
default: default:
@ -1184,6 +1191,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS: case Protocols.VMESS:
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true; return true;
default: default:
return false; return false;
@ -1410,8 +1418,66 @@ class Inbound extends XrayCommonClass {
genSSLink(address='', remark='', clientIndex = 0) { genSSLink(address='', remark='', clientIndex = 0) {
let settings = this.settings; let settings = this.settings;
const port = this.port; const port = this.port;
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
case "tcp":
const tcp = this.stream.tcp;
if (tcp.type === 'http') {
const request = tcp.request;
params.set("path", request.path.join(','));
const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = request.headers[index].value;
params.set("host", host);
}
params.set("headerType", 'http');
}
break;
case "kcp":
const kcp = this.stream.kcp;
params.set("headerType", kcp.type);
params.set("seed", kcp.seed);
break;
case "ws":
const ws = this.stream.ws;
params.set("path", ws.path);
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = ws.headers[index].value;
params.set("host", host);
}
break;
case "http":
const http = this.stream.http;
params.set("path", http.path);
params.set("host", http.host);
break;
case "quic":
const quic = this.stream.quic;
params.set("quicSecurity", quic.security);
params.set("key", quic.key);
params.set("headerType", quic.type);
break;
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break;
}
return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark); let clientPassword = this.isSSMultiUser ? ':' + settings.shadowsockses[clientIndex].password : '';
let link = `ss://${safeBase64(settings.method + ':' + settings.password + clientPassword)}@${address}:${this.port}`;
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value)
}
url.hash = encodeURIComponent(remark);
return url.toString();
} }
genTrojanLink(address = '', remark = '', clientIndex = 0) { genTrojanLink(address = '', remark = '', clientIndex = 0) {

View File

@ -37,7 +37,7 @@
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
settings = JSON.parse(this.inbound.settings); settings = JSON.parse(this.inbound.settings);
this.client = settings.clients[clientIndex]; this.client = settings.clients[clientIndex];
remark = this.dbInbound.remark + "-" + this.client.email; remark = this.dbInbound.remark + ( this.client ? "-" + this.client.email : '');
address = this.dbInbound.address; address = this.dbInbound.address;
this.subId = ''; this.subId = '';
this.qrcodes = []; this.qrcodes = [];

View File

@ -1,5 +1,6 @@
{{define "form/shadowsocks"}} {{define "form/shadowsocks"}}
<a-form layout="inline" style="padding: 10px 0px;"> <a-form layout="inline" style="padding: 10px 0px;">
<template v-if="inbound.isSSMultiUser">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item> <a-form-item>
@ -106,10 +107,11 @@
</table> </table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</template>
</a-form> </a-form>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'> <a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass"> <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass" @change="SSMethodChange">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option> <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>

View File

@ -179,6 +179,19 @@
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td> <td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
</tr> </tr>
</table> </table>
<template v-if="inbound.protocol == Protocols.SHADOWSOCKS && !inbound.isSSMultiUser">
<a-divider>URL</a-divider>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template>
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;"> <table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
<tr> <tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th> <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
@ -251,7 +264,7 @@
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null; this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index); this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
remark = this.dbInbound.remark + "-" + this.clientSettings.email; remark = this.dbInbound.remark + ( this.clientSettings ? "-" + this.clientSettings.email : '');
address = this.dbInbound.address; address = this.dbInbound.address;
this.links = []; this.links = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) { if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {

View File

@ -54,23 +54,11 @@
}, },
}; };
const protocols = {
VMESS: Protocols.VMESS,
VLESS: Protocols.VLESS,
TROJAN: Protocols.TROJAN,
SHADOWSOCKS: Protocols.SHADOWSOCKS,
DOKODEMO: Protocols.DOKODEMO,
SOCKS: Protocols.SOCKS,
HTTP: Protocols.HTTP,
};
new Vue({ new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#inbound-modal', el: '#inbound-modal',
data: { data: {
inModal: inModal, inModal: inModal,
Protocols: protocols,
SSMethods: SSMethods,
delayedStart: false, delayedStart: false,
get inbound() { get inbound() {
return inModal.inbound; return inModal.inbound;
@ -117,6 +105,17 @@
}); });
} }
}, },
SSMethodChange() {
if (this.inModal.inbound.isSSMultiUser) {
if (this.inModal.inbound.settings.shadowsockses.length ==0){
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
}
} else {
if (this.inModal.inbound.settings.shadowsockses.length > 0){
this.inModal.inbound.settings.shadowsockses = [];
}
}
},
setDefaultCertData(index) { setDefaultCertData(index) {
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert; inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey; inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;

View File

@ -131,7 +131,11 @@
<a-icon type="edit"></a-icon> <a-icon type="edit"></a-icon>
{{ i18n "edit" }} {{ i18n "edit" }}
</a-menu-item> </a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS"> <a-menu-item key="qrcode" v-if="dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.toInbound().isSSMultiUser">
<a-menu-item key="addClient"> <a-menu-item key="addClient">
<a-icon type="user-add"></a-icon> <a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}} {{ i18n "pages.client.add"}}
@ -255,7 +259,7 @@
{{template "client_table"}} {{template "client_table"}}
</a-table> </a-table>
<a-table <a-table
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS" v-else-if="record.protocol === Protocols.TROJAN || record.toInbound().isSSMultiUser"
:row-key="client => client.id" :row-key="client => client.id"
:columns="innerTrojanColumns" :columns="innerTrojanColumns"
:data-source="getInboundClients(record)" :data-source="getInboundClients(record)"
@ -274,7 +278,6 @@
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}} {{template "component/themeSwitcher" .}}
<script> <script>
const columns = [{ const columns = [{
title: '{{ i18n "pages.inbounds.operate" }}', title: '{{ i18n "pages.inbounds.operate" }}',
align: 'center', align: 'center',
@ -357,7 +360,7 @@
trafficDiff: 0, trafficDiff: 0,
defaultCert: '', defaultCert: '',
defaultKey: '', defaultKey: '',
clientCount: {}, clientCount: [],
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false, isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false, refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000, refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
@ -409,12 +412,16 @@
setInbounds(dbInbounds) { setInbounds(dbInbounds) {
this.inbounds.splice(0); this.inbounds.splice(0);
this.dbInbounds.splice(0); this.dbInbounds.splice(0);
this.clientCount.splice(0);
for (const inbound of dbInbounds) { for (const inbound of dbInbounds) {
const dbInbound = new DBInbound(inbound); const dbInbound = new DBInbound(inbound);
to_inbound = dbInbound.toInbound() to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound); this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound); this.dbInbounds.push(dbInbound);
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) { if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
if (inbound.protocol === Protocols.SHADOWSOCKS && (!to_inbound.isSSMultiUser)) {
continue;
}
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound); this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
} }
} }