mirror of
https://github.com/alireza0/x-ui.git
synced 2026-03-18 23:05:48 +00:00
完成xray启动
This commit is contained in:
@@ -19,4 +19,28 @@ class Msg {
|
||||
this.obj = obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DBInbound {
|
||||
id = 0;
|
||||
userId = 0;
|
||||
up = 0;
|
||||
down = 0;
|
||||
remark = 0;
|
||||
enable = false;
|
||||
expiryTime = 0;
|
||||
listen = "";
|
||||
port = 0;
|
||||
protocol = "";
|
||||
settings = "";
|
||||
streamSettings = "";
|
||||
tag = "";
|
||||
sniffing = "";
|
||||
|
||||
constructor(data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
ObjectUtil.cloneProps(this, data);
|
||||
}
|
||||
}
|
||||
1192
web/assets/js/model/xray.js
Normal file
1192
web/assets/js/model/xray.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -135,7 +135,7 @@ class DateUtil {
|
||||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY年M月D日 H时m分s秒')
|
||||
return moment(millis).format('YYYY-M-D H:m:s')
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
|
||||
@@ -13,7 +13,7 @@ class HttpUtil {
|
||||
}
|
||||
}
|
||||
|
||||
static async _respToMsg(resp) {
|
||||
static _respToMsg(resp) {
|
||||
const data = resp.data;
|
||||
if (data == null) {
|
||||
return new Msg(true);
|
||||
@@ -227,6 +227,10 @@ class ObjectUtil {
|
||||
for (const key of Object.keys(src)) {
|
||||
if (!src.hasOwnProperty(key)) {
|
||||
continue;
|
||||
} else if (!dest.hasOwnProperty(key)) {
|
||||
continue;
|
||||
} else if (src[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (ignoreEmpty) {
|
||||
dest[key] = src[key];
|
||||
@@ -259,4 +263,11 @@ class ObjectUtil {
|
||||
}
|
||||
}
|
||||
|
||||
static orDefault(obj, defaultValue) {
|
||||
if (obj == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@ import (
|
||||
type BaseController struct {
|
||||
}
|
||||
|
||||
func NewBaseController(g *gin.RouterGroup) *BaseController {
|
||||
return &BaseController{}
|
||||
}
|
||||
|
||||
func (a *BaseController) before(c *gin.Context) {
|
||||
if !session.IsLogin(c) {
|
||||
pureJsonMsg(c, false, "登录时效已过,请重新登录")
|
||||
116
web/controller/server.go
Normal file
116
web/controller/server.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gin-gonic/gin"
|
||||
"runtime"
|
||||
"time"
|
||||
"x-ui/web/service"
|
||||
)
|
||||
|
||||
func stopServerController(a *ServerController) {
|
||||
a.stopTask()
|
||||
}
|
||||
|
||||
type ServerController struct {
|
||||
*serverController
|
||||
}
|
||||
|
||||
func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||
a := &ServerController{
|
||||
serverController: newServerController(g),
|
||||
}
|
||||
runtime.SetFinalizer(a, stopServerController)
|
||||
return a
|
||||
}
|
||||
|
||||
type serverController struct {
|
||||
BaseController
|
||||
|
||||
serverService service.ServerService
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
lastStatus *service.Status
|
||||
lastGetStatusTime time.Time
|
||||
|
||||
lastVersions []string
|
||||
lastGetVersionsTime time.Time
|
||||
}
|
||||
|
||||
func newServerController(g *gin.RouterGroup) *serverController {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a := &serverController{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
lastGetStatusTime: time.Now(),
|
||||
}
|
||||
a.initRouter(g)
|
||||
a.startTask()
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *serverController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/server/status", a.status)
|
||||
g.POST("/server/getXrayVersion", a.getXrayVersion)
|
||||
g.POST("/server/installXray/:version", a.installXray)
|
||||
}
|
||||
|
||||
func (a *serverController) refreshStatus() {
|
||||
status := a.serverService.GetStatus(a.lastStatus)
|
||||
a.lastStatus = status
|
||||
}
|
||||
|
||||
func (a *serverController) startTask() {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
break
|
||||
default:
|
||||
}
|
||||
now := time.Now()
|
||||
if now.Sub(a.lastGetStatusTime) > time.Minute*3 {
|
||||
time.Sleep(time.Second * 2)
|
||||
continue
|
||||
}
|
||||
a.refreshStatus()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *serverController) stopTask() {
|
||||
a.cancel()
|
||||
}
|
||||
|
||||
func (a *serverController) status(c *gin.Context) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
|
||||
jsonObj(c, a.lastStatus, nil)
|
||||
}
|
||||
|
||||
func (a *serverController) getXrayVersion(c *gin.Context) {
|
||||
now := time.Now()
|
||||
if now.Sub(a.lastGetVersionsTime) <= time.Minute {
|
||||
jsonObj(c, a.lastVersions, nil)
|
||||
return
|
||||
}
|
||||
|
||||
versions, err := a.serverService.GetXrayVersions()
|
||||
if err != nil {
|
||||
jsonMsg(c, "获取版本", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.lastVersions = versions
|
||||
a.lastGetVersionsTime = time.Now()
|
||||
|
||||
jsonObj(c, versions, nil)
|
||||
}
|
||||
|
||||
func (a *serverController) installXray(c *gin.Context) {
|
||||
version := c.Param("version")
|
||||
err := a.serverService.UpdateXray(version)
|
||||
jsonMsg(c, "安装 xray", err)
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
|
||||
} else {
|
||||
m.Success = false
|
||||
m.Msg = msg + "失败: " + err.Error()
|
||||
logger.Warning(msg, err)
|
||||
logger.Warning(msg+"失败: ", err)
|
||||
}
|
||||
c.JSON(http.StatusOK, m)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
|
||||
}
|
||||
data["title"] = title
|
||||
data["request_uri"] = c.Request.RequestURI
|
||||
data["base_path"] = config.GetBasePath()
|
||||
data["base_path"] = c.GetString("base_path")
|
||||
c.HTML(http.StatusOK, name, getContext(data))
|
||||
}
|
||||
|
||||
|
||||
80
web/controller/xui.go
Normal file
80
web/controller/xui.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"strconv"
|
||||
"x-ui/database/model"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
)
|
||||
|
||||
type XUIController struct {
|
||||
BaseController
|
||||
|
||||
inboundService service.InboundService
|
||||
}
|
||||
|
||||
func NewXUIController(g *gin.RouterGroup) *XUIController {
|
||||
a := &XUIController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/xui")
|
||||
|
||||
g.GET("/", a.index)
|
||||
g.GET("/inbounds", a.inbounds)
|
||||
g.POST("/inbounds", a.postInbounds)
|
||||
g.POST("/inbound/add", a.addInbound)
|
||||
g.POST("/inbound/del/:id", a.delInbound)
|
||||
g.GET("/setting", a.setting)
|
||||
}
|
||||
|
||||
func (a *XUIController) index(c *gin.Context) {
|
||||
html(c, "index.html", "系统状态", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) inbounds(c *gin.Context) {
|
||||
html(c, "inbounds.html", "入站列表", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) setting(c *gin.Context) {
|
||||
html(c, "setting.html", "设置", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) postInbounds(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
||||
if err != nil {
|
||||
jsonMsg(c, "获取", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, inbounds, nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) addInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, "添加", err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
inbound.UserId = user.Id
|
||||
inbound.Enable = true
|
||||
log.Println(inbound)
|
||||
err = a.inboundService.AddInbound(inbound)
|
||||
jsonMsg(c, "添加", err)
|
||||
}
|
||||
|
||||
func (a *XUIController) delInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, "删除", err)
|
||||
return
|
||||
}
|
||||
err = a.inboundService.DelInbound(id)
|
||||
jsonMsg(c, "删除", err)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type XUIController struct {
|
||||
BaseController
|
||||
}
|
||||
|
||||
func NewXUIController(g *gin.RouterGroup) *XUIController {
|
||||
a := &XUIController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/xui")
|
||||
|
||||
g.GET("/", a.index)
|
||||
g.GET("/accounts", a.index)
|
||||
g.GET("/setting", a.setting)
|
||||
}
|
||||
|
||||
func (a *XUIController) index(c *gin.Context) {
|
||||
html(c, "index.html", "系统状态", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) setting(c *gin.Context) {
|
||||
|
||||
}
|
||||
@@ -9,10 +9,11 @@
|
||||
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/uri/URI.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/models.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/common.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/utils.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/models.js?{{ .cur_ver }}"></script>
|
||||
<script>
|
||||
const basePath = '{{ .base_path }}';
|
||||
axios.defaults.baseURL = basePath;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{define "promptModel"}}
|
||||
{{define "promptModal"}}
|
||||
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
|
||||
:closable="true" @ok="promptModal.ok" :mask-closable="false"
|
||||
:ok-text="promptModal.okText" cancel-text="取消">
|
||||
|
||||
59
web/html/common/qrcode_modal.html
Normal file
59
web/html/common/qrcode_modal.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{{define "qrcodeModal"}}
|
||||
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
||||
:closable="true" width="300px" :ok-text="qrModal.okText"
|
||||
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
|
||||
<canvas id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
|
||||
const qrModal = {
|
||||
title: '',
|
||||
content: '',
|
||||
okText: '',
|
||||
copyText: '',
|
||||
qrcode: null,
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
show: function (title='', content='', okText='{{ i18n "copy" }}', copyText='') {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.okText = okText;
|
||||
if (ObjectUtil.isEmpty(copyText)) {
|
||||
this.copyText = content;
|
||||
} else {
|
||||
this.copyText = copyText;
|
||||
}
|
||||
this.visible = true;
|
||||
qrModalApp.$nextTick(() => {
|
||||
if (this.clipboard === null) {
|
||||
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
|
||||
text: () => this.copyText,
|
||||
});
|
||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
||||
}
|
||||
if (this.qrcode === null) {
|
||||
this.qrcode = new QRious({
|
||||
element: document.querySelector('#qrCode'),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
} else {
|
||||
this.qrcode.value = content;
|
||||
}
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
const qrModalApp = new Vue({
|
||||
el: '#qrcode-modal',
|
||||
data: {
|
||||
qrModal: qrModal,
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
58
web/html/common/text_modal.html
Normal file
58
web/html/common/text_modal.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{{define "textModal"}}
|
||||
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
|
||||
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
|
||||
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
||||
@click="downloader.download(txtModal.fileName, txtModal.content)">
|
||||
{{ i18n "download" }} [[ txtModal.fileName ]]
|
||||
</a-button>
|
||||
<a-input type="textarea" v-model="txtModal.content"
|
||||
:autosize="{ minRows: 10, maxRows: 20}"></a-input>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
|
||||
const txtModal = {
|
||||
title: '',
|
||||
content: '',
|
||||
fileName: '',
|
||||
qrcode: null,
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
show: function (title='', content='', fileName='') {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.fileName = fileName;
|
||||
this.visible = true;
|
||||
textModalApp.$nextTick(() => {
|
||||
if (this.clipboard === null) {
|
||||
this.clipboard = new ClipboardJS('#txt-modal-ok-btn', {
|
||||
text: () => this.content,
|
||||
});
|
||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
||||
}
|
||||
if (this.qrcode === null) {
|
||||
this.qrcode = new QRious({
|
||||
element: document.querySelector('#qrCode'),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
} else {
|
||||
this.qrcode.value = content;
|
||||
}
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
this.visible = false;
|
||||
},
|
||||
};
|
||||
|
||||
const textModalApp = new Vue({
|
||||
el: '#text-modal',
|
||||
data: {
|
||||
txtModal: txtModal,
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -3,7 +3,7 @@
|
||||
<a-icon type="dashboard"></a-icon>
|
||||
<span>系统状态</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}xui/accounts">
|
||||
<a-menu-item key="{{ .base_path }}xui/inbounds">
|
||||
<a-icon type="user"></a-icon>
|
||||
<span>账号列表</span>
|
||||
</a-menu-item>
|
||||
|
||||
535
web/html/x-ui/inbound_modal.html
Normal file
535
web/html/x-ui/inbound_modal.html
Normal file
@@ -0,0 +1,535 @@
|
||||
{{define "inboundModal"}}
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
|
||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
|
||||
<!-- base -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "remark" }}'>
|
||||
<a-input v-model.trim="inModal.inbound.remark"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "enable" }}'>
|
||||
<a-switch v-model="inModal.inbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="inModal.inbound.protocol" style="width: 160px;"
|
||||
@change="protocolChange">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
监听 IP
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
不懂请保持默认
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="inModal.inbound.listen"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="端口">
|
||||
<a-input type="number" v-model.number="inModal.inbound.port"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess settings -->
|
||||
<a-form v-if="inModal.inbound.protocol === Protocols.VMESS"
|
||||
layout="inline">
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="inModal.inbound.settings.vmesses[0].id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="额外 ID">
|
||||
<a-input type="number" v-model.number="inModal.inbound.settings.vmesses[0].alterId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="禁用不安全加密">
|
||||
<a-switch v-model.number="inModal.inbound.settings.disableInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vless settings -->
|
||||
<a-form v-if="inModal.inbound.protocol === Protocols.VLESS"
|
||||
layout="inline">
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="inModal.inbound.settings.vlesses[0].id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="flow">
|
||||
<a-select v-model="inModal.inbound.settings.vlesses[0].flow" style="width: 150px">
|
||||
<a-select-option value="">无</a-select-option>
|
||||
<a-select-option v-for="key in VLESS_FLOW" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-form v-if="inModal.inbound.protocol === Protocols.VLESS"
|
||||
layout="inline">
|
||||
<a-form-item label="fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inModal.inbound.settings.addFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inModal.inbound.settings.fallbacks" layout="inline">
|
||||
<a-form-item>
|
||||
<a-divider>
|
||||
fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inModal.inbound.settings.delFallback(index)" style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
</a-form-item>
|
||||
<a-form-item label="name">
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn">
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="path">
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="dest">
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- trojan settings -->
|
||||
<a-form v-if="inModal.inbound.protocol === Protocols.TROJAN"
|
||||
layout="inline">
|
||||
<a-form-item label="密码">
|
||||
<a-input v-model.trim="inModal.inbound.settings.clients[0].password"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- shadowsocks -->
|
||||
<a-form v-if="inModal.inbound.protocol === Protocols.SHADOWSOCKS"
|
||||
layout="inline">
|
||||
<a-form-item label="加密">
|
||||
<a-select v-model="inModal.inbound.settings.method" style="width: 165px;">
|
||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input v-model.trim="inModal.inbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="网络">
|
||||
<a-select v-model="inModal.inbound.settings.network" style="width: 100px;">
|
||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="udp">udp</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="inModal.inbound.protocol === Protocols.VMESS
|
||||
|| inModal.inbound.protocol === Protocols.VLESS
|
||||
|| inModal.inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
|
||||
<!-- select stream network -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="传输">
|
||||
<a-select v-model="inModal.inbound.stream.network" @change="streamNetworkChange">
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="kcp">kcp</a-select-option>
|
||||
<a-select-option value="ws">ws</a-select-option>
|
||||
<a-select-option value="http">http</a-select-option>
|
||||
<a-select-option value="quic">quic</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess tcp -->
|
||||
<template v-if="inModal.inbound.stream.network === 'tcp'">
|
||||
<!-- vmess tcp type -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="http 伪装">
|
||||
<a-switch
|
||||
:checked="inModal.inbound.stream.tcp.type === 'http'"
|
||||
@change="checked => inModal.inbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess tcp request -->
|
||||
<a-form v-if="inModal.inbound.stream.tcp.type === 'http'"
|
||||
layout="inline">
|
||||
<a-form-item label="请求版本">
|
||||
<a-input v-model.trim="inModal.inbound.stream.tcp.request.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="请求方法">
|
||||
<a-input v-model.trim="inModal.inbound.stream.tcp.request.method"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="请求路径">
|
||||
<a-row v-for="(path, index) in inModal.inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inModal.inbound.stream.tcp.request.path[index]"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item label="请求头">
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inModal.inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inModal.inbound.stream.tcp.request.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before="名称"></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before="值">
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inModal.inbound.stream.tcp.request.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess tcp response -->
|
||||
<a-form v-if="inModal.inbound.stream.tcp.type === 'http'"
|
||||
layout="inline">
|
||||
<a-form-item label="响应版本">
|
||||
<a-input v-model.trim="inModal.inbound.stream.tcp.response.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="响应状态">
|
||||
<a-input v-model.trim="inModal.inbound.stream.tcp.response.status"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="响应状态说明">
|
||||
<a-input v-model.trim="inModal.inbound.stream.tcp.response.reason"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="响应头">
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inModal.inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inModal.inbound.stream.tcp.response.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before="名称"></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before="值">
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inModal.inbound.stream.tcp.response.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<!-- vmess kcp -->
|
||||
<a-form v-if="inModal.inbound.stream.network === 'kcp'"
|
||||
layout="inline">
|
||||
<a-form-item label="伪装">
|
||||
<a-select v-model="inModal.inbound.stream.kcp.type" style="width: 280px;">
|
||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input v-model.number="inModal.inbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="mtu">
|
||||
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.mtu"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="tti (ms)">
|
||||
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.tti"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uplink capacity (MB/S)">
|
||||
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.upCap"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="downlink capacity (MB/S)">
|
||||
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.downCap"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="congestion">
|
||||
<a-switch v-model="inModal.inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="read buffer size (MB)">
|
||||
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.readBuffer"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="write buffer size (MB)">
|
||||
<a-input type="number" v-model.number="inModal.inbound.stream.kcp.writeBuffer"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess ws -->
|
||||
<a-form v-if="inModal.inbound.stream.network === 'ws'"
|
||||
layout="inline">
|
||||
<a-form-item label="路径">
|
||||
<a-input v-model.trim="inModal.inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="请求头">
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inModal.inbound.stream.ws.addHeader('Host', '')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inModal.inbound.stream.ws.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name"
|
||||
addon-before="名称"></a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
addon-before="值">
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small"
|
||||
@click="inModal.inbound.stream.ws.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess http -->
|
||||
<a-form v-if="inModal.inbound.stream.network === 'http'"
|
||||
layout="inline">
|
||||
<a-form-item label="路径">
|
||||
<a-input v-model.trim="inModal.inbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="host">
|
||||
<a-row v-for="(host, index) in inModal.inbound.stream.http.host">
|
||||
<a-input v-model.trim="inModal.inbound.stream.http.host[index]"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess quic -->
|
||||
<a-form v-if="inModal.inbound.stream.network === 'quic'"
|
||||
layout="inline">
|
||||
<a-form-item label="加密">
|
||||
<a-select v-model="inModal.inbound.stream.quic.security" style="width: 165px;">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input v-model.trim="inModal.inbound.stream.quic.key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="伪装">
|
||||
<a-select v-model="inModal.inbound.stream.quic.type" style="width: 280px;">
|
||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<!-- dokodemo-door -->
|
||||
<a-form v-if="inModal.inbound.protocol === Protocols.DOKODEMO"
|
||||
layout="inline">
|
||||
<a-form-item label="目标地址">
|
||||
<a-input v-model.trim="inModal.inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="目标端口">
|
||||
<a-input type="number" v-model.number="inModal.inbound.settings.port"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="网络">
|
||||
<a-select v-model="inModal.inbound.settings.network" style="width: 100px;">
|
||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="udp">udp</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- socks -->
|
||||
<a-form v-if="inModal.inbound.protocol === Protocols.SOCKS"
|
||||
layout="inline">
|
||||
<a-form-item label="密码认证">
|
||||
<a-switch :checked="inModal.inbound.settings.auth === 'password'"
|
||||
@change="checked => inModal.inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inModal.inbound.settings.auth === 'password'">
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model.trim="inModal.inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input v-model.trim="inModal.inbound.settings.accounts[0].pass"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="启用 udp">
|
||||
<a-switch v-model="inModal.inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inModal.inbound.settings.udp"
|
||||
label="IP">
|
||||
<a-input v-model.trim="inModal.inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- http -->
|
||||
<a-form v-if="inModal.inbound.protocol === Protocols.HTTP"
|
||||
layout="inline">
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model.trim="inModal.inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input v-model.trim="inModal.inbound.settings.accounts[0].pass"></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="(inModal.inbound.protocol === Protocols.VMESS
|
||||
|| inModal.inbound.protocol === Protocols.VLESS
|
||||
|| inModal.inbound.protocol === Protocols.TROJAN
|
||||
|| inModal.inbound.protocol === Protocols.SHADOWSOCKS)
|
||||
&& ['tcp', 'ws', 'http', 'quic'].indexOf(inModal.inbound.stream.network) >= 0">
|
||||
|
||||
<!-- tls enable -->
|
||||
<a-form layout="inline" v-if="inModal.inbound.protocol !== Protocols.TROJAN">
|
||||
<a-form-item label="tls">
|
||||
<a-switch
|
||||
:checked="inModal.inbound.stream.security === 'tls'"
|
||||
@change="checked => inModal.inbound.stream.security = checked ? 'tls' : 'none'">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inModal.inbound.protocol === Protocols.VLESS && inModal.inbound.stream.security === 'tls' && inModal.inbound.stream.network === 'tcp'" label="xtls">
|
||||
<a-switch v-model="inModal.inbound.stream.is_xtls"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tls settings -->
|
||||
<a-form v-if="inModal.inbound.stream.security === 'tls'"
|
||||
layout="inline">
|
||||
<a-form-item label="域名">
|
||||
<a-input v-model.trim="inModal.inbound.stream.tls.server"></a-input>
|
||||
</a-form-item>
|
||||
{# <a-form-item label="允许不安全">#}
|
||||
{# <a-switch v-model="inModal.inbound.stream.tls.allowInsecure"></a-switch>#}
|
||||
{# </a-form-item>#}
|
||||
<a-form-item label="证书">
|
||||
<a-radio-group v-model="inModal.inbound.stream.tls.certs[0].useFile"
|
||||
button-style="solid">
|
||||
<a-radio-button :value="true">certificate file path</a-radio-button>
|
||||
<a-radio-button :value="false">certificate file content</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="inModal.inbound.stream.tls.certs[0].useFile">
|
||||
<a-form-item label="公钥文件路径">
|
||||
<a-input v-model.trim="inModal.inbound.stream.tls.certs[0].certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="密钥文件路径">
|
||||
<a-input v-model.trim="inModal.inbound.stream.tls.certs[0].keyFile"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label="公钥内容">
|
||||
<a-input type="textarea" :rows="2"
|
||||
v-model="inModal.inbound.stream.tls.certs[0].cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="密钥内容">
|
||||
<a-input type="textarea" :rows="2"
|
||||
v-model="inModal.inbound.stream.tls.certs[0].key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<!-- sniffing -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
sniffing
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
没有特殊需求保持默认即可
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-switch v-model="inModal.inbound.sniffing.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const inModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '确定',
|
||||
confirm: null,
|
||||
inbound: new Inbound(),
|
||||
ok() {
|
||||
ObjectUtil.execute(inModal.confirm);
|
||||
},
|
||||
show({ title='', okText='确定', inbound=null, confirm=()=>{} }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
if (inbound) {
|
||||
this.inbound = Inbound.fromJson(inbound.toJson());
|
||||
} else {
|
||||
this.inbound = new Inbound();
|
||||
}
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
inModal.visible = false;
|
||||
inModal.closeLoading();
|
||||
},
|
||||
loading() {
|
||||
inModal.confirmLoading = true;
|
||||
},
|
||||
closeLoading() {
|
||||
inModal.confirmLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
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({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#inbound-modal',
|
||||
data: {
|
||||
inModal: inModal,
|
||||
Protocols: protocols,
|
||||
SSMethods: SSMethods,
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange(oldValue) {
|
||||
if (oldValue === 'kcp') {
|
||||
this.inModal.inbound.stream.security = 'none';
|
||||
}
|
||||
},
|
||||
protocolChange(value) {
|
||||
this.inModal.inbound.settings = Inbound.Settings.getSettings(value);
|
||||
if (value === Protocols.TROJAN) {
|
||||
this.inModal.inbound.stream.security = 'tls';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
286
web/html/x-ui/inbounds.html
Normal file
286
web/html/x-ui/inbounds.html
Normal file
@@ -0,0 +1,286 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||
<transition name="list" appear>
|
||||
<a-tag v-if="true" color="red" style="margin-bottom: 10px">
|
||||
Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information
|
||||
</a-tag>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable style="margin-bottom: 20px;">
|
||||
<div slot="title">
|
||||
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
|
||||
</div>
|
||||
<a-row>
|
||||
<a-input v-model="searchKey" placeholder="search" autofocus></a-input>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
upload / download:
|
||||
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
total traffic:
|
||||
<a-popconfirm title="Are you sure you want to reset all traffic to 0? It\'s unrecoverable"
|
||||
@confirm="resetAllTraffic()"
|
||||
ok-text="confirm" cancel-text="cancel">
|
||||
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
|
||||
</a-popconfirm>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
number of accounts:
|
||||
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable>
|
||||
<a-table :columns="columns" :row-key="inbound => inbound.id"
|
||||
:data-source="dbInbounds"
|
||||
:loading="spinning" :scroll="{ x: 1500 }"
|
||||
:pagination="false"
|
||||
@change="() => getDBInbounds()">
|
||||
<template slot="protocol" slot-scope="text, inbound">
|
||||
<a-tag color="blue">[[ inbound.protocol ]]</a-tag>
|
||||
</template>
|
||||
<template slot="settings" slot-scope="text, inbound">
|
||||
<a-button type="link">查看</a-button>
|
||||
</template>
|
||||
<template slot="streamSettings" slot-scope="text, inbound">
|
||||
<a-button type="link">查看</a-button>
|
||||
</template>
|
||||
<template slot="enable" slot-scope="text, inbound">
|
||||
<a-tag v-if="inbound.enable" color="green">启用</a-tag>
|
||||
<a-tag v-else color="red">禁用</a-tag>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, inbound">
|
||||
<span v-if="inbound.expiryTime > 0" color="red">[[ DateUtil.formatMillis(inbound.expiryTime) ]]</span>
|
||||
<span v-else>无限期</span>
|
||||
</template>
|
||||
<template slot="action" slot-scope="text, inbound">
|
||||
<a-button type="primary" icon="qrcode"></a-button>
|
||||
<a-button type="primary" icon="edit"></a-button>
|
||||
<a-button type="danger" icon="delete" @click="delInbound(inbound)"></a-button>
|
||||
</template>
|
||||
|
||||
<template slot="expandedRowRender" slot-scope="inbound" style="margin: 0">
|
||||
[[ inbound.id ]]
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</transition>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
<script>
|
||||
|
||||
const columns = [{
|
||||
title: "id",
|
||||
align: 'center',
|
||||
dataIndex: "id",
|
||||
width: 60,
|
||||
}, {
|
||||
title: "protocol",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'protocol' },
|
||||
}, {
|
||||
title: "port",
|
||||
align: 'center',
|
||||
dataIndex: "port",
|
||||
width: 60,
|
||||
}, {
|
||||
title: "settings",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'settings' },
|
||||
}, {
|
||||
title: "streamSettings",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'streamSettings' },
|
||||
}, {
|
||||
title: "enable",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'enable' },
|
||||
}, {
|
||||
title: "expiryTime",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'expiryTime' },
|
||||
}, {
|
||||
title: "action",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'action' },
|
||||
}];
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
ip: location.hostname,
|
||||
spinning: false,
|
||||
dbInbounds: [],
|
||||
searchKey: '',
|
||||
},
|
||||
methods: {
|
||||
loading(spinning=true) {
|
||||
this.spinning = spinning;
|
||||
},
|
||||
empDefault(o, defaultValue='') {return ObjectUtil.isEmpty(o) ? defaultValue : o},
|
||||
async getDBInbounds() {
|
||||
this.loading();
|
||||
const msg = await HttpUtil.post('/xui/inbounds');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
this.setInbounds(msg.obj);
|
||||
},
|
||||
setInbounds(dbInbounds) {
|
||||
this.dbInbounds.splice(0);
|
||||
for (const inbound of dbInbounds) {
|
||||
this.dbInbounds.push(new DBInbound(inbound));
|
||||
}
|
||||
},
|
||||
searchInbounds(key) {
|
||||
if (ObjectUtil.isEmpty(key)) {
|
||||
this.searchedInbounds = this.dbInbounds.slice();
|
||||
} else {
|
||||
this.searchedInbounds.splice(0, this.searchedInbounds.length);
|
||||
this.dbInbounds.forEach(inbound => {
|
||||
if (ObjectUtil.deepSearch(inbound, key)) {
|
||||
this.searchedInbounds.push(inbound);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
openAddInbound() {
|
||||
inModal.show({
|
||||
title: 'add account',
|
||||
okText: 'add',
|
||||
confirm: async () => {
|
||||
inModal.loading();
|
||||
await this.addInbound(inModal.inbound);
|
||||
inModal.closeLoading();
|
||||
}
|
||||
});
|
||||
},
|
||||
openEditInbound(inbound) {
|
||||
inModal.show({
|
||||
title: 'update account',
|
||||
okText: 'update',
|
||||
inbound: inbound,
|
||||
confirm: async () => {
|
||||
inModal.loading();
|
||||
inModal.inbound.id = inbound.id;
|
||||
await this.updateInbound(inModal.inbound);
|
||||
inModal.closeLoading();
|
||||
}
|
||||
});
|
||||
},
|
||||
async addInbound(inbound) {
|
||||
let data = {
|
||||
port: inbound.port,
|
||||
listen: inbound.listen,
|
||||
protocol: inbound.protocol,
|
||||
settings: inbound.settings.toString(false),
|
||||
stream_settings: inbound.stream.toString(false),
|
||||
sniffing: [Protocols.VMESS, Protocols.VLESS, Protocols.SHADOWSOCKS].indexOf(inbound.protocol) >= 0 ? inbound.sniffing.toString(false) : '{}',
|
||||
remark: inbound.remark,
|
||||
};
|
||||
await this.submit('/xui/inbound/add', data, inModal);
|
||||
},
|
||||
async updateInbound(inbound) {
|
||||
const data = {
|
||||
port: inbound.port,
|
||||
listen: inbound.listen,
|
||||
protocol: inbound.protocol,
|
||||
settings: inbound.settings.toString(false),
|
||||
stream_settings: inbound.stream.toString(false),
|
||||
sniffing: [Protocols.VMESS, Protocols.VLESS, Protocols.SHADOWSOCKS].indexOf(inbound.protocol) >= 0 ? inbound.sniffing.toString(false) : '{}',
|
||||
remark: inbound.remark,
|
||||
enable: inbound.enable,
|
||||
};
|
||||
await this.submit(`/xui/inbound/update/${inbound.id}`, data, inModal);
|
||||
},
|
||||
delInbound(inbound) {
|
||||
this.$confirm({
|
||||
title: 'delete account',
|
||||
content: 'Cannot be restored after deletion, confirm deletion?',
|
||||
okText: 'delete',
|
||||
cancelText: 'cancel',
|
||||
onOk: () => this.submit('/xui/inbound/del/' + inbound.id),
|
||||
});
|
||||
},
|
||||
resetTraffic(inbound) {
|
||||
this.submit(`/xui/reset_traffic/${inbound.id}`);
|
||||
},
|
||||
resetAllTraffic() {
|
||||
this.submit('/xui/reset_all_traffic');
|
||||
},
|
||||
setEnable(inbound, enable) {
|
||||
let data = {enable: enable};
|
||||
this.submit(`/xui/inbound/update/${inbound.id}`, data);
|
||||
},
|
||||
async submit(url, data, modal) {
|
||||
const msg = await HttpUtil.post(url, data);
|
||||
if (msg.success) {
|
||||
this.getDBInbounds();
|
||||
if (modal != null) {
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchKey(value) {
|
||||
this.searchInbounds(value);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getDBInbounds();
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
let down = 0, up = 0;
|
||||
for (let i = 0; i < this.dbInbounds.length; ++i) {
|
||||
down += this.dbInbounds[i].down;
|
||||
up += this.dbInbounds[i].up;
|
||||
}
|
||||
return {
|
||||
down: down,
|
||||
up: up,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{template "inboundModal"}}
|
||||
{{template "promptModal"}}
|
||||
{{template "qrcodeModal"}}
|
||||
{{template "textModal"}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -68,7 +68,7 @@
|
||||
<a-card hoverable>
|
||||
xray 状态:
|
||||
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
|
||||
<a-tooltip v-if="status.xray.stat === State.Error">
|
||||
<a-tooltip v-if="status.xray.state === State.Error">
|
||||
<template slot="title">
|
||||
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
||||
</template>
|
||||
@@ -175,7 +175,6 @@
|
||||
</template>
|
||||
</a-modal>
|
||||
</a-layout>
|
||||
</body>
|
||||
{{template "js" .}}
|
||||
<script>
|
||||
|
||||
@@ -330,4 +329,5 @@
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
64
web/service/config.json
Normal file
64
web/service/config.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"api": {
|
||||
"services": [
|
||||
"HandlerService",
|
||||
"LoggerService",
|
||||
"StatsService"
|
||||
],
|
||||
"tag": "api"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"port": 62789,
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
},
|
||||
"tag": "api"
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"settings": {},
|
||||
"tag": "blocked"
|
||||
}
|
||||
],
|
||||
"policy": {
|
||||
"system": {
|
||||
"statsInboundDownlink": true,
|
||||
"statsInboundUplink": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
"rules": [
|
||||
{
|
||||
"inboundTag": [
|
||||
"api"
|
||||
],
|
||||
"outboundTag": "api",
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
],
|
||||
"outboundTag": "blocked",
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"outboundTag": "blocked",
|
||||
"protocol": [
|
||||
"bittorrent"
|
||||
],
|
||||
"type": "field"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stats": {}
|
||||
}
|
||||
40
web/service/inbound.go
Normal file
40
web/service/inbound.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
)
|
||||
|
||||
type InboundService struct {
|
||||
}
|
||||
|
||||
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Where("user_id = ?", userId).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) AddInbound(inbound *model.Inbound) error {
|
||||
db := database.GetDB()
|
||||
return db.Save(inbound).Error
|
||||
}
|
||||
|
||||
func (s *InboundService) DelInbound(id int) error {
|
||||
db := database.GetDB()
|
||||
return db.Delete(model.Inbound{}, id).Error
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
package controller
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"fmt"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"github.com/shirou/gopsutil/load"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
"github.com/shirou/gopsutil/net"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
type ProcessState string
|
||||
@@ -26,7 +30,8 @@ const (
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
Cpu float64 `json:"cpu"`
|
||||
T time.Time `json:"-"`
|
||||
Cpu float64 `json:"cpu"`
|
||||
Mem struct {
|
||||
Current uint64 `json:"current"`
|
||||
Total uint64 `json:"total"`
|
||||
@@ -62,43 +67,15 @@ type Release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
func stopServerController(a *ServerController) {
|
||||
a.stopTask()
|
||||
type ServerService struct {
|
||||
xrayService XrayService
|
||||
}
|
||||
|
||||
type ServerController struct {
|
||||
BaseController
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
lastStatus *Status
|
||||
lastRefreshTime time.Time
|
||||
lastGetStatusTime time.Time
|
||||
}
|
||||
|
||||
func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a := &ServerController{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
lastGetStatusTime: time.Now(),
|
||||
}
|
||||
a.initRouter(g)
|
||||
go a.runTask()
|
||||
runtime.SetFinalizer(a, stopServerController)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/server/status", a.status)
|
||||
g.POST("/server/getXrayVersion", a.getXrayVersion)
|
||||
}
|
||||
|
||||
func (a *ServerController) refreshStatus() {
|
||||
status := &Status{}
|
||||
|
||||
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
now := time.Now()
|
||||
status := &Status{
|
||||
T: now,
|
||||
}
|
||||
|
||||
percents, err := cpu.Percent(time.Second*2, false)
|
||||
if err != nil {
|
||||
@@ -153,11 +130,11 @@ func (a *ServerController) refreshStatus() {
|
||||
status.NetTraffic.Sent = ioStat.BytesSent
|
||||
status.NetTraffic.Recv = ioStat.BytesRecv
|
||||
|
||||
if a.lastStatus != nil {
|
||||
duration := now.Sub(a.lastRefreshTime)
|
||||
if lastStatus != nil {
|
||||
duration := now.Sub(lastStatus.T)
|
||||
seconds := float64(duration) / float64(time.Second)
|
||||
up := uint64(float64(status.NetTraffic.Sent-a.lastStatus.NetTraffic.Sent) / seconds)
|
||||
down := uint64(float64(status.NetTraffic.Recv-a.lastStatus.NetTraffic.Recv) / seconds)
|
||||
up := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds)
|
||||
down := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds)
|
||||
status.NetIO.Up = up
|
||||
status.NetIO.Down = down
|
||||
}
|
||||
@@ -179,55 +156,28 @@ func (a *ServerController) refreshStatus() {
|
||||
status.UdpCount = len(udpConnStats)
|
||||
}
|
||||
|
||||
// TODO 临时
|
||||
status.Xray.State = Running
|
||||
status.Xray.ErrorMsg = ""
|
||||
status.Xray.Version = "1.0.0"
|
||||
|
||||
a.lastStatus = status
|
||||
a.lastRefreshTime = now
|
||||
}
|
||||
|
||||
func (a *ServerController) runTask() {
|
||||
for {
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
break
|
||||
default:
|
||||
if s.xrayService.IsXrayRunning() {
|
||||
status.Xray.State = Running
|
||||
status.Xray.ErrorMsg = ""
|
||||
} else {
|
||||
err := s.xrayService.GetXrayErr()
|
||||
if err != nil {
|
||||
status.Xray.State = Error
|
||||
} else {
|
||||
status.Xray.State = Stop
|
||||
}
|
||||
now := time.Now()
|
||||
if now.Sub(a.lastGetStatusTime) > time.Minute*3 {
|
||||
time.Sleep(time.Second * 2)
|
||||
continue
|
||||
}
|
||||
a.refreshStatus()
|
||||
status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
|
||||
}
|
||||
status.Xray.Version = s.xrayService.GetXrayVersion()
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func (a *ServerController) stopTask() {
|
||||
a.cancel()
|
||||
}
|
||||
|
||||
func (a *ServerController) status(c *gin.Context) {
|
||||
a.lastGetStatusTime = time.Now()
|
||||
|
||||
jsonMsgObj(c, "", a.lastStatus, nil)
|
||||
}
|
||||
|
||||
var lastVersions []string
|
||||
var lastGetReleaseTime time.Time
|
||||
|
||||
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
now := time.Now()
|
||||
if now.Sub(lastGetReleaseTime) <= time.Minute {
|
||||
jsonMsgObj(c, "", lastVersions, nil)
|
||||
return
|
||||
}
|
||||
func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
url := "https://api.github.com/repos/XTLS/Xray-core/releases"
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
jsonMsg(c, "获取版本失败,请稍后尝试", err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
@@ -235,22 +185,115 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
buffer.Reset()
|
||||
_, err = buffer.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
jsonMsg(c, "获取版本失败,请稍后尝试", err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
releases := make([]Release, 0)
|
||||
err = json.Unmarshal(buffer.Bytes(), &releases)
|
||||
if err != nil {
|
||||
jsonMsg(c, "获取版本失败,请向作者反馈此问题", err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
versions := make([]string, 0, len(releases))
|
||||
for _, release := range releases {
|
||||
versions = append(versions, release.TagName)
|
||||
}
|
||||
lastVersions = versions
|
||||
lastGetReleaseTime = time.Now()
|
||||
|
||||
jsonMsgObj(c, "", versions, nil)
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) downloadXRay(version string) (string, error) {
|
||||
osName := runtime.GOOS
|
||||
arch := runtime.GOARCH
|
||||
|
||||
switch osName {
|
||||
case "darwin":
|
||||
osName = "macos"
|
||||
}
|
||||
|
||||
switch arch {
|
||||
case "amd64":
|
||||
arch = "64"
|
||||
case "arm64":
|
||||
arch = "arm64-v8a"
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
|
||||
url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
os.Remove(fileName)
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fileName, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) UpdateXray(version string) error {
|
||||
zipFileName, err := s.downloadXRay(version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zipFile, err := os.Open(zipFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
zipFile.Close()
|
||||
os.Remove(zipFileName)
|
||||
}()
|
||||
|
||||
stat, err := zipFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := zip.NewReader(zipFile, stat.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.xrayService.StopXray()
|
||||
defer s.xrayService.StartXray()
|
||||
|
||||
copyZipFile := func(zipName string, fileName string) error {
|
||||
zipFile, err := reader.Open(zipName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Remove(fileName)
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(file, zipFile)
|
||||
return err
|
||||
}
|
||||
|
||||
err = copyZipFile("xray", xray.GetBinaryPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyZipFile("geosite.dat", xray.GetGeositePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyZipFile("geoip.dat", xray.GetGeoipPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
112
web/service/setting.go
Normal file
112
web/service/setting.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strconv"
|
||||
"strings"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/random"
|
||||
)
|
||||
|
||||
//go:embed config.json
|
||||
var xrayTemplateConfig string
|
||||
|
||||
type SettingService struct {
|
||||
}
|
||||
|
||||
func (s *SettingService) ClearSetting() error {
|
||||
db := database.GetDB()
|
||||
return db.Delete(model.Setting{}).Error
|
||||
}
|
||||
|
||||
func (s *SettingService) getSetting(key string) (*model.Setting, error) {
|
||||
db := database.GetDB()
|
||||
setting := &model.Setting{}
|
||||
err := db.Model(model.Setting{}).Where("key = ?", key).First(setting).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) saveSetting(key string, value string) error {
|
||||
setting, err := s.getSetting(key)
|
||||
db := database.GetDB()
|
||||
if database.IsNotFound(err) {
|
||||
return db.Create(&model.Setting{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}).Error
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
setting.Key = key
|
||||
setting.Value = value
|
||||
return db.Save(setting).Error
|
||||
}
|
||||
|
||||
func (s *SettingService) getString(key string, defaultValue string) (string, error) {
|
||||
setting, err := s.getSetting(key)
|
||||
if database.IsNotFound(err) {
|
||||
return defaultValue, nil
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) getInt(key string, defaultValue int) (int, error) {
|
||||
str, err := s.getString(key, strconv.Itoa(defaultValue))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(str)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetXrayConfigTemplate() (string, error) {
|
||||
return s.getString("xray_template_config", xrayTemplateConfig)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetListen() (string, error) {
|
||||
return s.getString("web_listen", "")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetPort() (int, error) {
|
||||
return s.getInt("web_port", 65432)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetCertFile() (string, error) {
|
||||
return s.getString("web_cert_file", "")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetKeyFile() (string, error) {
|
||||
return s.getString("web_key_file", "")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
seq := random.Seq(32)
|
||||
secret, err := s.getString("secret", seq)
|
||||
if secret == seq {
|
||||
err := s.saveSetting("secret", secret)
|
||||
if err != nil {
|
||||
logger.Warning("save secret failed:", err)
|
||||
}
|
||||
}
|
||||
return []byte(secret), err
|
||||
}
|
||||
|
||||
func (s *SettingService) GetBasePath() (string, error) {
|
||||
basePath, err := s.getString("web_base_path", "/")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !strings.HasPrefix(basePath, "/") {
|
||||
basePath = "/" + basePath
|
||||
}
|
||||
if !strings.HasSuffix(basePath, "/") {
|
||||
basePath += "/"
|
||||
}
|
||||
return basePath, nil
|
||||
}
|
||||
100
web/service/xray.go
Normal file
100
web/service/xray.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"x-ui/util/common"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
var p *xray.Process
|
||||
var result string
|
||||
|
||||
type XrayService struct {
|
||||
inboundService InboundService
|
||||
settingService SettingService
|
||||
}
|
||||
|
||||
func (s *XrayService) IsXrayRunning() bool {
|
||||
return p != nil && p.IsRunning()
|
||||
}
|
||||
|
||||
func (s *XrayService) GetXrayErr() error {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p.GetErr()
|
||||
}
|
||||
|
||||
func (s *XrayService) GetXrayResult() string {
|
||||
if result != "" {
|
||||
return result
|
||||
}
|
||||
if s.IsXrayRunning() {
|
||||
return ""
|
||||
}
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
result = p.GetResult()
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *XrayService) GetXrayVersion() string {
|
||||
if p == nil {
|
||||
return "Unknown"
|
||||
}
|
||||
return p.GetVersion()
|
||||
}
|
||||
|
||||
func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
templateConfig, err := s.settingService.GetXrayConfigTemplate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
xrayConfig := &xray.Config{}
|
||||
err = json.Unmarshal([]byte(templateConfig), xrayConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inbounds, err := s.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
inboundConfig := inbound.GenXrayInboundConfig()
|
||||
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
|
||||
}
|
||||
return xrayConfig, nil
|
||||
}
|
||||
|
||||
func (s *XrayService) StartXray() error {
|
||||
if s.IsXrayRunning() {
|
||||
return nil
|
||||
}
|
||||
|
||||
xrayConfig, err := s.GetXrayConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p = xray.NewProcess(xrayConfig)
|
||||
err = p.Start()
|
||||
result = ""
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *XrayService) StopXray() error {
|
||||
if s.IsXrayRunning() {
|
||||
return p.Stop()
|
||||
}
|
||||
return errors.New("xray is not running")
|
||||
}
|
||||
|
||||
func (s *XrayService) RestartXray() error {
|
||||
err1 := s.StopXray()
|
||||
err2 := s.StartXray()
|
||||
return common.Combine(err1, err2)
|
||||
}
|
||||
@@ -1,3 +1,12 @@
|
||||
"username" = "username"
|
||||
"password" = "password"
|
||||
"login" = "login"
|
||||
"login" = "login"
|
||||
"confirm" = "confirm"
|
||||
"cancel" = "cancel"
|
||||
"close" = "close"
|
||||
"copy" = "copy"
|
||||
"copied" = "copied"
|
||||
"download" = "download"
|
||||
"remark" = "remark"
|
||||
"enable" = "enable"
|
||||
"protocol" = "protocol"
|
||||
@@ -1,3 +1,12 @@
|
||||
"username" = "用户名"
|
||||
"password" = "密码"
|
||||
"login" = "登录"
|
||||
"login" = "登录"
|
||||
"confirm" = "confirm"
|
||||
"cancel" = "cancel"
|
||||
"close" = "close"
|
||||
"copy" = "copy"
|
||||
"copied" = "copied"
|
||||
"download" = "download"
|
||||
"remark" = "remark"
|
||||
"enable" = "enable"
|
||||
"protocol" = "protocol"
|
||||
@@ -1,3 +1,12 @@
|
||||
"username" = "用戶名"
|
||||
"password" = "密碼"
|
||||
"login" = "登錄"
|
||||
"login" = "登錄"
|
||||
"confirm" = "confirm"
|
||||
"cancel" = "cancel"
|
||||
"close" = "close"
|
||||
"copy" = "copy"
|
||||
"copied" = "copied"
|
||||
"download" = "download"
|
||||
"remark" = "remark"
|
||||
"enable" = "enable"
|
||||
"protocol" = "protocol"
|
||||
118
web/web.go
118
web/web.go
@@ -15,10 +15,14 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/controller"
|
||||
"x-ui/web/service"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
@@ -38,22 +42,43 @@ func (f *wrapAssetsFS) Open(name string) (fs.File, error) {
|
||||
return f.FS.Open("assets/" + name)
|
||||
}
|
||||
|
||||
func stopServer(s *Server) {
|
||||
s.Stop()
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
*server
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
s := &Server{newServer()}
|
||||
runtime.SetFinalizer(s, stopServer)
|
||||
return s
|
||||
}
|
||||
|
||||
type server struct {
|
||||
listener net.Listener
|
||||
|
||||
index *controller.IndexController
|
||||
server *controller.ServerController
|
||||
xui *controller.XUIController
|
||||
|
||||
xrayService service.XrayService
|
||||
settingService service.SettingService
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return new(Server)
|
||||
func newServer() *server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &server{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
func (s *server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
@@ -64,9 +89,22 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
store := cookie.NewStore(config.GetSecret())
|
||||
secret, err := s.settingService.GetSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
basePath, err := s.settingService.GetBasePath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store := cookie.NewStore(secret)
|
||||
engine.Use(sessions.Sessions("session", store))
|
||||
err := s.initI18n(engine)
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set("base_path", basePath)
|
||||
})
|
||||
err = s.initI18n(engine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,7 +112,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
// for develop
|
||||
engine.LoadHTMLGlob("web/html/**/*.html")
|
||||
engine.StaticFS(config.GetBasePath()+"assets", http.FS(os.DirFS("web/assets")))
|
||||
engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
|
||||
} else {
|
||||
t := template.New("")
|
||||
t, err = t.ParseFS(htmlFS, "html/**/*.html")
|
||||
@@ -82,10 +120,10 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
return nil, err
|
||||
}
|
||||
engine.SetHTMLTemplate(t)
|
||||
engine.StaticFS(config.GetBasePath()+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
|
||||
engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS}))
|
||||
}
|
||||
|
||||
g := engine.Group(config.GetBasePath())
|
||||
g := engine.Group(basePath)
|
||||
|
||||
s.index = controller.NewIndexController(g)
|
||||
s.server = controller.NewServerController(g)
|
||||
@@ -94,7 +132,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (s *Server) initI18n(engine *gin.Engine) error {
|
||||
func (s *server) initI18n(engine *gin.Engine) error {
|
||||
bundle := i18n.NewBundle(language.SimplifiedChinese)
|
||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error {
|
||||
@@ -163,18 +201,66 @@ func (s *Server) initI18n(engine *gin.Engine) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Run() error {
|
||||
func (s *server) startTask() {
|
||||
go func() {
|
||||
err := s.xrayService.StartXray()
|
||||
if err != nil {
|
||||
logger.Warning("start xray failed:", err)
|
||||
}
|
||||
ticker := time.NewTicker(time.Second * 30)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
if s.xrayService.IsXrayRunning() {
|
||||
continue
|
||||
}
|
||||
err := s.xrayService.StartXray()
|
||||
if err != nil {
|
||||
logger.Warning("start xray failed:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *server) Run() error {
|
||||
engine, err := s.initRouter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certFile := config.GetCertFile()
|
||||
keyFile := config.GetKeyFile()
|
||||
|
||||
s.startTask()
|
||||
|
||||
certFile, err := s.settingService.GetCertFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyFile, err := s.settingService.GetKeyFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listen, err := s.settingService.GetListen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port, err := s.settingService.GetPort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info("web server run https on", config.GetListen())
|
||||
return engine.RunTLS(config.GetListen(), certFile, keyFile)
|
||||
logger.Info("web server run https on", listenAddr)
|
||||
return engine.RunTLS(listenAddr, certFile, keyFile)
|
||||
} else {
|
||||
logger.Info("web server run http on", config.GetListen())
|
||||
return engine.Run(config.GetListen())
|
||||
logger.Info("web server run http on", listenAddr)
|
||||
return engine.Run(listenAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
s.cancel()
|
||||
return s.listener.Close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user