Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21add1f3ce | |||
| 9968f3885f | |||
| 2ac13ef8f4 | |||
| 4900c14295 | |||
| 55a6d78114 | |||
| caa115bbe3 | |||
| e3be3be9d9 | |||
| 988675a7a7 | |||
| 458f0c20da | |||
| f8fbc3c329 | |||
| 89bc3b5b23 | |||
| edfe0c86e7 | |||
| 6865c8b49d | |||
| 07947c9665 | |||
| 09616b6fac | |||
| 15105710bc |
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install upx -yq
|
||||
sudo apt-get update
|
||||
if [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||
@@ -69,22 +69,18 @@ jobs:
|
||||
fi
|
||||
|
||||
#### Build Sing-Box
|
||||
git clone -b v1.8.13 https://github.com/SagerNet/sing-box
|
||||
export VERSION=v1.8.14
|
||||
git clone -b $VERSION https://github.com/SagerNet/sing-box
|
||||
cd sing-box
|
||||
go build -v -gcflags=all="-l -B -C" -mod=mod -trimpath \
|
||||
-ldflags "-s -w -buildid= -extldflags '-static'" -a \
|
||||
-tags='netgo osusergo static_build with_quic with_grpc with_wireguard with_ech with_utls with_reality_server with_acme with_v2ray_api with_clash_api with_gvisor' \
|
||||
go build -tags with_quic,with_grpc,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_v2ray_api,with_clash_api,with_gvisor \
|
||||
-v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' -s -w -buildid=" \
|
||||
-o sing-box ./cmd/sing-box
|
||||
upx --ultra-brute -9 -v --lzma --best --force sing-box
|
||||
cd ..
|
||||
|
||||
### Build s-ui
|
||||
cd backend
|
||||
go build -v -gcflags=all="-l -B -C" -mod=mod -trimpath \
|
||||
-ldflags "-s -w -buildid= -extldflags '-static'" -a -tags='netgo osusergo static_build sqlite_omit_load_extension' \
|
||||
-o ../sui main.go
|
||||
go build -o ../sui main.go
|
||||
cd ..
|
||||
upx --ultra-brute -9 -v --lzma --best --force sui
|
||||
|
||||
mkdir s-ui
|
||||
cp sui s-ui/
|
||||
|
||||
+5
-4
@@ -3,17 +3,18 @@ WORKDIR /app
|
||||
COPY frontend/ ./
|
||||
RUN npm install && npm run build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS backend-builder
|
||||
FROM golang:1.22-alpine AS backend-builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||
ENV CGO_ENABLED=1
|
||||
RUN apk --no-cache --update add build-base gcc wget unzip
|
||||
ENV GOARCH=$TARGETARCH
|
||||
RUN apk update && apk --no-cache --update add build-base gcc wget unzip
|
||||
COPY backend/ ./
|
||||
COPY --from=front-builder /app/dist/ /app/web/html/
|
||||
RUN go build -o sui main.go
|
||||
RUN go build -ldflags="-w -s" -o sui main.go
|
||||
|
||||
FROM --platform=$BUILDPLATFORM alpine
|
||||
FROM --platform=$TARGETPLATFORM alpine
|
||||
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
||||
ENV TZ=Asia/Tehran
|
||||
WORKDIR /app
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.0.3
|
||||
0.0.4
|
||||
@@ -22,4 +22,5 @@ func (s *DelStatsJob) Run() {
|
||||
logger.Warning("Deleting old statistics failed: ", err)
|
||||
return
|
||||
}
|
||||
logger.Debug("Stats older than ", s.trafficAge, " days were deleted")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ var defaultValueMap = map[string]string{
|
||||
"webListen": "",
|
||||
"webDomain": "",
|
||||
"webPort": "2095",
|
||||
"webSecret": common.Random(32),
|
||||
"secret": common.Random(32),
|
||||
"webCertFile": "",
|
||||
"webKeyFile": "",
|
||||
"webPath": "/app/",
|
||||
@@ -191,11 +191,11 @@ func (s *SettingService) SetWebPath(webPath string) error {
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
secret, err := s.getString("webSecret")
|
||||
if secret == defaultValueMap["webSecret"] {
|
||||
err := s.saveSetting("webSecret", secret)
|
||||
secret, err := s.getString("secret")
|
||||
if secret == defaultValueMap["secret"] {
|
||||
err := s.saveSetting("secret", secret)
|
||||
if err != nil {
|
||||
logger.Warning("save webSecret failed:", err)
|
||||
logger.Warning("save secret failed:", err)
|
||||
}
|
||||
}
|
||||
return []byte(secret), err
|
||||
@@ -318,10 +318,10 @@ func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
|
||||
json.Unmarshal(change.Obj, &obj)
|
||||
|
||||
// Secure file existance check
|
||||
if key == "webCertFile" ||
|
||||
if obj != "" && (key == "webCertFile" ||
|
||||
key == "webKeyFile" ||
|
||||
key == "subCertFile" ||
|
||||
key == "subKeyFile" {
|
||||
key == "subKeyFile") {
|
||||
err = s.fileExists(obj)
|
||||
if err != nil {
|
||||
return common.NewError(" -> ", obj, " is not exists")
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
package common
|
||||
|
||||
import "math/rand"
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var allSeq [62]rune
|
||||
var (
|
||||
allSeq []rune
|
||||
rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
)
|
||||
|
||||
func init() {
|
||||
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
for _, char := range chars {
|
||||
allSeq = append(allSeq, char)
|
||||
}
|
||||
}
|
||||
|
||||
func Random(n int) string {
|
||||
runes := make([]rune, n)
|
||||
for i := 0; i < n; i++ {
|
||||
runes[i] = allSeq[rand.Intn(len(allSeq))]
|
||||
runes[i] = allSeq[rnd.Intn(len(allSeq))]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS singbox-builder
|
||||
LABEL maintainer="Alireza <alireza7@gmail.com>"
|
||||
WORKDIR /app
|
||||
ARG TARGETOS TARGETARCH
|
||||
ARG SINGBOX_VER=v1.8.10
|
||||
ARG SINGBOX_VER=v1.8.13
|
||||
ARG SINGBOX_TAGS="with_quic,with_grpc,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_v2ray_api,with_clash_api,with_gvisor"
|
||||
ARG GOPROXY=""
|
||||
ENV GOPROXY ${GOPROXY}
|
||||
@@ -18,7 +18,7 @@ RUN set -ex \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$SINGBOX_VER\" -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
|
||||
FROM --platform=$BUILDPLATFORM alpine
|
||||
FROM --platform=$TARGETPLATFORM alpine
|
||||
LABEL maintainer="Alireza <alireza7@gmail.com>"
|
||||
ENV TZ=Asia/Tehran
|
||||
WORKDIR /app
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
s-ui:
|
||||
image: alireza7/s-ui
|
||||
|
||||
Generated
+560
-1900
File diff suppressed because it is too large
Load Diff
+20
-20
@@ -3,41 +3,41 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.0.96",
|
||||
"axios": "^1.6.5",
|
||||
"chart.js": "^4.4.1",
|
||||
"axios": "^1.7.2",
|
||||
"chart.js": "^4.4.3",
|
||||
"clipboard": "^2.0.11",
|
||||
"core-js": "^3.29.0",
|
||||
"core-js": "^3.37.1",
|
||||
"moment": "^2.30.1",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"roboto-fontface": "*",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"vue": "^3.2.0",
|
||||
"vue-chartjs": "^5.3.0",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"vue-router": "^4.0.0",
|
||||
"vue-chartjs": "^5.3.1",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue3-persian-datetime-picker": "^1.2.2",
|
||||
"vuetify": "^3.0.0"
|
||||
"vuetify": "^3.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.21.4",
|
||||
"@types/node": "^18.15.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"@babel/types": "^7.24.5",
|
||||
"@types/node": "^18.19.33",
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"sass": "^1.60.0",
|
||||
"typescript": "^5.0.0",
|
||||
"unplugin-fonts": "^1.0.3",
|
||||
"sass": "^1.77.2",
|
||||
"typescript": "^5.4.5",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"vite": "^4.5.3",
|
||||
"vite-plugin-vuetify": "^1.0.0",
|
||||
"vue-tsc": "^1.2.0"
|
||||
"vite-plugin-vuetify": "^1.0.2",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-card subtitle="Hysteria2">
|
||||
<v-row v-if="direction == 'in'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.masquerade != undefined">
|
||||
<v-text-field
|
||||
label="HTTP3 server on auth fail"
|
||||
hide-details
|
||||
@@ -46,7 +46,7 @@
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="data.obfs">
|
||||
<v-row v-if="data.obfs != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.hy.obfs')"
|
||||
@@ -66,6 +66,9 @@
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionObfs" color="primary" :label="$t('types.hy.obfs')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMasq" color="primary" label="Masquerade" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
@@ -95,6 +98,10 @@ export default {
|
||||
optionObfs: {
|
||||
get(): boolean { return this.$props.data.obfs != undefined },
|
||||
set(v:boolean) { this.$props.data.obfs = v ? { type: "salamander", password: "" } : undefined }
|
||||
},
|
||||
optionMasq: {
|
||||
get(): boolean { return this.$props.data.masquerade != undefined },
|
||||
set(v:boolean) { this.$props.data.masquerade = v ? "" : undefined }
|
||||
}
|
||||
},
|
||||
components: { Network }
|
||||
|
||||
@@ -49,8 +49,7 @@ const gaugeColor = computed(() => {
|
||||
background: `rgb(var(--v-theme-${gaugeColor}))`
|
||||
}">
|
||||
</div>
|
||||
<span class="gauge__cover" dir="ltr" v-html="data.text">
|
||||
</span>
|
||||
<div class="gauge__cover"><span dir="ltr" v-html="data.text"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
:label="$t('out.port')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import axios from 'axios'
|
||||
import axios from 'axios';
|
||||
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
||||
|
||||
axios.defaults.baseURL = "./"
|
||||
const pendingRequests = new Map()
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
// Generate a unique key for the request
|
||||
const requestKey = `${config.method}:${config.url}`
|
||||
|
||||
// Check if there is already a pending request with the same key
|
||||
if (pendingRequests.has(requestKey)) {
|
||||
const cancelSource = pendingRequests.get(requestKey)
|
||||
cancelSource.cancel('Duplicate request cancelled')
|
||||
}
|
||||
|
||||
// Create a new cancel token for the request
|
||||
const cancelSource = axios.CancelToken.source()
|
||||
config.cancelToken = cancelSource.token
|
||||
|
||||
// Store the cancel token in the pending requests map
|
||||
pendingRequests.set(requestKey, cancelSource)
|
||||
|
||||
if (config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data'
|
||||
}
|
||||
@@ -15,6 +32,26 @@ axios.interceptors.request.use(
|
||||
(error) => Promise.reject(error),
|
||||
)
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
// Remove the request from the pending requests map
|
||||
const requestKey = `${response.config.method}:${response.config.url}`
|
||||
pendingRequests.delete(requestKey)
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (axios.isCancel(error)) {
|
||||
// Handle duplicate request cancellation here if needed
|
||||
console.warn(error.message)
|
||||
} else {
|
||||
// Remove the request from the pending requests map on error
|
||||
const requestKey = `${error.config.method}:${error.config.url}`
|
||||
pendingRequests.delete(requestKey)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
);
|
||||
|
||||
const api = axios.create()
|
||||
|
||||
export default api
|
||||
@@ -140,10 +140,12 @@ export namespace LinkUtil {
|
||||
host: <string|null>'',
|
||||
path: <string|null>'',
|
||||
serviceName: <string|null>'',
|
||||
type: <string|null>null,
|
||||
}
|
||||
switch (t.type){
|
||||
case TrspTypes.HTTP:
|
||||
const th = <HTTP>t
|
||||
params.type = 'http'
|
||||
params.host = th.host?.join(',')?? null
|
||||
params.path = th.path?? null
|
||||
break
|
||||
@@ -225,6 +227,7 @@ export namespace LinkUtil {
|
||||
host: tParams.host,
|
||||
id: u?.uuid,
|
||||
net: transport?.type?? 'tcp',
|
||||
type: transport?.type == 'http' ? 'http' : undefined,
|
||||
path: tParams.path,
|
||||
port: inbound.listen_port,
|
||||
ps: inbound.tag,
|
||||
|
||||
@@ -158,7 +158,9 @@ const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = (data:Inbound, stats: boolean) => {
|
||||
if (inbounds.value.findIndex(c => c.tag == data.tag) != modal.value.id) {
|
||||
// Check duplicate tag
|
||||
const oldTag = modal.value.id != -1 ? inbounds.value[modal.value.id].tag : null
|
||||
if (data.tag != oldTag && inTags.value.includes(data.tag)) {
|
||||
const sb = Message()
|
||||
sb.showMessage(i18n.global.t('error.dplData') + ': ' + i18n.global.t('objects.tag') ,'error', 5000)
|
||||
return
|
||||
@@ -188,10 +190,12 @@ const saveModal = (data:Inbound, stats: boolean) => {
|
||||
|
||||
inbounds.value[modal.value.id] = data
|
||||
}
|
||||
if (Object.hasOwn(data,'users')) {
|
||||
// Set users
|
||||
data = buildInboundsUsers(data)
|
||||
// Update links
|
||||
if (Object.hasOwn(data,'users')) updateLinks(data)
|
||||
updateLinks(data)
|
||||
}
|
||||
modal.value.visible = false
|
||||
}
|
||||
const updateLinks = (i: InboundWithUser) => {
|
||||
|
||||
@@ -139,7 +139,9 @@ const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = (data:Outbound, stats: boolean) => {
|
||||
if (outbounds.value.findIndex(c => c.tag == data.tag) != modal.value.id) {
|
||||
// Check duplicate tag
|
||||
const oldTag = modal.value.id != -1 ? outbounds.value[modal.value.id].tag : null
|
||||
if (data.tag != oldTag && outboundTags.value.includes(data.tag)) {
|
||||
const sb = Message()
|
||||
sb.showMessage(i18n.global.t('error.dplData') + ': ' + i18n.global.t('objects.tag') ,'error', 5000)
|
||||
return
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<v-text-field v-model="settings.webListen" :label="$t('setting.addr')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="webPort" :label="$t('setting.port')" hide-details></v-text-field>
|
||||
<v-text-field v-model.number="webPort" min="1" type="number" :label="$t('setting.port')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.webPath" :label="$t('setting.webPath')" hide-details></v-text-field>
|
||||
@@ -51,8 +51,9 @@
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="sessionMaxAge"
|
||||
min="0"
|
||||
:label="$t('setting.sessionAge')"
|
||||
:suffix="$t('date.h')"
|
||||
:suffix="$t('date.m')"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
@@ -60,6 +61,7 @@
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="trafficAge"
|
||||
min="0"
|
||||
:label="$t('setting.trafficAge')"
|
||||
:suffix="$t('date.d')"
|
||||
hide-details
|
||||
@@ -87,7 +89,7 @@
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model="subPort"
|
||||
v-model.number="subPort"
|
||||
min="1"
|
||||
:label="$t('setting.port')"
|
||||
hide-details></v-text-field>
|
||||
@@ -114,6 +116,7 @@
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="subUpdates"
|
||||
min="0"
|
||||
:label="$t('setting.update')"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
@@ -248,28 +251,28 @@ const subShowInfo = computed({
|
||||
})
|
||||
|
||||
const webPort = computed({
|
||||
get: () => { return parseInt(settings.value.webPort) },
|
||||
set: (v:number) => { settings.value.webPort = v.toString() }
|
||||
get: () => { return settings.value.webPort.length>0 ? parseInt(settings.value.webPort) : 2095 },
|
||||
set: (v:number) => { settings.value.webPort = v>0 ? v.toString() : "2095" }
|
||||
})
|
||||
|
||||
const sessionMaxAge = computed({
|
||||
get: () => { return parseInt(settings.value.sessionMaxAge) },
|
||||
set: (v:number) => { settings.value.sessionMaxAge = v.toString() }
|
||||
get: () => { return settings.value.sessionMaxAge.length>0 ? parseInt(settings.value.sessionMaxAge) : 0 },
|
||||
set: (v:number) => { settings.value.sessionMaxAge = v>0 ? v.toString() : "0" }
|
||||
})
|
||||
|
||||
const trafficAge = computed({
|
||||
get: () => { return parseInt(settings.value.trafficAge) },
|
||||
set: (v:number) => { settings.value.trafficAge = v.toString() }
|
||||
get: () => { return settings.value.trafficAge.length>0 ? parseInt(settings.value.trafficAge) : 0 },
|
||||
set: (v:number) => { settings.value.trafficAge = v>0 ? v.toString() : "0" }
|
||||
})
|
||||
|
||||
const subPort = computed({
|
||||
get: () => { return parseInt(settings.value.subPort) },
|
||||
set: (v:number) => { settings.value.subPort = v.toString() }
|
||||
get: () => { return settings.value.subPort.length>0 ? parseInt(settings.value.subPort) : 2096 },
|
||||
set: (v:number) => { settings.value.subPort = v>0 ? v.toString() : "2096" }
|
||||
})
|
||||
|
||||
const subUpdates = computed({
|
||||
get: () => { return parseInt(settings.value.subUpdates) },
|
||||
set: (v:number) => { settings.value.subUpdates = v.toString() }
|
||||
get: () => { return settings.value.subUpdates.length>0 ? parseInt(settings.value.subUpdates) : 12 },
|
||||
set: (v:number) => { settings.value.subUpdates = v>0 ? v.toString() : "12" }
|
||||
})
|
||||
|
||||
const stateChange = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user