Browse Source

Finish!!!

xcbosa-itx 2 years ago
parent
commit
7a95270813

BIN
.DS_Store


+ 1 - 1
CMakeLists.txt

@@ -37,7 +37,7 @@ add_subdirectory(utils)
 add_subdirectory(httpserver)
 add_subdirectory(processor)
 
-add_executable(FRPCWebUI ${DIR_SRCS} ${DIR_CONTROLLERS} ${DIR_CONTROLLERS_AUTOGENERATED} ${DIR_VIEW})
+add_executable(FRPCWebUI ${DIR_SRCS} ${DIR_CONTROLLERS} ${DIR_CONTROLLERS_AUTOGENERATED} ${DIR_VIEW} controller/PortListController.cpp)
 
 find_package(Threads)
 target_link_libraries(FRPCWebUI PRIVATE HttpServerUtils HttpServer Processor ${CMAKE_THREAD_LIBS_INIT})

+ 4 - 3
controller/EntryController.cpp

@@ -17,7 +17,7 @@ namespace xc::controller {
 
     ResponseData *EntryController(RequestData request) {
         bool isUserLogin = user::isLogin(request.getCookie("Token"));
-        auto data = new TemplateResponseData({
+        auto resp = new TemplateResponseData({
             If(isUserLogin, {
                 ContentGeneratorReference("PortListController", request)
             }, {
@@ -25,9 +25,10 @@ namespace xc::controller {
             })
         });
         if (!isUserLogin) {
-            data->addCookie("Token", "");
+            resp->addCookie("Token", "");
         }
-        return data;
+        resp->addCookie("message", "");
+        return resp;
     }
 
     ContentGeneratorDefineS(request.getURLPath() == "/", EntryController(request))

+ 182 - 32
controller/PortListController.cpp

@@ -9,6 +9,9 @@
 #include "../user.hpp"
 #include "../frp.h"
 
+#define RequireLogin(username) \
+string username = user::getTokenUserName(request.getCookie("Token")); if (username.empty()) { return new RedirectResponse("/"); }
+
 using namespace std;
 using namespace xc::processor;
 using namespace xc::processor::templates;
@@ -24,7 +27,7 @@ namespace xc::controller {
     }
 
     ResponseData *PortListController(RequestData request) {
-        string username = user::getTokenUserName(request.getCookie("Token"));
+        RequireLogin(username)
         auto profiles = frp::listUserAvailableProfiles(username);
         int totalPortCnt(0), usedPortCnt(0);
         for (auto profile : profiles) {
@@ -35,11 +38,13 @@ namespace xc::controller {
         if (totalPortCnt == 0) {
             guagePercent = 0;
         }
-        return new TemplateResponseData({
+        auto resp = new TemplateResponseData({
             Framework7Document({
-                a("退出登陆").classAdd("link").onclick("window.location='/quitLogin'"),
-                a("添加端口").classAdd("link").onclick("window.location='/createPort'")
+                a("退出登陆").classAdd("link").onclick("window.location='/quitLogin'")
             }, {
+                If(!request.getCookie("message").empty(), {
+                    OnLoadScript("app.dialog.alert('" + request.getCookie("message") + "')")
+                }),
                 BlockTitleView("总览"),
                 BlockView({
                     h1("端口总览").style("text-align", "center"),
@@ -47,49 +52,194 @@ namespace xc::controller {
                 }),
                 Foreach(profiles, [] (frp::ProfileInfo profile) {
                     return View("", {
-                        BlockTitleView(""), center({
-                        templates::div({
-                            templates::div({
-                                templates::div({
-                                    templates::div({
-                                        profile.getServerAddr(),
-                                        VerticalSpacer(10),
-                                        small("您可用范围: " + to_string(profile.getAllowPortLow()) + " - " + to_string(profile.getAllowPortLow() + profile.getAllowPortCount() - 1)).style("opacity", "0.7"),
-                                        GuageView(to_string(profile.ports.size()) + "/" + to_string(profile.getAllowPortCount()) + "端口",
-                                                  "可在此服务器创建", divide(profile.getAllowPortCount() - profile.ports.size(), profile.getAllowPortCount(), 0), 200)
-                                    }).classAdd("card-header text-color-white display-block"),
-                                    Link("关闭").href("#").classAdd("link card-close card-opened-fade-in color-white").prop("style", "position: absolute; right: 15px; top: 15px")
-                                }).classAdd("bg-class-red").style("height", "300px"),
+                        BlockTitleView("IP地址 " + profile.getServerAddr() + " (端口" + to_string(profile.getAllowPortLow()) + "-" + to_string(profile.getAllowPortLow() + profile.getAllowPortCount() - 1) + ")"),
+                        Foreach(profile.ports, [&profile] (frp::ProfilePortInfo portInfo) {
+                            return View("", {
                                 templates::div({
                                     templates::div({
                                         templates::div({
                                             templates::div({
-                                                templates::div({
-                                                    "This is a simple card with pla"
-                                                }).classAdd("card-content card-content-padding")
-                                            }).classAdd("card")
-                                        }).classAdd("page-content")
-                                    }).classAdd("page")
-                                }).classAdd("card-content-padding")
-                            }).classAdd("card-content")
-                        }).classAdd("card card-expandable")})
+                                                profile.getServerAddr() + ":" + to_string(portInfo.remotePort),
+                                                VerticalSpacer(10),
+                                                small({
+                                                    "转发给: ",
+                                                    portInfo.localIp,
+                                                    ":",
+                                                    to_string(portInfo.localPort)
+                                                }).style("opacity", "0.7")
+                                            }).classAdd("card-header text-color-white display-block"),
+                                            Link("关闭").href("#").classAdd("link card-close card-opened-fade-in color-white").prop("style", "position: absolute; right: 15px; top: 15px")
+                                        }).style("height", "300px").style("background-color", "#123456"),
+                                        templates::div({
+                                            FormView({
+                                                input().name("uuid").value(portInfo.uuid).prop("hidden", "true"),
+                                                FormInputView("address", "内部IP地址", "text", "例如:10.0.0.2", portInfo.localIp),
+                                                FormInputView("port", "内部端口", "number", "例如:22 (SSH服务端口)", to_string(portInfo.localPort)),
+                                                FormCustomInputView("外部端口 (在" + profile.getServerAddr() + "上)", {
+                                                    templates::select({
+                                                        Foreach(profile.getFreeRemotePortsAndAppend(portInfo.remotePort), [&portInfo] (int port) {
+                                                            return If(portInfo.remotePort == port, {
+                                                                View("option", to_string(port)).value(to_string(port)).prop("selected", "selected")
+                                                            }, {
+                                                                View("option", to_string(port)).value(to_string(port))
+                                                            });
+                                                        })
+                                                    }).name("remotePort")
+                                                })
+                                            }).id("modifyForm_" + portInfo.uuid),
+                                            VerticalSpacer(25),
+                                            button("保存端口设置").classAdd("button button-fill").onclick("doModify('" + portInfo.uuid + "')"),
+                                            VerticalSpacer(10),
+                                            button("删除此条").classAdd("button button-fill color-red").onclick("doDelete('" + portInfo.uuid +"')")
+                                        }).classAdd("card-content-padding")
+                                    }).classAdd("card-content")
+                                }).classAdd("card card-expandable"),
+                            });
+                        }),
                     });
-                })
+                }),
+                BlockView({
+                    ButtonView("创建新的端口").classAdd("button-large button-fill").classAdd("sheet-open").prop("data-sheet", ".create-new")
+                }),
+                templates::div({
+                    templates::div({
+                        templates::div({
+                            templates::div({
+                                a("取消").classAdd("link sheet-close")
+                            }).classAdd("left"),
+                            templates::div({
+                                p("创建端口")
+                            }).classAdd("center"),
+                            templates::div({
+                                a("创建").classAdd("link").onclick("doCreate()")
+                            }).classAdd("right")
+                        }).classAdd("toolbar-inner")
+                    }).classAdd("toolbar"),
+                    templates::div({
+                        templates::div({
+                            BlockTitleView("端口信息"),
+                            FormView({
+                                FormInputView("address", "内部IP地址", "text", "例如:10.0.0.2"),
+                                FormInputView("port", "内部端口", "number", "例如:22 (SSH服务端口)"),
+                                FormCustomInputView("转发到", {
+                                    templates::select({
+                                        Foreach(frp::listingAvailableServerAndPortForUser(username), [] (string pair) {
+                                            return View("option", pair);
+                                        })
+                                    }).name("remotePair")
+                                })
+                            }).id("createNewForm")
+                        }).classAdd("page-content")
+                    }).classAdd("sheet-modal-inner")
+                }).classAdd("sheet-modal sheet-modal-push create-new")
             }, {
                 a("2023 © Frp-WebUI by XCBOSA").classAdd("link").onclick("window.open('https://github.com/XCBOSA/frp-webui-500k.git')")
             })
         });
+        return resp;
     }
 
-    ContentGeneratorDefineWithNameS("PortListController", false, PortListController(request))
-
     ResponseData *CreatePortController(RequestData request) {
-        string username = user::getTokenUserName(request.getCookie("Token"));
-        if (!username.empty()) {
-            
+        RequireLogin(username)
+        string arg = request.getURLArgument("v");
+        JsonModel model = json::parse(arg);
+        string localAddress = model["address"];
+        int localPort = to_int(model["port"], -1);
+        string remotePair = model["remotePair"];
+        auto remoteList = split(remotePair, ":");
+        if (remoteList.size() != 2) {
+            return new RedirectResponse("/");
         }
+        string serverIp = remoteList[0];
+        int serverPort = to_int(remoteList[1], -1);
+
+        auto profiles = frp::listUserAvailableProfiles(username);
+        for (auto &profile : profiles) {
+            if (profile.getServerAddr() != serverIp) { continue; }
+            if (!utils::is_in(serverPort, profile.getAllowPortLow(), profile.getAllowPortLow() + profile.getAllowPortCount())) { continue; }
+            auto freePorts = profile.getFreeRemotePorts();
+            if (std::count(freePorts.begin(), freePorts.end(), serverPort)) {
+                profile.addPortInfo(frp::ProfilePortInfo(localAddress, localPort, serverPort));
+                profile.save();
+                auto resp = new RedirectResponse("/");
+                resp->addCookie("message", "添加成功。");
+                return resp;
+            } else {
+                auto resp = new RedirectResponse("/");
+                resp->addCookie("message", "该端口已被占用,可能在您编辑期间,他人占用了这个端口,请重新选择。");
+                return resp;
+            }
+        }
+
+        auto resp = new RedirectResponse("/");
+        resp->addCookie("message", "添加失败,您可能没有权限使用指定服务器和端口号段。");
+        return resp;
+    }
+
+    ResponseData *ChangePortSettings(RequestData request) {
+        RequireLogin(username)
+        string arg = request.getURLArgument("v");
+        JsonModel model = json::parse(arg);
+        string uuid = model["uuid"];
+        int remotePort = to_int(model["remotePort"], -1);
+        string localAddress = model["address"];
+        int localPort = to_int(model["port"], -1);
+
+        auto profiles = frp::listUserAvailableProfiles(username);
+        for (auto &profile : profiles) {
+            for (auto &port : profile.ports) {
+                if (port.uuid == uuid) {
+                    auto allowPortsIncludedMe = profile.getFreeRemotePortsAndAppend(port.remotePort);
+                    if (std::count(allowPortsIncludedMe.begin(), allowPortsIncludedMe.end(), remotePort)) {
+                        port.remotePort = remotePort;
+                        port.localPort = localPort;
+                        port.localIp = localAddress;
+                        profile.save();
+                        auto resp = new RedirectResponse("/");
+                        resp->addCookie("message", "修改成功");
+                        return resp;
+                    } else {
+                        auto resp = new RedirectResponse("/");
+                        resp->addCookie("message", "您指定的远程端口" + to_string(remotePort) + "并不在其所在的配置文件允许的端口范围中,或者正在使用。");
+                        return resp;
+                    }
+                }
+            }
+        }
+
+        auto resp = new RedirectResponse("/");
+        resp->addCookie("message", "找不到您刚刚修改的端口配置,它可能在您编辑的时候被其他人删除了。");
+        return resp;
     }
 
+    ResponseData *RemovePort(RequestData request) {
+        RequireLogin(username)
+
+        string arg = request.getURLArgument("v");
+        JsonModel model = json::parse(arg);
+        string uuid = model["uuid"];
+
+        auto profiles = frp::listUserAvailableProfiles(username);
+        for (auto &profile : profiles) {
+            for (auto port : profile.ports) {
+                if (port.uuid == uuid) {
+                    profile.removePort(port.remotePort);
+                    profile.save();
+                    auto resp = new RedirectResponse("/");
+                    resp->addCookie("message", "删除成功。");
+                    return resp;
+                }
+            }
+        }
+
+        auto resp = new RedirectResponse("/");
+        resp->addCookie("message", "找不到您要删除的端口,它可能已经被删除。");
+        return resp;
+    }
+
+    ContentGeneratorDefineWithNameS("PortListController", false, PortListController(request))
     ContentGeneratorDefineWithNameS("/createPort", request.getURLPath() == "/createPort", CreatePortController(request))
+    ContentGeneratorDefineWithNameS("/changePortSettings", request.getURLPath() == "/changePortSettings", ChangePortSettings(request))
+    ContentGeneratorDefineWithNameS("/removePort", request.getURLPath() == "/removePort", RemovePort(request))
 
 }

+ 1 - 1
controller/auto-generated/framework7-framework7-bundle.min.css.cpp

@@ -1,5 +1,5 @@
 //
-// Created by xcbosa on 2023-01-31
+// Created by xcbosa on 2023-02-06
 //
 
 #include "../../processor/processor.h"

+ 1 - 1
controller/auto-generated/framework7-framework7-bundle.min.js.cpp

@@ -1,5 +1,5 @@
 //
-// Created by xcbosa on 2023-01-31
+// Created by xcbosa on 2023-02-06
 //
 
 #include "../../processor/processor.h"

+ 1 - 1
controller/auto-generated/index.css.cpp

@@ -1,5 +1,5 @@
 //
-// Created by xcbosa on 2023-01-31
+// Created by xcbosa on 2023-02-06
 //
 
 #include "../../processor/processor.h"

+ 49 - 1
controller/auto-generated/index.js.cpp

@@ -1,5 +1,5 @@
 //
-// Created by xcbosa on 2023-01-31
+// Created by xcbosa on 2023-02-06
 //
 
 #include "../../processor/processor.h"
@@ -66,6 +66,54 @@ namespace xc::controller {
             string("    data.password = sha256_digest(data.password + salt)\n") + 
             string("    window.location = \"/login?v=\" + JSON.stringify(data)\n") + 
             string("    return\n") + 
+            string("}\n") + 
+            string("\n") + 
+            string("function isValidIP(ip) {\n") + 
+            string("    let reg = /^(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])\\.(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])\\.(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])\\.(\\d{1,2}|1\\d\\d|2[0-4]\\d|25[0-5])$/\n") + 
+            string("    return reg.test(ip)\n") + 
+            string("}\n") + 
+            string("\n") + 
+            string("function doModify(uuid) {\n") + 
+            string("    let data = app.form.convertToData(\"#modifyForm_\" + uuid)\n") + 
+            string("    if (!isValidIP(data.address)) {\n") + 
+            string("        app.dialog.alert(\"IP地址格式错误,请重新填写。\")\n") + 
+            string("        return\n") + 
+            string("    }\n") + 
+            string("    if (data.port < 0 && data.port >= 65536) {\n") + 
+            string("        app.dialog.alert(\"内部端口号必须在0-65535范围内。\")\n") + 
+            string("        return\n") + 
+            string("    }\n") + 
+            string("    window.location = \"/changePortSettings?v=\" + JSON.stringify(data)\n") + 
+            string("    return\n") + 
+            string("}\n") + 
+            string("\n") + 
+            string("function doCreate() {\n") + 
+            string("    let data = app.form.convertToData(\"#createNewForm\")\n") + 
+            string("    if (data.address == \"\") {\n") + 
+            string("        app.dialog.alert(\"请填写内部IP地址。\")\n") + 
+            string("        return\n") + 
+            string("    }\n") + 
+            string("    if (!isValidIP(data.address)) {\n") + 
+            string("        app.dialog.alert(\"IP地址格式错误,请重新填写。\")\n") + 
+            string("        return\n") + 
+            string("    }\n") + 
+            string("    if (data.port == \"\") {\n") + 
+            string("        app.dialog.alert(\"请填写内部端口。\")\n") + 
+            string("        return\n") + 
+            string("    }\n") + 
+            string("    if (data.port < 0 && data.port >= 65536) {\n") + 
+            string("        app.dialog.alert(\"内部端口号必须在0-65535范围内。\")\n") + 
+            string("        return\n") + 
+            string("    }\n") + 
+            string("    window.location = \"/createPort?v=\" + JSON.stringify(data)\n") + 
+            string("    return\n") + 
+            string("}\n") + 
+            string("\n") + 
+            string("function doDelete(uuid) {\n") + 
+            string("    let req = {\n") + 
+            string("        \"uuid\": uuid\n") + 
+            string("    }\n") + 
+            string("    window.location = \"/removePort?v=\" + JSON.stringify(req)\n") + 
             string("}\n")
         , mimeTypeOfFile(ControllerPath));
     }

+ 1 - 1
controller/auto-generated/sha256.js.cpp

@@ -1,5 +1,5 @@
 //
-// Created by xcbosa on 2023-01-31
+// Created by xcbosa on 2023-02-06
 //
 
 #include "../../processor/processor.h"

BIN
data/.DS_Store


+ 48 - 0
data/html/index.js

@@ -48,3 +48,51 @@ function doLogin(salt) {
     window.location = "/login?v=" + JSON.stringify(data)
     return
 }
+
+function isValidIP(ip) {
+    let reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/
+    return reg.test(ip)
+}
+
+function doModify(uuid) {
+    let data = app.form.convertToData("#modifyForm_" + uuid)
+    if (!isValidIP(data.address)) {
+        app.dialog.alert("IP地址格式错误,请重新填写。")
+        return
+    }
+    if (data.port < 0 && data.port >= 65536) {
+        app.dialog.alert("内部端口号必须在0-65535范围内。")
+        return
+    }
+    window.location = "/changePortSettings?v=" + JSON.stringify(data)
+    return
+}
+
+function doCreate() {
+    let data = app.form.convertToData("#createNewForm")
+    if (data.address == "") {
+        app.dialog.alert("请填写内部IP地址。")
+        return
+    }
+    if (!isValidIP(data.address)) {
+        app.dialog.alert("IP地址格式错误,请重新填写。")
+        return
+    }
+    if (data.port == "") {
+        app.dialog.alert("请填写内部端口。")
+        return
+    }
+    if (data.port < 0 && data.port >= 65536) {
+        app.dialog.alert("内部端口号必须在0-65535范围内。")
+        return
+    }
+    window.location = "/createPort?v=" + JSON.stringify(data)
+    return
+}
+
+function doDelete(uuid) {
+    let req = {
+        "uuid": uuid
+    }
+    window.location = "/removePort?v=" + JSON.stringify(req)
+}

+ 99 - 9
frp.cpp

@@ -11,6 +11,10 @@
 #include <signal.h>
 #include <strstream>
 #include "frp.h"
+#include <iomanip>
+#include <chrono>
+#include <functional>
+#include <random>
 
 using namespace std;
 using namespace xc;
@@ -228,16 +232,30 @@ namespace xc::frp {
         }
     }
 
+    static string create_uuid() {
+        ostringstream stream;
+        auto random_seed = std::chrono::system_clock::now().time_since_epoch().count();
+        std::mt19937 seed_engine(random_seed);
+        std::uniform_int_distribution<std::size_t> random_gen;
+        std::size_t value = random_gen(seed_engine);
+        stream << hex << value;
+        return stream.str();
+    }
+
     ProfilePortInfo::ProfilePortInfo() { }
 
     ProfilePortInfo::ProfilePortInfo(string localIp, int localPort, int remotePort):
-        localIp(localIp), localPort(localPort), remotePort(remotePort) { }
+        localIp(localIp), localPort(localPort), remotePort(remotePort), uuid(create_uuid()) { }
+
+    ProfilePortInfo::ProfilePortInfo(string localIp, int localPort, int remotePort, string uuid):
+        localIp(localIp), localPort(localPort), remotePort(remotePort), uuid(uuid) { }
 
     ProfileInfo::ProfileInfo() { }
 
     ProfileInfo::ProfileInfo(string profileName) {
         this->profileName = profileName;
         INIFile file(conf::getFrpcDir() + "/" + profileName);
+        bool needSave = false;
         for (auto &it : file.data) {
             if (it.getTitle() == "common") {
                 this->serverAddr = it.get("server_addr");
@@ -261,9 +279,15 @@ namespace xc::frp {
                 string localIp = it.get("local_ip");
                 bool success;
                 int localPort = to_int(it.get("local_port"), success);
-                if (!success || is_in(localPort, 0, 65536)) { continue; }
+                if (!success || !is_in(localPort, 0, 65536)) { continue; }
                 int remotePort = to_int(it.get("remote_port"), success);
-                if (!success || is_in(localPort, 0, 65536)) { continue; }
+                if (!success || !is_in(localPort, 0, 65536)) { continue; }
+                string id = it.get("id");
+                if (id.empty()) {
+                    id = create_uuid();
+                    needSave = true;
+                }
+                it.set("id", id);
                 if (type == "tcp" || type == "udp") {
                     bool alreadyHave = false;
                     for (auto v : this->ports) {
@@ -272,10 +296,62 @@ namespace xc::frp {
                         }
                     }
                     if (alreadyHave) { continue; }
-                    this->ports.push_back(ProfilePortInfo(localIp, localPort, remotePort));
+                    this->ports.push_back(ProfilePortInfo(localIp, localPort, remotePort, id));
+                }
+            }
+        }
+        if (needSave) {
+            file.save();
+        }
+    }
+
+    int ProfileInfo::getFirstFreeRemotePort() {
+        set<int> usingPorts;
+        for (auto port : this->ports) {
+            usingPorts.insert(port.remotePort);
+        }
+        for (int i = this->allowPortLow; i < this->allowPortLow + this->allowPortCount; i++) {
+            if (!usingPorts.count(i)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    int ProfileInfo::getFirstFreeRemotePort(int ifNoneThenReturn) {
+        int value = getFirstFreeRemotePort();
+        if (value == -1) return ifNoneThenReturn;
+        return value;
+    }
+
+    vector<int> ProfileInfo::getFreeRemotePorts() {
+        return getFreeRemotePorts(65536);
+    }
+
+    vector<int> ProfileInfo::getFreeRemotePorts(int maxCnt) {
+        set<int> usingPorts;
+        vector<int> res;
+        for (auto port : this->ports) {
+            usingPorts.insert(port.remotePort);
+        }
+        int cnt = 0;
+        for (int i = this->allowPortLow; i < this->allowPortLow + this->allowPortCount; i++) {
+            if (!usingPorts.count(i)) {
+                res.push_back(i);
+                cnt++;
+                if (cnt >= maxCnt) {
+                    break;
                 }
             }
         }
+        return res;
+    }
+
+    vector<int> ProfileInfo::getFreeRemotePortsAndAppend(int me) {
+        vector<int> ports = getFreeRemotePorts();
+        ports.push_back(me);
+        std::sort(ports.begin(), ports.end());
+        return ports;
     }
 
     void ProfileInfo::addPortInfo(ProfilePortInfo portInfo) {
@@ -290,9 +366,12 @@ namespace xc::frp {
     }
 
     void ProfileInfo::removePort(int remotePort) {
-        std::remove_if(this->ports.begin(), this->ports.end(), [&](const auto &item) {
-            return item.remotePort == remotePort;
-        });
+        for (auto ptr = this->ports.begin(); ptr != this->ports.end(); ptr++) {
+            if (ptr->remotePort == remotePort) {
+                this->ports.erase(ptr);
+                return;
+            }
+        }
     }
 
     string ProfileInfo::getServerAddr() { return this->serverAddr; }
@@ -304,9 +383,10 @@ namespace xc::frp {
     void ProfileInfo::save() const {
         string path = conf::getFrpcDir() + "/" + this->profileName;
         INIFile ini(path);
+        ini.data.clear();
         ini.getMust("common")->set("server_addr", this->serverAddr);
         ini.getMust("common")->set("server_port", to_string(this->serverPort));
-        ini.getMust("common")->set("server_token", this->token);
+        ini.getMust("common")->set("token", this->token);
         ini.getMust("common")->set("tls_enable", "true");
         ostringstream usersOss;
         for (auto user : this->users) {
@@ -317,7 +397,6 @@ namespace xc::frp {
         ostringstream oss;
         oss << this->allowPortLow << "," << this->allowPortCount;
         ini.getMust("common")->set("webui_allowServerPorts", oss.str());
-        ini.data.clear();
         for (auto port : this->ports) {
             for (auto type : conf::supportTypes) {
                 ostringstream titleOss;
@@ -356,4 +435,15 @@ namespace xc::frp {
         return profiles;
     }
 
+    vector<string> listingAvailableServerAndPortForUser(string username) {
+        auto profiles = listUserAvailableProfiles(username);
+        vector<string> items;
+        for (auto profile : profiles) {
+            for (auto port : profile.getFreeRemotePorts()) {
+                items.push_back(profile.getServerAddr() + ":" + to_string(port));
+            }
+        }
+        return items;
+    }
+
 }

+ 10 - 1
frp.h

@@ -22,14 +22,18 @@ namespace xc::frp {
     set<int> serverUsingPorts(string serverIp);
     void addProfile(string name, string ip, string port, string token);
 
+    vector<string> listingAvailableServerAndPortForUser(string username);
+
     class ProfilePortInfo {
     public:
         ProfilePortInfo();
         ProfilePortInfo(string localIp, int localPort, int remotePort);
+        ProfilePortInfo(string localIp, int localPort, int remotePort, string uuid);
         string localIp;
         int localPort;
         int remotePort;
-        CONFIGOR_BIND(json::value, ProfilePortInfo, REQUIRED(localIp), REQUIRED(localPort), REQUIRED(remotePort))
+        string uuid;
+        CONFIGOR_BIND(json::value, ProfilePortInfo, REQUIRED(localIp), REQUIRED(localPort), REQUIRED(remotePort), REQUIRED(uuid))
     };
 
     class ProfileInfo {
@@ -45,6 +49,11 @@ namespace xc::frp {
         void addUser(string name);
         void removeUser(string name);
         void save() const;
+        int getFirstFreeRemotePort();
+        int getFirstFreeRemotePort(int ifNoneThenReturn);
+        vector<int> getFreeRemotePorts();
+        vector<int> getFreeRemotePorts(int maxCnt);
+        vector<int> getFreeRemotePortsAndAppend(int me);
         CONFIGOR_BIND(json::value, ProfileInfo,
                       REQUIRED(users),
                       REQUIRED(profileName),

+ 16 - 1
processor/templates/framework7/Framework7Document.hpp

@@ -91,7 +91,7 @@ namespace xc::processor::templates::framework7 {
 
     class FormInputView: public FormItemView {
     public:
-        FormInputView(string keyName, string displayName, string contentType, string placeholder): FormItemView({
+        FormInputView(string keyName, string displayName, string contentType, string placeholder, string value): FormItemView({
             div({
                 div({
                     div(displayName).classAdd("item-title item-label"),
@@ -100,10 +100,25 @@ namespace xc::processor::templates::framework7 {
                             .type(contentType)
                             .name(keyName)
                             .placeholder(placeholder)
+                            .value(value)
                     }).classAdd("item-input-wrap")
                 }).classAdd("item-inner")
             }).classAdd("item-content item-input")
         }) { }
+
+        FormInputView(string keyName, string displayName, string contentType, string placeholder): FormInputView(keyName, displayName, contentType, placeholder, "") { }
+    };
+
+    class FormCustomInputView: public FormItemView {
+    public:
+        FormCustomInputView(string displayName, ViewCollection inner): FormItemView({
+            div({
+                div({
+                    div(displayName).classAdd("item-title item-label"),
+                    div(inner).classAdd("item-input-wrap")
+                }).classAdd("item-inner")
+            }).classAdd("item-content item-input")
+        }) { }
     };
 
     class FormInputButtonView: public input {