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 转发管理面板

- -
- - - -
- 状态: - 检查中... +
+ + + + +
+ 状态: + 检查中... +
+ +
+ +

当前转发规则

+ + + + + + + + + + + + + +
序号中转端口落地机 IP目标端口操作
+ +

添加转发规则

+
+ + +
+
+ + +
+
+ + +
+ + +

批量添加转发规则

+
+ +

IPv4 格式:中转端口:落地机IP:目标端口

+

IPv6 格式:中转端口:[落地机IPv6]:目标端口

+ +
+
-
- -

当前转发规则

- - - - - - - - - - - - - -
序号中转端口落地鸡 IP目标端口操作
- -

添加转发规则

-
- - -
-
- - -
-
- - -
- - -
- - - + - - + \ No newline at end of file diff --git a/web/templates/login.html b/web/templates/login.html index 98734bf..c5b0f66 100644 --- a/web/templates/login.html +++ b/web/templates/login.html @@ -38,10 +38,11 @@ } .form-group input { width: 100%; - padding: 10px; + padding: 12px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; + font-size: 16px; } .password-input-wrapper { position: relative; @@ -55,6 +56,8 @@ border: none; cursor: pointer; color: #666; + padding: 5px; + font-size: 20px; } button[type="submit"] { width: 100%; @@ -65,14 +68,35 @@ border-radius: 4px; cursor: pointer; font-size: 16px; + transition: background-color 0.3s; } button[type="submit"]:hover { background-color: #218838; } .error-message { color: #dc3545; - margin-top: 10px; + margin-top: 15px; text-align: center; + min-height: 20px; + font-size: 14px; + } + .loading { + opacity: 0.7; + cursor: not-allowed; + } + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + .loading-spinner { + display: none; + width: 20px; + height: 20px; + border: 3px solid #f3f3f3; + border-top: 3px solid #28a745; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 10px auto; } @@ -82,30 +106,50 @@
- - + +
- + +
- - + \ No newline at end of file