完成xray启动

This commit is contained in:
sprov
2021-05-27 23:04:39 +08:00
parent 56ed8f355c
commit 3cd25ce5ea
42 changed files with 3627 additions and 229 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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() {

View File

@@ -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;
}
}

View File

@@ -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
View 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)
}

View File

@@ -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
View 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)
}

View File

@@ -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) {
}

View File

@@ -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;

View File

@@ -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="取消">

View 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}}

View 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}}

View File

@@ -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>

View 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">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>
<a-select-option value="wechat-video">wechat-videocamouflage WeChat video</a-select-option>
<a-select-option value="dtls">dtlscamouflage DTLS 1.2 packages</a-select-option>
<a-select-option value="wireguard">wireguardcamouflage 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">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>
<a-select-option value="wechat-video">wechat-videocamouflage WeChat video</a-select-option>
<a-select-option value="dtls">dtlscamouflage DTLS 1.2 packages</a-select-option>
<a-select-option value="wireguard">wireguardcamouflage 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
View 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>

View File

@@ -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
View 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
View 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
}

View File

@@ -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
View 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
View 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)
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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()
}