diff --git a/web/go.mod b/web/go.mod
index 3af0670..f7668a9 100644
--- a/web/go.mod
+++ b/web/go.mod
@@ -10,12 +10,16 @@ require (
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/gin-contrib/sessions v1.0.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
+ github.com/gorilla/context v1.1.2 // indirect
+ github.com/gorilla/securecookie v1.1.2 // indirect
+ github.com/gorilla/sessions v1.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@@ -26,10 +30,10 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
- golang.org/x/crypto v0.23.0 // indirect
- golang.org/x/net v0.25.0 // indirect
- golang.org/x/sys v0.20.0 // indirect
- golang.org/x/text v0.15.0 // indirect
+ golang.org/x/crypto v0.31.0 // indirect
+ golang.org/x/net v0.33.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+ golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/web/go.sum b/web/go.sum
index e6bbf9f..e17e764 100644
--- a/web/go.sum
+++ b/web/go.sum
@@ -12,6 +12,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
+github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@@ -25,6 +27,12 @@ github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaC
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
+github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
+github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -63,14 +71,22 @@ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/web/main.go b/web/main.go
index f125c3f..78164b8 100644
--- a/web/main.go
+++ b/web/main.go
@@ -171,7 +171,7 @@ func main() {
session := sessions.Default(c)
session.Set("user", true)
session.Options(sessions.Options{
- MaxAge: 3600 * 24, // 24小时
+ MaxAge: 3600 * 2, // 2小时
})
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Session保存失败"})
@@ -266,7 +266,15 @@ func main() {
c.JSON(200, gin.H{"message": "服务停止成功"})
})
-
+ authorized.POST("/restart_service", func(c *gin.Context) {
+ cmd := exec.Command("systemctl", "restart", "realm")
+ if err := cmd.Run(); err != nil {
+ c.JSON(500, gin.H{"error": "服务重启失败"})
+ return
+ }
+
+ c.JSON(200, gin.H{"message": "服务重启成功"})
+ })
authorized.GET("/check_status", func(c *gin.Context) {
cmd := exec.Command("systemctl", "is-active", "--quiet", "realm")
err := cmd.Run()
diff --git a/web/static/app.js b/web/static/app.js
index bd200f7..9fe905d 100644
--- a/web/static/app.js
+++ b/web/static/app.js
@@ -2,71 +2,147 @@ document.addEventListener('DOMContentLoaded', () => {
const outputDiv = document.getElementById('output');
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
+ const restartButton = document.getElementById('restartButton');
const addRuleButton = document.getElementById('addRuleButton');
+ const addBatchRulesButton = document.getElementById('addBatchRulesButton');
+ const logoutButton = document.getElementById('logoutButton');
const localPortInput = document.getElementById('localPort');
const remoteIPInput = document.getElementById('remoteIP');
const remotePortInput = document.getElementById('remotePort');
+ const rulesInput = document.getElementById('rulesInput');
- async function fetchForwardingRules() {
+ // 更新服务状态
+ async function updateServiceStatus() {
try {
- const response = await fetch('/get_rules');
+ const response = await fetch('/check_status');
if (!response.ok) {
- throw new Error('获取规则失败:' + response.statusText);
+ throw new Error('检查状态失败:' + response.statusText);
+ }
+ const data = await response.json();
+ const statusElement = document.getElementById('serviceStatus');
+
+ if (data.status === "启用") {
+ statusElement.textContent = "运行中";
+ statusElement.className = 'status-tag running';
+ } else {
+ statusElement.textContent = "已停止";
+ statusElement.className = 'status-tag stopped';
}
- const rules = await response.json();
- const tbody = document.querySelector('#forwardingTable tbody');
- tbody.innerHTML = '';
-
- rules.forEach((rule, index) => {
- const [, localPort] = rule.Listen.split(':');
- const lastColonIndex = rule.Remote.lastIndexOf(':');
- const remoteIP = rule.Remote.substring(0, lastColonIndex);
- const remotePort = rule.Remote.substring(lastColonIndex + 1);
-
- const row = document.createElement('tr');
- row.innerHTML = `
-
${index + 1} |
- ${localPort} |
- ${remoteIP} |
- ${remotePort} |
- |
- `;
- tbody.appendChild(row);
- });
-
- // 为删除按钮添加事件监听
- const deleteButtons = document.querySelectorAll('.delete-btn');
- deleteButtons.forEach(button => {
- button.addEventListener('click', function() {
- const listenAddress = this.getAttribute('data-listen');
- deleteRule(listenAddress);
- });
- });
} catch (error) {
- console.error('请求失败:', error);
- outputDiv.textContent = '获取转发规则失败';
+ console.error('状态检查失败:', error);
+ const statusElement = document.getElementById('serviceStatus');
+ statusElement.textContent = "未知";
+ statusElement.className = 'status-tag stopped';
}
}
+ // 获取转发规则
+ async function fetchForwardingRules() {
+ try {
+ const response = await fetch('/get_rules', {
+ method: 'GET',
+ headers: {
+ 'Cache-Control': 'no-cache',
+ 'Pragma': 'no-cache'
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('获取规则失败:' + response.statusText);
+ }
+
+ const rules = await response.json();
+ const tbody = document.querySelector('#forwardingTable tbody');
+ tbody.innerHTML = '';
+
+ // 创建当前规则的端口占用列表
+ const usedPorts = new Set();
+
+ if (!Array.isArray(rules)) {
+ throw new Error('服务器返回的数据格式不正确');
+ }
+
+ rules.forEach(rule => {
+ // 检查规则格式并统一属性名
+ const listen = rule.Listen || rule.listen;
+ const remote = rule.Remote || rule.remote;
+
+ if (!listen || !remote) {
+ console.error('规则格式错误:', rule);
+ return;
+ }
+
+ const localPort = listen.split(':')[1];
+ usedPorts.add(localPort);
+
+ const lastColonIndex = remote.lastIndexOf(':');
+ const remoteIP = remote.substring(0, lastColonIndex);
+ const remotePort = remote.substring(lastColonIndex + 1);
+
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ ${tbody.children.length + 1} |
+ ${localPort} |
+ ${remoteIP} |
+ ${remotePort} |
+ |
+ `;
+ tbody.appendChild(row);
+ });
+
+ // 为删除按钮添加事件监听
+ document.querySelectorAll('.delete-btn').forEach(button => {
+ button.addEventListener('click', function() {
+ deleteRule(this.getAttribute('data-listen'));
+ });
+ });
+
+ return usedPorts;
+ } catch (error) {
+ console.error('获取规则失败:', error);
+ outputDiv.textContent = `获取转发规则失败: ${error.message}`;
+ // 添加更详细的错误信息输出
+ if (error.response) {
+ console.error('Response status:', error.response.status);
+ console.error('Response text:', await error.response.text());
+ }
+ return new Set();
+ }
+ }
+
+ // 删除规则
async function deleteRule(listenAddress) {
try {
const response = await fetch(`/delete_rule?listen=${encodeURIComponent(listenAddress)}`, {
method: 'DELETE'
});
+
if (!response.ok) {
throw new Error('删除规则失败:' + response.statusText);
}
- fetchForwardingRules(); // 重新获取规则列表
+
+ // 删除成功后重启服务
+ const restartResponse = await fetch('/restart_service', {
+ method: 'POST'
+ });
+ if (!restartResponse.ok) {
+ throw new Error('重启服务失败:' + restartResponse.statusText);
+ }
+
+ outputDiv.textContent = '规则已删除,服务已重启';
+ await fetchForwardingRules();
+ await updateServiceStatus();
} catch (error) {
- console.error('删除规则失败:', error);
- outputDiv.textContent = '删除规则失败';
+ console.error('删除失败:', error);
+ outputDiv.textContent = error.message;
}
}
+ // 添加单个规则
async function addRule() {
- const localPort = localPortInput.value;
- const remoteIP = remoteIPInput.value;
- const remotePort = remotePortInput.value;
+ const localPort = localPortInput.value.trim();
+ const remoteIP = remoteIPInput.value.trim();
+ const remotePort = remotePortInput.value.trim();
if (!localPort || !remoteIP || !remotePort) {
outputDiv.textContent = '请填写所有字段';
@@ -74,27 +150,123 @@ document.addEventListener('DOMContentLoaded', () => {
}
try {
+ const usedPorts = await fetchForwardingRules();
+ if (usedPorts.has(localPort)) {
+ outputDiv.textContent = `端口 ${localPort} 已被占用`;
+ return;
+ }
+
const response = await fetch('/add_rule', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
- localPort,
- remoteIP,
- remotePort
+ listen: `0.0.0.0:${localPort}`,
+ remote: `${remoteIP}:${remotePort}`
})
});
+
if (!response.ok) {
throw new Error('添加规则失败:' + response.statusText);
}
- fetchForwardingRules(); // 重新获取规则列表
+
+ // 添加成功后重启服务
+ const restartResponse = await fetch('/restart_service', {
+ method: 'POST'
+ });
+ if (!restartResponse.ok) {
+ throw new Error('重启服务失败:' + restartResponse.statusText);
+ }
+
+ outputDiv.textContent = '规则添加成功,服务已重启';
+ localPortInput.value = '';
+ remoteIPInput.value = '';
+ remotePortInput.value = '';
+ await fetchForwardingRules();
+ await updateServiceStatus();
} catch (error) {
- console.error('添加规则失败:', error);
- outputDiv.textContent = '添加规则失败';
+ console.error('添加失败:', error);
+ outputDiv.textContent = error.message;
}
}
+ // 批量添加规则
+ async function addBatchRules() {
+ const rules = rulesInput.value.trim().split('\n').filter(Boolean);
+ if (rules.length === 0) {
+ outputDiv.textContent = '请输入要添加的规则';
+ return;
+ }
+
+ const usedPorts = await fetchForwardingRules();
+ const failedRules = [];
+ let hasSuccess = false;
+
+ for (const rule of rules) {
+ const match = rule.match(/^(\d+):(\[.*?\]:\d+|\S+)$/);
+ if (!match) {
+ failedRules.push(`格式错误: ${rule}`);
+ continue;
+ }
+
+ const localPort = match[1];
+ const remoteAddress = match[2];
+
+ if (usedPorts.has(localPort)) {
+ failedRules.push(`端口 ${localPort} 已被占用`);
+ continue;
+ }
+
+ try {
+ const response = await fetch('/add_rule', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ listen: `0.0.0.0:${localPort}`,
+ remote: remoteAddress
+ })
+ });
+
+ if (!response.ok) {
+ failedRules.push(`添加失败: ${rule}`);
+ continue;
+ }
+
+ usedPorts.add(localPort);
+ hasSuccess = true;
+ } catch (error) {
+ failedRules.push(`添加失败: ${rule} - ${error.message}`);
+ }
+ }
+
+ if (hasSuccess) {
+ try {
+ const restartResponse = await fetch('/restart_service', {
+ method: 'POST'
+ });
+ if (!restartResponse.ok) {
+ throw new Error('重启服务失败');
+ }
+ } catch (error) {
+ failedRules.push('服务重启失败');
+ }
+ }
+
+ rulesInput.value = '';
+ await fetchForwardingRules();
+ await updateServiceStatus();
+
+ if (failedRules.length > 0) {
+ outputDiv.textContent = `添加完成。\n失败的规则:\n${failedRules.join('\n')}`;
+ } else {
+ outputDiv.textContent = '所有规则添加成功,服务已重启';
+ }
+ }
+
+ // 事件监听器
startButton.addEventListener('click', async () => {
try {
const response = await fetch('/start_service', {
@@ -103,11 +275,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (!response.ok) {
throw new Error('启动服务失败:' + response.statusText);
}
- const result = await response.json();
- outputDiv.textContent = result.output;
+ outputDiv.textContent = '服务启动成功';
+ await updateServiceStatus();
} catch (error) {
- console.error('启动服务失败:', error);
- outputDiv.textContent = '启动服务失败';
+ console.error('启动失败:', error);
+ outputDiv.textContent = error.message;
}
});
@@ -119,16 +291,53 @@ document.addEventListener('DOMContentLoaded', () => {
if (!response.ok) {
throw new Error('停止服务失败:' + response.statusText);
}
- const result = await response.json();
- outputDiv.textContent = result.output;
+ outputDiv.textContent = '服务停止成功';
+ await updateServiceStatus();
} catch (error) {
- console.error('停止服务失败:', error);
- outputDiv.textContent = '停止服务失败';
+ console.error('停止失败:', error);
+ outputDiv.textContent = error.message;
+ }
+ });
+
+ restartButton.addEventListener('click', async () => {
+ try {
+ const response = await fetch('/restart_service', {
+ method: 'POST'
+ });
+ if (!response.ok) {
+ throw new Error('重启服务失败:' + response.statusText);
+ }
+ outputDiv.textContent = '服务重启成功';
+ await updateServiceStatus();
+ } catch (error) {
+ console.error('重启失败:', error);
+ outputDiv.textContent = error.message;
+ }
+ });
+
+ logoutButton.addEventListener('click', async () => {
+ try {
+ const response = await fetch('/logout', {
+ method: 'POST'
+ });
+ if (response.ok) {
+ window.location.href = '/login';
+ } else {
+ throw new Error('登出失败:' + response.statusText);
+ }
+ } catch (error) {
+ console.error('登出失败:', error);
+ outputDiv.textContent = error.message;
}
});
addRuleButton.addEventListener('click', addRule);
+ addBatchRulesButton.addEventListener('click', addBatchRules);
- // 初始化时获取规则列表
+ // 初始化
fetchForwardingRules();
-});
+ updateServiceStatus();
+
+ // 定期更新服务状态
+ setInterval(updateServiceStatus, 15000);
+});
\ No newline at end of file
diff --git a/web/templates/index.html b/web/templates/index.html
index 3cfa20b..43fa04a 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -36,6 +36,12 @@
button:hover {
background-color: #218838;
}
+ .delete-btn {
+ background-color: #dc3545;
+ }
+ .delete-btn:hover {
+ background-color: #c82333;
+ }
table {
width: 100%;
border-collapse: collapse;
@@ -56,7 +62,7 @@
display: block;
margin-bottom: 5px;
}
- .form-group input {
+ .form-group input, .form-group textarea {
width: calc(100% - 22px);
padding: 10px;
border: 1px solid #ccc;
@@ -64,12 +70,12 @@
}
#output {
margin-top: 20px;
- background-color: #fff;
+ background-color: #f8f9fa;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
min-height: 50px;
- white-space: pre-wrap; /* 保持空格和换行 */
+ white-space: pre-wrap;
}
.status-tag {
display: inline-flex;
@@ -81,292 +87,94 @@
margin-left: 10px;
font-weight: 500;
}
-
.status-tag.running {
background-color: #52c41a;
color: white;
}
-
.status-tag.stopped {
background-color: #ff4d4f;
color: white;
}
-
.status-wrapper {
display: inline-flex;
align-items: center;
margin-left: 15px;
}
-
.status-label {
color: #666;
margin-right: 8px;
}
-
.button-group {
display: flex;
align-items: center;
margin-bottom: 20px;
}
+ #logoutButton {
+ background-color: #6c757d;
+ }
+ #logoutButton:hover {
+ background-color: #5a6268;
+ }
+
+
Realm 转发管理面板
-
-
Realm 转发管理面板
-
-
-
-
-
+
-