CVE-2024-52010
Description
Zoraxy is a general purpose HTTP reverse proxy and forwarding tool. A command injection vulnerability in the Web SSH feature allows an authenticated attacker to execute arbitrary commands as root on the host. Zoraxy has a Web SSH terminal feature that allows authenticated users to connect to SSH servers from their browsers. In HandleCreateProxySession the request to create an SSH session is handled. An attacker can exploit the username variable to escape from the bash command and inject arbitrary commands into sshCommand. This is possible, because, unlike hostname and port, the username is not validated or sanitized.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/tobychui/zoraxyGo | >= 2.6.1, < 3.1.3 | 3.1.3 |
Patches
24 files changed · +136 −23
src/mod/sshprox/sshprox.go+11 −15 modified@@ -50,21 +50,6 @@ func NewSSHProxyManager() *Manager { } } -// Get the next free port in the list -func (m *Manager) GetNextPort() int { - nextPort := m.StartingPort - occupiedPort := make(map[int]bool) - for _, instance := range m.Instances { - occupiedPort[instance.AssignedPort] = true - } - for { - if !occupiedPort[nextPort] { - return nextPort - } - nextPort++ - } -} - func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) { targetInstance, err := m.GetInstanceById(instanceId) if err != nil { @@ -168,6 +153,17 @@ func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIp if username != "" { connAddr = username + "@" + remoteIpAddr } + + //Trim the space in the username and remote address + username = strings.TrimSpace(username) + remoteIpAddr = strings.TrimSpace(remoteIpAddr) + + //Validate the username and remote address + err := ValidateUsernameAndRemoteAddr(username, remoteIpAddr) + if err != nil { + return err + } + configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty") title := username + "@" + remoteIpAddr if remotePort != 22 {
src/mod/sshprox/sshprox_test.go+66 −0 added@@ -0,0 +1,66 @@ +package sshprox + +import ( + "testing" +) + +func TestInstance_Destroy(t *testing.T) { + manager := NewSSHProxyManager() + instance, err := manager.NewSSHProxy("/tmp") + if err != nil { + t.Fatalf("Failed to create new SSH proxy: %v", err) + } + + instance.Destroy() + + if len(manager.Instances) != 0 { + t.Errorf("Expected Instances to be empty, got %d", len(manager.Instances)) + } +} + +func TestInstance_ValidateUsernameAndRemoteAddr(t *testing.T) { + tests := []struct { + username string + remoteAddr string + expectError bool + }{ + {"validuser", "127.0.0.1", false}, + {"valid.user", "example.com", false}, + {"; bash ;", "example.com", true}, + {"valid-user", "example.com", false}, + {"invalid user", "127.0.0.1", true}, + {"validuser", "invalid address", true}, + {"invalid@user", "127.0.0.1", true}, + {"validuser", "invalid@address", true}, + {"injection; rm -rf /", "127.0.0.1", true}, + {"validuser", "127.0.0.1; rm -rf /", true}, + {"$(reboot)", "127.0.0.1", true}, + {"validuser", "$(reboot)", true}, + {"validuser", "127.0.0.1; $(reboot)", true}, + {"validuser", "127.0.0.1 | ls", true}, + {"validuser", "127.0.0.1 & ls", true}, + {"validuser", "127.0.0.1 && ls", true}, + {"validuser", "127.0.0.1 |& ls", true}, + {"validuser", "127.0.0.1 ; ls", true}, + {"validuser", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", false}, + {"validuser", "2001:db8::ff00:42:8329", false}, + {"validuser", "2001:db8:0:1234:0:567:8:1", false}, + {"validuser", "2001:db8::1234:0:567:8:1", false}, + {"validuser", "2001:db8:0:0:0:0:2:1", false}, + {"validuser", "2001:db8::2:1", false}, + {"validuser", "2001:db8:0:0:8:800:200c:417a", false}, + {"validuser", "2001:db8::8:800:200c:417a", false}, + {"validuser", "2001:db8:0:0:8:800:200c:417a; rm -rf /", true}, + {"validuser", "2001:db8::8:800:200c:417a; rm -rf /", true}, + } + + for _, test := range tests { + err := ValidateUsernameAndRemoteAddr(test.username, test.remoteAddr) + if test.expectError && err == nil { + t.Errorf("Expected error for username %s and remoteAddr %s, but got none", test.username, test.remoteAddr) + } + if !test.expectError && err != nil { + t.Errorf("Did not expect error for username %s and remoteAddr %s, but got %v", test.username, test.remoteAddr, err) + } + } +}
src/mod/sshprox/utils.go+57 −6 modified@@ -1,9 +1,11 @@ package sshprox import ( + "errors" "fmt" "net" "net/url" + "regexp" "runtime" "strings" "time" @@ -34,6 +36,21 @@ func IsWebSSHSupported() bool { return true } +// Get the next free port in the list +func (m *Manager) GetNextPort() int { + nextPort := m.StartingPort + occupiedPort := make(map[int]bool) + for _, instance := range m.Instances { + occupiedPort[instance.AssignedPort] = true + } + for { + if !occupiedPort[nextPort] { + return nextPort + } + nextPort++ + } +} + // Check if a given domain and port is a valid ssh server func IsSSHConnectable(ipOrDomain string, port int) bool { timeout := time.Second * 3 @@ -60,13 +77,47 @@ func IsSSHConnectable(ipOrDomain string, port int) bool { return string(buf[:7]) == "SSH-2.0" } -// Check if the port is used by other process or application -func isPortInUse(port int) bool { - address := fmt.Sprintf(":%d", port) - listener, err := net.Listen("tcp", address) - if err != nil { +// Validate the username and remote address to prevent injection +func ValidateUsernameAndRemoteAddr(username string, remoteIpAddr string) error { + // Validate and sanitize the username to prevent ssh injection + validUsername := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + if !validUsername.MatchString(username) { + return errors.New("invalid username, only alphanumeric characters, dots, underscores and dashes are allowed") + } + + //Check if the remoteIpAddr is a valid ipv4 or ipv6 address + if net.ParseIP(remoteIpAddr) != nil { + //A valid IP address do not need further validation + return nil + } + + // Validate and sanitize the remote domain to prevent injection + validRemoteAddr := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + if !validRemoteAddr.MatchString(remoteIpAddr) { + return errors.New("invalid remote address, only alphanumeric characters, dots, underscores and dashes are allowed") + } + + return nil +} + +// Check if the given ip or domain is a loopback address +// or resolves to a loopback address +func IsLoopbackIPOrDomain(ipOrDomain string) bool { + if strings.EqualFold(strings.TrimSpace(ipOrDomain), "localhost") || strings.TrimSpace(ipOrDomain) == "127.0.0.1" { return true } - listener.Close() + + //Check if the ipOrDomain resolves to a loopback address + ips, err := net.LookupIP(ipOrDomain) + if err != nil { + return false + } + + for _, ip := range ips { + if ip.IsLoopback() { + return true + } + } + return false }
src/webssh.go+2 −2 modified@@ -42,7 +42,7 @@ func HandleCreateProxySession(w http.ResponseWriter, r *http.Request) { if !*allowSshLoopback { //Not allow loopback connections - if strings.EqualFold(strings.TrimSpace(ipaddr), "localhost") || strings.TrimSpace(ipaddr) == "127.0.0.1" { + if sshprox.IsLoopbackIPOrDomain(ipaddr) { //Request target is loopback utils.SendErrorResponse(w, "loopback web ssh connection is not enabled on this host") return @@ -74,7 +74,7 @@ func HandleCreateProxySession(w http.ResponseWriter, r *http.Request) { utils.SendJSONResponse(w, string(js)) } -//Check if the host support ssh, or if the target domain (and port, optional) support ssh +// Check if the host support ssh, or if the target domain (and port, optional) support ssh func HandleWebSshSupportCheck(w http.ResponseWriter, r *http.Request) { domain, err := utils.PostPara(r, "domain") if err != nil {
87 files changed · +273125 −0
src/api.go+203 −0 added@@ -0,0 +1,203 @@ +package main + +import ( + "encoding/json" + "net/http" + + "imuslab.com/zoraxy/mod/auth" + "imuslab.com/zoraxy/mod/netstat" + "imuslab.com/zoraxy/mod/utils" +) + +/* + API.go + + This file contains all the API called by the web management interface + +*/ + +var requireAuth = true + +func initAPIs() { + + authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{ + AuthAgent: authAgent, + RequireAuth: requireAuth, + DeniedHandler: func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "401 - Unauthorized", http.StatusUnauthorized) + }, + }) + + //Register the standard web services urls + fs := http.FileServer(http.FS(webres)) + if development { + fs = http.FileServer(http.Dir("web/")) + } + //Add a layer of middleware for advance control + advHandler := FSHandler(fs) + http.Handle("/", advHandler) + + //Authentication APIs + registerAuthAPIs(requireAuth) + + //Reverse proxy + authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff) + authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint) + authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus) + authRouter.HandleFunc("/api/proxy/list", ReverseProxyList) + authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) + authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS) + authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet) + authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect) + authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck) + + //TLS / SSL config + authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy) + authRouter.HandleFunc("/api/cert/upload", handleCertUpload) + authRouter.HandleFunc("/api/cert/list", handleListCertificate) + authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck) + authRouter.HandleFunc("/api/cert/delete", handleCertRemove) + + //Redirection config + authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules) + authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule) + authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule) + + //Blacklist APIs + authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted) + authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd) + authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove) + authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd) + authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove) + authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable) + + //Statistic & uptime monitoring API + authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad) + authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary) + authRouter.HandleFunc("/api/stats/netstat", netstat.HandleGetNetworkInterfaceStats) + authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats) + authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces) + authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing) + + //Global Area Network APIs + authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID) + authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork) + authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork) + authRouter.HandleFunc("/api/gan/network/list", ganManager.HandleListNetwork) + authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming) + //authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails) + authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges) + authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList) + authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP) + authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming) + authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization) + authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete) + + //TCP Proxy + authRouter.HandleFunc("/api/tcpprox/config/add", tcpProxyManager.HandleAddProxyConfig) + authRouter.HandleFunc("/api/tcpprox/config/edit", tcpProxyManager.HandleEditProxyConfigs) + authRouter.HandleFunc("/api/tcpprox/config/list", tcpProxyManager.HandleListConfigs) + authRouter.HandleFunc("/api/tcpprox/config/status", tcpProxyManager.HandleGetProxyStatus) + authRouter.HandleFunc("/api/tcpprox/config/validate", tcpProxyManager.HandleConfigValidate) + + //mDNS APIs + authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing) + authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning) + + //Zoraxy Analytic + authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList) + authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary) + authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary) + + //Network utilities + authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan) + authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession) + authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck) + authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan) + authRouter.HandleFunc("/api/tools/smtp/get", HandleSMTPGet) + authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet) + authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet) + authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend) + + //Account Reset + http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail) + http.HandleFunc("/api/account/new", HandleNewPasswordSetup) + + //If you got APIs to add, append them here +} + +// Function to renders Auth related APIs +func registerAuthAPIs(requireAuth bool) { + //Auth APIs + http.HandleFunc("/api/auth/login", authAgent.HandleLogin) + http.HandleFunc("/api/auth/logout", authAgent.HandleLogout) + http.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) { + if requireAuth { + authAgent.CheckLogin(w, r) + } else { + utils.SendJSONResponse(w, "true") + } + }) + http.HandleFunc("/api/auth/username", func(w http.ResponseWriter, r *http.Request) { + username, err := authAgent.GetUserName(w, r) + if err != nil { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + js, _ := json.Marshal(username) + utils.SendJSONResponse(w, string(js)) + }) + http.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) { + uc := authAgent.GetUserCounts() + js, _ := json.Marshal(uc) + utils.SendJSONResponse(w, string(js)) + }) + http.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) { + if authAgent.GetUserCounts() == 0 { + //Allow register root admin + authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) { + + }) + } else { + //This function is disabled + utils.SendErrorResponse(w, "Root management account already exists") + } + }) + http.HandleFunc("/api/auth/changePassword", func(w http.ResponseWriter, r *http.Request) { + username, err := authAgent.GetUserName(w, r) + if err != nil { + http.Error(w, "401 - Unauthorized", http.StatusUnauthorized) + return + } + + oldPassword, err := utils.PostPara(r, "oldPassword") + if err != nil { + utils.SendErrorResponse(w, "empty current password") + return + } + newPassword, err := utils.PostPara(r, "newPassword") + if err != nil { + utils.SendErrorResponse(w, "empty new password") + return + } + confirmPassword, _ := utils.PostPara(r, "confirmPassword") + + if newPassword != confirmPassword { + utils.SendErrorResponse(w, "confirm password not match") + return + } + + //Check if the old password correct + oldPasswordCorrect, _ := authAgent.ValidateUsernameAndPasswordWithReason(username, oldPassword) + if !oldPasswordCorrect { + utils.SendErrorResponse(w, "Invalid current password given") + return + } + + //Change the password of the root user + authAgent.UnregisterUser(username) + authAgent.CreateUserAccount(username, newPassword, "") + }) + +}
src/blacklist.go+102 −0 added@@ -0,0 +1,102 @@ +package main + +import ( + "encoding/json" + "net/http" + + "imuslab.com/zoraxy/mod/utils" +) + +/* + blacklist.go + + This script file is added to extend the + reverse proxy function to include + banning a specific IP address or country code +*/ + +//List a of blacklisted ip address or country code +func handleListBlacklisted(w http.ResponseWriter, r *http.Request) { + bltype, err := utils.GetPara(r, "type") + if err != nil { + bltype = "country" + } + + resulst := []string{} + if bltype == "country" { + resulst = geodbStore.GetAllBlacklistedCountryCode() + } else if bltype == "ip" { + resulst = geodbStore.GetAllBlacklistedIp() + } + + js, _ := json.Marshal(resulst) + utils.SendJSONResponse(w, string(js)) + +} + +func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) { + countryCode, err := utils.PostPara(r, "cc") + if err != nil { + utils.SendErrorResponse(w, "invalid or empty country code") + return + } + + geodbStore.AddCountryCodeToBlackList(countryCode) + + utils.SendOK(w) +} + +func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) { + countryCode, err := utils.PostPara(r, "cc") + if err != nil { + utils.SendErrorResponse(w, "invalid or empty country code") + return + } + + geodbStore.RemoveCountryCodeFromBlackList(countryCode) + + utils.SendOK(w) +} + +func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) { + ipAddr, err := utils.PostPara(r, "ip") + if err != nil { + utils.SendErrorResponse(w, "invalid or empty ip address") + return + } + + geodbStore.AddIPToBlackList(ipAddr) +} + +func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) { + ipAddr, err := utils.PostPara(r, "ip") + if err != nil { + utils.SendErrorResponse(w, "invalid or empty ip address") + return + } + + geodbStore.RemoveIPFromBlackList(ipAddr) + + utils.SendOK(w) +} + +func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) { + enable, err := utils.PostPara(r, "enable") + if err != nil { + //Return the current enabled state + currentEnabled := geodbStore.Enabled + js, _ := json.Marshal(currentEnabled) + utils.SendJSONResponse(w, string(js)) + } else { + if enable == "true" { + geodbStore.ToggleBlacklist(true) + } else if enable == "false" { + geodbStore.ToggleBlacklist(false) + } else { + utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted") + return + } + + utils.SendOK(w) + } +}
src/cert.go+189 −0 added@@ -0,0 +1,189 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + + "imuslab.com/zoraxy/mod/utils" +) + +// Check if the default certificates is correctly setup +func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) { + type CheckResult struct { + DefaultPubExists bool + DefaultPriExists bool + } + + pub, pri := tlsCertManager.DefaultCertExistsSep() + js, _ := json.Marshal(CheckResult{ + pub, + pri, + }) + + utils.SendJSONResponse(w, string(js)) +} + +// Return a list of domains where the certificates covers +func handleListCertificate(w http.ResponseWriter, r *http.Request) { + filenames, err := tlsCertManager.ListCertDomains() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + showDate, _ := utils.GetPara(r, "date") + if showDate == "true" { + type CertInfo struct { + Domain string + LastModifiedDate string + } + + results := []*CertInfo{} + + for _, filename := range filenames { + fileInfo, err := os.Stat(filepath.Join(tlsCertManager.CertStore, filename+".crt")) + if err != nil { + utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename) + return + } + modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05") + + thisCertInfo := CertInfo{ + Domain: filename, + LastModifiedDate: modifiedTime, + } + + results = append(results, &thisCertInfo) + } + + js, _ := json.Marshal(results) + w.Header().Set("Content-Type", "application/json") + w.Write(js) + } else { + response, err := json.Marshal(filenames) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(response) + } + +} + +// Handle front-end toggling TLS mode +func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) { + currentTlsSetting := false + if sysdb.KeyExists("settings", "usetls") { + sysdb.Read("settings", "usetls", ¤tTlsSetting) + } + + newState, err := utils.PostPara(r, "set") + if err != nil { + //No setting. Get the current status + js, _ := json.Marshal(currentTlsSetting) + utils.SendJSONResponse(w, string(js)) + } else { + if newState == "true" { + sysdb.Write("settings", "usetls", true) + log.Println("Enabling TLS mode on reverse proxy") + dynamicProxyRouter.UpdateTLSSetting(true) + } else if newState == "false" { + sysdb.Write("settings", "usetls", false) + log.Println("Disabling TLS mode on reverse proxy") + dynamicProxyRouter.UpdateTLSSetting(false) + } else { + utils.SendErrorResponse(w, "invalid state given. Only support true or false") + return + } + + utils.SendOK(w) + + } +} + +// Handle upload of the certificate +func handleCertUpload(w http.ResponseWriter, r *http.Request) { + // check if request method is POST + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // get the key type + keytype, err := utils.GetPara(r, "ktype") + overWriteFilename := "" + if err != nil { + http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest) + return + } + + // get the domain + domain, err := utils.GetPara(r, "domain") + if err != nil { + //Assume localhost + domain = "default" + } + + if keytype == "pub" { + overWriteFilename = domain + ".crt" + } else if keytype == "pri" { + overWriteFilename = domain + ".key" + } else { + http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest) + return + } + + // parse multipart form data + err = r.ParseMultipartForm(10 << 20) // 10 MB + if err != nil { + http.Error(w, "Failed to parse form data", http.StatusBadRequest) + return + } + + // get file from form data + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, "Failed to get file", http.StatusBadRequest) + return + } + defer file.Close() + + // create file in upload directory + os.MkdirAll("./certs", 0775) + f, err := os.Create(filepath.Join("./certs", overWriteFilename)) + if err != nil { + http.Error(w, "Failed to create file", http.StatusInternalServerError) + return + } + defer f.Close() + + // copy file contents to destination file + _, err = io.Copy(f, file) + if err != nil { + http.Error(w, "Failed to save file", http.StatusInternalServerError) + return + } + + // send response + fmt.Fprintln(w, "File upload successful!") +} + +// Handle cert remove +func handleCertRemove(w http.ResponseWriter, r *http.Request) { + domain, err := utils.PostPara(r, "domain") + if err != nil { + utils.SendErrorResponse(w, "invalid domain given") + return + } + err = tlsCertManager.RemoveCert(domain) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + } +}
src/config.go+86 −0 added@@ -0,0 +1,86 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + "imuslab.com/zoraxy/mod/utils" +) + +/* + Reverse Proxy Configs + + The following section handle + the reverse proxy configs +*/ + +type Record struct { + ProxyType string + Rootname string + ProxyTarget string + UseTLS bool +} + +func SaveReverseProxyConfig(ptype string, rootname string, proxyTarget string, useTLS bool) error { + os.MkdirAll("conf", 0775) + filename := getFilenameFromRootName(rootname) + + //Generate record + thisRecord := Record{ + ProxyType: ptype, + Rootname: rootname, + ProxyTarget: proxyTarget, + UseTLS: useTLS, + } + + //Write to file + js, _ := json.MarshalIndent(thisRecord, "", " ") + return ioutil.WriteFile(filepath.Join("conf", filename), js, 0775) +} + +func RemoveReverseProxyConfig(rootname string) error { + filename := getFilenameFromRootName(rootname) + removePendingFile := strings.ReplaceAll(filepath.Join("conf", filename), "\\", "/") + log.Println("Config Removed: ", removePendingFile) + if utils.FileExists(removePendingFile) { + err := os.Remove(removePendingFile) + if err != nil { + log.Println(err.Error()) + return err + } + } + + //File already gone + return nil +} + +// Return ptype, rootname and proxyTarget, error if any +func LoadReverseProxyConfig(filename string) (*Record, error) { + thisRecord := Record{} + configContent, err := ioutil.ReadFile(filename) + if err != nil { + return &thisRecord, err + } + + //Unmarshal the content into config + + err = json.Unmarshal(configContent, &thisRecord) + if err != nil { + return &thisRecord, err + } + + //Return it + return &thisRecord, nil +} + +func getFilenameFromRootName(rootname string) string { + //Generate a filename for this rootname + filename := strings.ReplaceAll(rootname, ".", "_") + filename = strings.ReplaceAll(filename, "/", "-") + filename = filename + ".config" + return filename +}
src/emails.go+298 −0 added@@ -0,0 +1,298 @@ +package main + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "net/http" + "strconv" + "strings" + "time" + + uuid "github.com/satori/go.uuid" + "imuslab.com/zoraxy/mod/email" + "imuslab.com/zoraxy/mod/utils" +) + +/* + SMTP Settings and Test Email Handlers +*/ + +func HandleSMTPSet(w http.ResponseWriter, r *http.Request) { + hostname, err := utils.PostPara(r, "hostname") + if err != nil { + utils.SendErrorResponse(w, "hostname cannot be empty") + return + } + + domain, err := utils.PostPara(r, "domain") + if err != nil { + utils.SendErrorResponse(w, "domain cannot be empty") + return + } + + portString, err := utils.PostPara(r, "port") + if err != nil { + utils.SendErrorResponse(w, "port must be a valid integer") + return + } + + port, err := strconv.Atoi(portString) + if err != nil { + utils.SendErrorResponse(w, "port must be a valid integer") + return + } + + username, err := utils.PostPara(r, "username") + if err != nil { + utils.SendErrorResponse(w, "username cannot be empty") + return + } + + password, err := utils.PostPara(r, "password") + if err != nil { + //Empty password. Use old one if exists + oldConfig := loadSMTPConfig() + if oldConfig.Password == "" { + utils.SendErrorResponse(w, "password cannot be empty") + return + } else { + password = oldConfig.Password + } + } + + senderAddr, err := utils.PostPara(r, "senderAddr") + if err != nil { + utils.SendErrorResponse(w, "senderAddr cannot be empty") + return + } + + adminAddr, err := utils.PostPara(r, "adminAddr") + if err != nil { + utils.SendErrorResponse(w, "adminAddr cannot be empty") + return + } + + //Set the email sender properties + thisEmailSender := email.Sender{ + Hostname: strings.TrimSpace(hostname), + Domain: strings.TrimSpace(domain), + Port: port, + Username: strings.TrimSpace(username), + Password: strings.TrimSpace(password), + SenderAddr: strings.TrimSpace(senderAddr), + } + + //Write this into database + setSMTPConfig(&thisEmailSender) + + //Update the current EmailSender + EmailSender = &thisEmailSender + + //Set the admin address of password reset + setSMTPAdminAddress(adminAddr) + + //Reply ok + utils.SendOK(w) +} + +func HandleSMTPGet(w http.ResponseWriter, r *http.Request) { + // Create a buffer to store the encoded value + var buf bytes.Buffer + + // Encode the original object into the buffer + encoder := gob.NewEncoder(&buf) + err := encoder.Encode(*EmailSender) + if err != nil { + utils.SendErrorResponse(w, "Internal encode error") + return + } + + // Decode the buffer into a new object + var copied email.Sender + decoder := gob.NewDecoder(&buf) + err = decoder.Decode(&copied) + if err != nil { + utils.SendErrorResponse(w, "Internal decode error") + return + } + + copied.Password = "" + + js, _ := json.Marshal(copied) + utils.SendJSONResponse(w, string(js)) +} + +func HandleAdminEmailGet(w http.ResponseWriter, r *http.Request) { + js, _ := json.Marshal(loadSMTPAdminAddr()) + utils.SendJSONResponse(w, string(js)) +} + +func HandleTestEmailSend(w http.ResponseWriter, r *http.Request) { + adminEmailAccount := loadSMTPAdminAddr() + if adminEmailAccount == "" { + utils.SendErrorResponse(w, "Management account not set") + return + } + + err := EmailSender.SendEmail(adminEmailAccount, + "SMTP Testing Email | Zoraxy", "This is a test email sent by Zoraxy. Please do not reply to this email.<br>Zoraxy からのテストメールです。このメールには返信しないでください。<br>這是由 Zoraxy 發送的測試電子郵件。請勿回覆此郵件。<br>Ceci est un email de test envoyé par Zoraxy. Merci de ne pas répondre à cet email.<br>Dies ist eine Test-E-Mail, die von Zoraxy gesendet wurde. Bitte antworten Sie nicht auf diese E-Mail.") + + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +/* + SMTP config + + The following handle SMTP configs +*/ + +func setSMTPConfig(config *email.Sender) error { + return sysdb.Write("smtp", "config", config) +} + +func loadSMTPConfig() *email.Sender { + if sysdb.KeyExists("smtp", "config") { + thisEmailSender := email.Sender{ + Port: 587, + } + err := sysdb.Read("smtp", "config", &thisEmailSender) + if err != nil { + return &email.Sender{ + Port: 587, + } + } + return &thisEmailSender + } else { + //Not set. Return an empty one + return &email.Sender{ + Port: 587, + } + } +} + +func setSMTPAdminAddress(adminAddr string) error { + return sysdb.Write("smtp", "admin", adminAddr) +} + +//Load SMTP admin address. Return empty string if not set +func loadSMTPAdminAddr() string { + adminAddr := "" + if sysdb.KeyExists("smtp", "admin") { + err := sysdb.Read("smtp", "admin", &adminAddr) + if err != nil { + return "" + } + return adminAddr + } else { + return "" + } +} + +/* + Admin Account Reset +*/ + +var ( + accountResetEmailDelay int64 = 30 //Delay between each account reset email, default 30s + tokenValidDuration int64 = 15 * 60 //Duration of the token, default 15 minutes + lastAccountResetEmail int64 = 0 //Timestamp for last sent account reset email + passwordResetAccessToken string = "" //Access token for resetting password +) + +func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) { + if EmailSender.Username == "" || EmailSender.Domain == "" { + //Reset account not setup + utils.SendErrorResponse(w, "Reset account not setup.") + return + } + + if loadSMTPAdminAddr() == "" { + utils.SendErrorResponse(w, "Reset account not setup.") + } + + //Check if the delay expired + if lastAccountResetEmail+accountResetEmailDelay > time.Now().Unix() { + //Too frequent + utils.SendErrorResponse(w, "You cannot send another account reset email in cooldown time") + return + } + + passwordResetAccessToken = uuid.NewV4().String() + + //SMTP info exists. Send reset account email + lastAccountResetEmail = time.Now().Unix() + EmailSender.SendEmail(loadSMTPAdminAddr(), "Management Account Reset | Zoraxy", + "Enter the following reset token to reset your password on your Zoraxy router.<br>"+passwordResetAccessToken+"<br><br> This is an automated generated email. DO NOT REPLY TO THIS EMAIL.") + + utils.SendOK(w) +} + +func HandleNewPasswordSetup(w http.ResponseWriter, r *http.Request) { + if passwordResetAccessToken == "" { + //Not initiated + utils.SendErrorResponse(w, "Invalid usage") + return + } + + username, err := utils.PostPara(r, "username") + if err != nil { + utils.SendErrorResponse(w, "Invalid username given") + return + } + token, err := utils.PostPara(r, "token") + if err != nil { + utils.SendErrorResponse(w, "Invalid token given") + return + } + newPassword, err := utils.PostPara(r, "newpw") + if err != nil { + utils.SendErrorResponse(w, "Invalid new password given") + return + } + + token = strings.TrimSpace(token) + username = strings.TrimSpace(username) + + //Validate the token + if token != passwordResetAccessToken { + utils.SendErrorResponse(w, "Invalid Token") + return + } + + //Check if time expired + if lastAccountResetEmail+tokenValidDuration < time.Now().Unix() { + //Expired + utils.SendErrorResponse(w, "Token expired") + return + } + + //Check if user exists + if !authAgent.UserExists(username) { + //Invalid admin account name + utils.SendErrorResponse(w, "Invalid Username") + return + } + + //Delete the user account + authAgent.UnregisterUser(username) + + //Ok. Set the new password + err = authAgent.CreateUserAccount(username, newPassword, "") + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +}
src/geoip.go+39 −0 added@@ -0,0 +1,39 @@ +package main + +import ( + "net" + "net/http" + "strings" + + "github.com/oschwald/geoip2-golang" +) + +func getCountryCodeFromRequest(r *http.Request) string { + countryCode := "" + + // Get the IP address of the user from the request headers + ipAddress := r.Header.Get("X-Forwarded-For") + if ipAddress == "" { + ipAddress = strings.Split(r.RemoteAddr, ":")[0] + } + + // Open the GeoIP database + db, err := geoip2.Open("./tmp/GeoIP2-Country.mmdb") + if err != nil { + // Handle the error + return countryCode + } + defer db.Close() + + // Look up the country code for the IP address + record, err := db.Country(net.ParseIP(ipAddress)) + if err != nil { + // Handle the error + return countryCode + } + + // Get the ISO country code from the record + countryCode = record.Country.IsoCode + + return countryCode +}
src/go.mod+16 −0 added@@ -0,0 +1,16 @@ +module imuslab.com/zoraxy + +go 1.16 + +require ( + github.com/boltdb/bolt v1.3.1 + github.com/go-ping/ping v1.1.0 + github.com/google/uuid v1.3.0 + github.com/gorilla/sessions v1.2.1 + github.com/gorilla/websocket v1.4.2 + github.com/grandcat/zeroconf v1.0.0 + github.com/oschwald/geoip2-golang v1.8.0 + github.com/satori/go.uuid v1.2.0 + golang.org/x/net v0.9.0 // indirect + golang.org/x/sys v0.7.0 +)
src/go.sum+94 −0 added@@ -0,0 +1,94 @@ +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= +github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= +github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= +github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= +github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= +github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= +github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg= +github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.3/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
src/main.go+158 −0 added@@ -0,0 +1,158 @@ +package main + +import ( + "embed" + "flag" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/google/uuid" + "imuslab.com/zoraxy/mod/aroz" + "imuslab.com/zoraxy/mod/auth" + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/dynamicproxy/redirection" + "imuslab.com/zoraxy/mod/email" + "imuslab.com/zoraxy/mod/ganserv" + "imuslab.com/zoraxy/mod/geodb" + "imuslab.com/zoraxy/mod/mdns" + "imuslab.com/zoraxy/mod/netstat" + "imuslab.com/zoraxy/mod/sshprox" + "imuslab.com/zoraxy/mod/statistic" + "imuslab.com/zoraxy/mod/statistic/analytic" + "imuslab.com/zoraxy/mod/tcpprox" + "imuslab.com/zoraxy/mod/tlscert" + "imuslab.com/zoraxy/mod/uptime" + "imuslab.com/zoraxy/mod/utils" +) + +// General flags +var noauth = flag.Bool("noauth", false, "Disable authentication for management interface") +var showver = flag.Bool("version", false, "Show version of this server") +var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)") +var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node") +var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port") +var ( + name = "Zoraxy" + version = "2.5" + nodeUUID = "generic" + development = false //Set this to false to use embedded web fs + + /* + Binary Embedding File System + */ + //go:embed web/* + webres embed.FS + + /* + Handler Modules + */ + handler *aroz.ArozHandler //Handle arozos managed permission system + sysdb *database.Database //System database + authAgent *auth.AuthAgent //Authentication agent + tlsCertManager *tlscert.Manager //TLS / SSL management + redirectTable *redirection.RuleTable //Handle special redirection rule sets + geodbStore *geodb.Store //GeoIP database + netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers + statisticCollector *statistic.Collector //Collecting statistic from visitors + uptimeMonitor *uptime.Monitor //Uptime monitor service worker + mdnsScanner *mdns.MDNSHost //mDNS discovery services + ganManager *ganserv.NetworkManager //Global Area Network Manager + webSshManager *sshprox.Manager //Web SSH connection service + tcpProxyManager *tcpprox.Manager //TCP Proxy Manager + + //Helper modules + EmailSender *email.Sender //Email sender that handle email sending + AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic +) + +// Kill signal handler. Do something before the system the core terminate. +func SetupCloseHandler() { + c := make(chan os.Signal, 2) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + fmt.Println("- Shutting down " + name) + fmt.Println("- Closing GeoDB ") + geodbStore.Close() + fmt.Println("- Closing Netstats Listener") + netstatBuffers.Close() + fmt.Println("- Closing Statistic Collector") + statisticCollector.Close() + fmt.Println("- Stopping mDNS Discoverer") + //Stop the mdns service + mdnsTickerStop <- true + mdnsScanner.Close() + + //Remove the tmp folder + fmt.Println("- Cleaning up tmp files") + os.RemoveAll("./tmp") + + //Close database, final + fmt.Println("- Stopping system database") + sysdb.Close() + os.Exit(0) + }() +} + +func main() { + //Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information + handler = aroz.HandleFlagParse(aroz.ServiceInfo{ + Name: name, + Desc: "Dynamic Reverse Proxy Server", + Group: "Network", + IconPath: "zoraxy/img/small_icon.png", + Version: version, + StartDir: "zoraxy/index.html", + SupportFW: true, + LaunchFWDir: "zoraxy/index.html", + SupportEmb: false, + InitFWSize: []int{1080, 580}, + }) + + if *showver { + fmt.Println(name + " - Version " + version) + os.Exit(0) + } + + SetupCloseHandler() + + //Read or create the system uuid + uuidRecord := "./sys.uuid" + if !utils.FileExists(uuidRecord) { + newSystemUUID := uuid.New().String() + os.WriteFile(uuidRecord, []byte(newSystemUUID), 0775) + } + uuidBytes, err := os.ReadFile(uuidRecord) + if err != nil { + log.Println("Unable to read system uuid from file system") + panic(err) + } + nodeUUID = string(uuidBytes) + + //Startup all modules + startupSequence() + + //Initiate management interface APIs + requireAuth = !(*noauth || handler.IsUsingExternalPermissionManager()) + initAPIs() + + //Start the reverse proxy server in go routine + go func() { + ReverseProxtInit() + }() + + time.Sleep(500 * time.Millisecond) + + log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port) + err = http.ListenAndServe(handler.Port, nil) + + if err != nil { + log.Fatal(err) + } + +}
src/Makefile+60 −0 added@@ -0,0 +1,60 @@ +# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64 +PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 +temp = $(subst /, ,$@) +os = $(word 1, $(temp)) +arch = $(word 2, $(temp)) + +#all: web.tar.gz $(PLATFORMS) fixwindows zoraxy_file_checksum.sha1 +all: clear_old $(PLATFORMS) fixwindows + +binary: $(PLATFORMS) + +hash: zoraxy_file_checksum.sha1 + +web: web.tar.gz + +clean: + rm -f zoraxy_*_* + rm -f web.tar.gz + +$(PLATFORMS): + @echo "Building $(os)/$(arch)" + GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath + + +fixwindows: + -mv ./dist/zoraxy_windows_amd64 ./dist/zoraxy_windows_amd64.exe +# -mv ./dist/zoraxy_windows_arm64 ./dist/zoraxy_windows_arm64.exe + + +clear_old: + -rm -rf ./dist/ + -mkdir ./dist/ + +web.tar.gz: + + @echo "Removing old build resources, if exists" + -rm -rf ./dist/ + -mkdir ./dist/ + + @echo "Moving subfolders to build folder" + -cp -r ./web ./dist/web/ + -cp -r ./system ./dist/system/ + + @ echo "Remove sensitive information" + -rm -rf ./dist/certs/ + -rm -rf ./dist/conf/ + -rm -rf ./dist/rules/ + + + @echo "Creating tarball for all required files" + -rm ./dist/web.tar.gz + -cd ./dist/ && tar -czf ./web.tar.gz system/ web/ + + @echo "Clearing up unzipped folder structures" + -rm -rf "./dist/web" + -rm -rf "./dist/system" + +zoraxy_file_checksum.sha1: + @echo "Generating the checksum, if sha1sum installed" + -sha1sum ./dist/web.tar.gz > ./dist/zoraxy_file_checksum.sha1 \ No newline at end of file
src/mod/aroz/aroz.go+76 −0 added@@ -0,0 +1,76 @@ +package aroz + +import ( + "encoding/json" + "flag" + "fmt" + "net/http" + "net/url" + "os" +) + +//To be used with arozos system +type ArozHandler struct { + Port string + restfulEndpoint string +} + +//Information required for registering this subservice to arozos +type ServiceInfo struct { + Name string //Name of this module. e.g. "Audio" + Desc string //Description for this module + Group string //Group of the module, e.g. "system" / "media" etc + IconPath string //Module icon image path e.g. "Audio/img/function_icon.png" + Version string //Version of the module. Format: [0-9]*.[0-9][0-9].[0-9] + StartDir string //Default starting dir, e.g. "Audio/index.html" + SupportFW bool //Support floatWindow. If yes, floatWindow dir will be loaded + LaunchFWDir string //This link will be launched instead of 'StartDir' if fw mode + SupportEmb bool //Support embedded mode + LaunchEmb string //This link will be launched instead of StartDir / Fw if a file is opened with this module + InitFWSize []int //Floatwindow init size. [0] => Width, [1] => Height + InitEmbSize []int //Embedded mode init size. [0] => Width, [1] => Height + SupportedExt []string //Supported File Extensions. e.g. ".mp3", ".flac", ".wav" +} + +//This function will request the required flag from the startup paramters and parse it to the need of the arozos. +func HandleFlagParse(info ServiceInfo) *ArozHandler { + var infoRequestMode = flag.Bool("info", false, "Show information about this program in JSON") + var port = flag.String("port", ":8000", "Management web interface listening port") + var restful = flag.String("rpt", "", "Reserved") + //Parse the flags + flag.Parse() + if *infoRequestMode { + //Information request mode + jsonString, _ := json.MarshalIndent(info, "", " ") + fmt.Println(string(jsonString)) + os.Exit(0) + } + return &ArozHandler{ + Port: *port, + restfulEndpoint: *restful, + } +} + +//Get the username and resources access token from the request, return username, token +func (a *ArozHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Request) (string, string) { + username := r.Header.Get("aouser") + token := r.Header.Get("aotoken") + + return username, token +} + +func (a *ArozHandler) IsUsingExternalPermissionManager() bool { + return !(a.restfulEndpoint == "") +} + +//Request gateway interface for advance permission sandbox control +func (a *ArozHandler) RequestGatewayInterface(token string, script string) (*http.Response, error) { + resp, err := http.PostForm(a.restfulEndpoint, + url.Values{"token": {token}, "script": {script}}) + if err != nil { + // handle error + return nil, err + } + + return resp, nil +}
src/mod/aroz/doc.txt+0 −0 addedsrc/mod/auth/auth.go+478 −0 added@@ -0,0 +1,478 @@ +package auth + +/* + + author: tobychui +*/ + +import ( + "crypto/rand" + "crypto/sha512" + "errors" + "net/http" + "net/mail" + "strings" + + "encoding/hex" + "log" + + "github.com/gorilla/sessions" + db "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/utils" +) + +type AuthAgent struct { + //Session related + SessionName string + SessionStore *sessions.CookieStore + Database *db.Database + LoginRedirectionHandler func(http.ResponseWriter, *http.Request) +} + +type AuthEndpoints struct { + Login string + Logout string + Register string + CheckLoggedIn string + Autologin string +} + +//Constructor +func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent { + store := sessions.NewCookieStore(key) + err := sysdb.NewTable("auth") + if err != nil { + log.Println("Failed to create auth database. Terminating.") + panic(err) + } + + //Create a new AuthAgent object + newAuthAgent := AuthAgent{ + SessionName: sessionName, + SessionStore: store, + Database: sysdb, + LoginRedirectionHandler: loginRedirectionHandler, + } + + //Return the authAgent + return &newAuthAgent +} + +func GetSessionKey(sysdb *db.Database) (string, error) { + sysdb.NewTable("auth") + sessionKey := "" + if !sysdb.KeyExists("auth", "sessionkey") { + key := make([]byte, 32) + rand.Read(key) + sessionKey = string(key) + sysdb.Write("auth", "sessionkey", sessionKey) + log.Println("[Auth] New authentication session key generated") + } else { + log.Println("[Auth] Authentication session key loaded from database") + err := sysdb.Read("auth", "sessionkey", &sessionKey) + if err != nil { + return "", errors.New("database read error. Is the database file corrupted?") + } + } + return sessionKey, nil +} + +//This function will handle an http request and redirect to the given login address if not logged in +func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) { + if a.CheckAuth(r) { + //User already logged in + handler(w, r) + } else { + //User not logged in + a.LoginRedirectionHandler(w, r) + } +} + +//Handle login request, require POST username and password +func (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) { + + //Get username from request using POST mode + username, err := utils.PostPara(r, "username") + if err != nil { + //Username not defined + log.Println("[Auth] " + r.RemoteAddr + " trying to login with username: " + username) + utils.SendErrorResponse(w, "Username not defined or empty.") + return + } + + //Get password from request using POST mode + password, err := utils.PostPara(r, "password") + if err != nil { + //Password not defined + utils.SendErrorResponse(w, "Password not defined or empty.") + return + } + + //Get rememberme settings + rememberme := false + rmbme, _ := utils.PostPara(r, "rmbme") + if rmbme == "true" { + rememberme = true + } + + //Check the database and see if this user is in the database + passwordCorrect, rejectionReason := a.ValidateUsernameAndPasswordWithReason(username, password) + //The database contain this user information. Check its password if it is correct + if passwordCorrect { + //Password correct + // Set user as authenticated + a.LoginUserByRequest(w, r, username, rememberme) + + //Print the login message to console + log.Println(username + " logged in.") + utils.SendOK(w) + } else { + //Password incorrect + log.Println(username + " login request rejected: " + rejectionReason) + + utils.SendErrorResponse(w, rejectionReason) + return + } +} + +func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string) bool { + succ, _ := a.ValidateUsernameAndPasswordWithReason(username, password) + return succ +} + +//validate the username and password, return reasons if the auth failed +func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, password string) (bool, string) { + hashedPassword := Hash(password) + var passwordInDB string + err := a.Database.Read("auth", "passhash/"+username, &passwordInDB) + if err != nil { + //User not found or db exception + log.Println("[Auth] " + username + " login with incorrect password") + return false, "Invalid username or password" + } + + if passwordInDB == hashedPassword { + return true, "" + } else { + return false, "Invalid username or password" + } +} + +//Login the user by creating a valid session for this user +func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, username string, rememberme bool) { + session, _ := a.SessionStore.Get(r, a.SessionName) + + session.Values["authenticated"] = true + session.Values["username"] = username + session.Values["rememberMe"] = rememberme + + //Check if remember me is clicked. If yes, set the maxage to 1 week. + if rememberme { + session.Options = &sessions.Options{ + MaxAge: 3600 * 24 * 7, //One week + Path: "/", + } + } else { + session.Options = &sessions.Options{ + MaxAge: 3600 * 1, //One hour + Path: "/", + } + } + session.Save(r, w) +} + +//Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION +func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) { + username, err := a.GetUserName(w, r) + if username != "" { + log.Println(username + " logged out.") + } + // Revoke users authentication + err = a.Logout(w, r) + if err != nil { + utils.SendErrorResponse(w, "Logout failed") + return + } + + w.Write([]byte("OK")) +} + +func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error { + session, err := a.SessionStore.Get(r, a.SessionName) + if err != nil { + return err + } + session.Values["authenticated"] = false + session.Values["username"] = nil + session.Save(r, w) + return nil +} + +//Get the current session username from request +func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string, error) { + if a.CheckAuth(r) { + //This user has logged in. + session, _ := a.SessionStore.Get(r, a.SessionName) + return session.Values["username"].(string), nil + } else { + //This user has not logged in. + return "", errors.New("user not logged in") + } +} + +//Get the current session user email from request +func (a *AuthAgent) GetUserEmail(w http.ResponseWriter, r *http.Request) (string, error) { + if a.CheckAuth(r) { + //This user has logged in. + session, _ := a.SessionStore.Get(r, a.SessionName) + username := session.Values["username"].(string) + userEmail := "" + err := a.Database.Read("auth", "email/"+username, &userEmail) + if err != nil { + return "", err + } + + return userEmail, nil + } else { + //This user has not logged in. + return "", errors.New("user not logged in") + } +} + +//Check if the user has logged in, return true / false in JSON +func (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) { + if a.CheckAuth(r) { + utils.SendJSONResponse(w, "true") + } else { + utils.SendJSONResponse(w, "false") + } +} + +//Handle new user register. Require POST username, password, group. +func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request, callback func(string, string)) { + //Get username from request + newusername, err := utils.PostPara(r, "username") + if err != nil { + utils.SendErrorResponse(w, "Missing 'username' paramter") + return + } + + //Get password from request + password, err := utils.PostPara(r, "password") + if err != nil { + utils.SendErrorResponse(w, "Missing 'password' paramter") + return + } + + //Get email from request + email, err := utils.PostPara(r, "email") + if err != nil { + utils.SendErrorResponse(w, "Missing 'email' paramter") + return + } + + _, err = mail.ParseAddress(email) + if err != nil { + utils.SendErrorResponse(w, "Invalid or malformed email") + return + } + + //Ok to proceed create this user + err = a.CreateUserAccount(newusername, password, email) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + //Do callback if exists + if callback != nil { + callback(newusername, email) + } + + //Return to the client with OK + utils.SendOK(w) + log.Println("[Auth] New user " + newusername + " added to system.") +} + +//Handle new user register without confirmation email. Require POST username, password, group. +func (a *AuthAgent) HandleRegisterWithoutEmail(w http.ResponseWriter, r *http.Request, callback func(string, string)) { + //Get username from request + newusername, err := utils.PostPara(r, "username") + if err != nil { + utils.SendErrorResponse(w, "Missing 'username' paramter") + return + } + + //Get password from request + password, err := utils.PostPara(r, "password") + if err != nil { + utils.SendErrorResponse(w, "Missing 'password' paramter") + return + } + + //Ok to proceed create this user + err = a.CreateUserAccount(newusername, password, "") + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + //Do callback if exists + if callback != nil { + callback(newusername, "") + } + + //Return to the client with OK + utils.SendOK(w) + log.Println("[Auth] Admin account created: " + newusername) +} + +//Check authentication from request header's session value +func (a *AuthAgent) CheckAuth(r *http.Request) bool { + session, err := a.SessionStore.Get(r, a.SessionName) + if err != nil { + return false + } + // Check if user is authenticated + if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { + return false + } + return true +} + +//Handle de-register of users. Require POST username. +//THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER +func (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) { + //Check if the user is logged in + if !a.CheckAuth(r) { + //This user has not logged in + utils.SendErrorResponse(w, "Login required to remove user from the system.") + return + } + + //Get username from request + username, err := utils.PostPara(r, "username") + if err != nil { + utils.SendErrorResponse(w, "Missing 'username' paramter") + return + } + + err = a.UnregisterUser(username) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + //Return to the client with OK + utils.SendOK(w) + log.Println("[Auth] User " + username + " has been removed from the system.") +} + +func (a *AuthAgent) UnregisterUser(username string) error { + //Check if the user exists in the system database. + if !a.Database.KeyExists("auth", "passhash/"+username) { + //This user do not exists. + return errors.New("this user does not exists") + } + + //OK! Remove the user from the database + a.Database.Delete("auth", "passhash/"+username) + a.Database.Delete("auth", "email/"+username) + return nil +} + +//Get the number of users in the system +func (a *AuthAgent) GetUserCounts() int { + entries, _ := a.Database.ListTable("auth") + usercount := 0 + for _, keypairs := range entries { + if strings.Contains(string(keypairs[0]), "passhash/") { + //This is a user registry + usercount++ + } + } + + if usercount == 0 { + log.Println("There are no user in the database.") + } + return usercount +} + +//List all username within the system +func (a *AuthAgent) ListUsers() []string { + entries, _ := a.Database.ListTable("auth") + results := []string{} + for _, keypairs := range entries { + if strings.Contains(string(keypairs[0]), "passhash/") { + username := strings.Split(string(keypairs[0]), "/")[1] + results = append(results, username) + } + } + return results +} + +//Check if the given username exists +func (a *AuthAgent) UserExists(username string) bool { + userpasswordhash := "" + err := a.Database.Read("auth", "passhash/"+username, &userpasswordhash) + if err != nil || userpasswordhash == "" { + return false + } + return true +} + +//Update the session expire time given the request header. +func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Request) bool { + session, _ := a.SessionStore.Get(r, a.SessionName) + if session.Values["authenticated"].(bool) { + //User authenticated. Extend its expire time + rememberme := session.Values["rememberMe"].(bool) + //Extend the session expire time + if rememberme { + session.Options = &sessions.Options{ + MaxAge: 3600 * 24 * 7, //One week + Path: "/", + } + } else { + session.Options = &sessions.Options{ + MaxAge: 3600 * 1, //One hour + Path: "/", + } + } + session.Save(r, w) + return true + } else { + return false + } +} + +//Create user account +func (a *AuthAgent) CreateUserAccount(newusername string, password string, email string) error { + //Check user already exists + if a.UserExists(newusername) { + return errors.New("user with same name already exists") + } + + key := newusername + hashedPassword := Hash(password) + err := a.Database.Write("auth", "passhash/"+key, hashedPassword) + if err != nil { + return err + } + + if email != "" { + err = a.Database.Write("auth", "email/"+key, email) + if err != nil { + return err + } + } + + return nil +} + +//Hash the given raw string into sha512 hash +func Hash(raw string) string { + h := sha512.New() + h.Write([]byte(raw)) + return hex.EncodeToString(h.Sum(nil)) +}
src/mod/auth/router.go+53 −0 added@@ -0,0 +1,53 @@ +package auth + +import ( + "errors" + "log" + "net/http" +) + +type RouterOption struct { + AuthAgent *AuthAgent + RequireAuth bool //This router require authentication + DeniedHandler func(http.ResponseWriter, *http.Request) //Things to do when request is rejected + +} + +type RouterDef struct { + option RouterOption + endpoints map[string]func(http.ResponseWriter, *http.Request) +} + +func NewManagedHTTPRouter(option RouterOption) *RouterDef { + return &RouterDef{ + option: option, + endpoints: map[string]func(http.ResponseWriter, *http.Request){}, + } +} + +func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request)) error { + //Check if the endpoint already registered + if _, exist := router.endpoints[endpoint]; exist { + log.Println("WARNING! Duplicated registering of web endpoint: " + endpoint) + return errors.New("endpoint register duplicated") + } + + authAgent := router.option.AuthAgent + + //OK. Register handler + http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + //Check authentication of the user + if router.option.RequireAuth { + authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) { + handler(w, r) + }) + } else { + handler(w, r) + } + + }) + + router.endpoints[endpoint] = handler + + return nil +}
src/mod/database/database_core.go+186 −0 added@@ -0,0 +1,186 @@ +//go:build !mipsle && !riscv64 +// +build !mipsle,!riscv64 + +package database + +import ( + "encoding/json" + "errors" + "log" + "sync" + + "github.com/boltdb/bolt" +) + +func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) { + db, err := bolt.Open(dbfile, 0600, nil) + if err != nil { + return nil, err + } + + tableMap := sync.Map{} + //Build the table list from database + err = db.View(func(tx *bolt.Tx) error { + return tx.ForEach(func(name []byte, _ *bolt.Bucket) error { + tableMap.Store(string(name), "") + return nil + }) + }) + + return &Database{ + Db: db, + Tables: tableMap, + ReadOnly: readOnlyMode, + }, err +} + +//Dump the whole db into a log file +func (d *Database) dump(filename string) ([]string, error) { + results := []string{} + + d.Tables.Range(func(tableName, v interface{}) bool { + entries, err := d.ListTable(tableName.(string)) + if err != nil { + log.Println("Reading table " + tableName.(string) + " failed: " + err.Error()) + return false + } + for _, keypairs := range entries { + results = append(results, string(keypairs[0])+":"+string(keypairs[1])+"\n") + } + return true + }) + + return results, nil +} + +//Create a new table +func (d *Database) newTable(tableName string) error { + if d.ReadOnly == true { + return errors.New("Operation rejected in ReadOnly mode") + } + + err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(tableName)) + if err != nil { + return err + } + return nil + }) + + d.Tables.Store(tableName, "") + return err +} + +//Check is table exists +func (d *Database) tableExists(tableName string) bool { + if _, ok := d.Tables.Load(tableName); ok { + return true + } + return false +} + +//Drop the given table +func (d *Database) dropTable(tableName string) error { + if d.ReadOnly == true { + return errors.New("Operation rejected in ReadOnly mode") + } + + err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error { + err := tx.DeleteBucket([]byte(tableName)) + if err != nil { + return err + } + return nil + }) + return err +} + +//Write to table +func (d *Database) write(tableName string, key string, value interface{}) error { + if d.ReadOnly { + return errors.New("Operation rejected in ReadOnly mode") + } + + jsonString, err := json.Marshal(value) + if err != nil { + return err + } + err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(tableName)) + if err != nil { + return err + } + b := tx.Bucket([]byte(tableName)) + err = b.Put([]byte(key), jsonString) + return err + }) + return err +} + +func (d *Database) read(tableName string, key string, assignee interface{}) error { + err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(tableName)) + v := b.Get([]byte(key)) + json.Unmarshal(v, &assignee) + return nil + }) + return err +} + +func (d *Database) keyExists(tableName string, key string) bool { + resultIsNil := false + if !d.TableExists(tableName) { + //Table not exists. Do not proceed accessing key + log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!") + return false + } + err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(tableName)) + v := b.Get([]byte(key)) + if v == nil { + resultIsNil = true + } + return nil + }) + + if err != nil { + return false + } else { + if resultIsNil { + return false + } else { + return true + } + } +} + +func (d *Database) delete(tableName string, key string) error { + if d.ReadOnly { + return errors.New("Operation rejected in ReadOnly mode") + } + + err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error { + tx.Bucket([]byte(tableName)).Delete([]byte(key)) + return nil + }) + + return err +} + +func (d *Database) listTable(tableName string) ([][][]byte, error) { + var results [][][]byte + err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(tableName)) + c := b.Cursor() + + for k, v := c.First(); k != nil; k, v = c.Next() { + results = append(results, [][]byte{k, v}) + } + return nil + }) + return results, err +} + +func (d *Database) close() { + d.Db.(*bolt.DB).Close() +}
src/mod/database/database.go+120 −0 added@@ -0,0 +1,120 @@ +package database + +/* + ArOZ Online Database Access Module + author: tobychui + + This is an improved Object oriented base solution to the original + aroz online database script. +*/ + +import ( + "sync" +) + +type Database struct { + Db interface{} //This will be nil on openwrt and *bolt.DB in the rest of the systems + Tables sync.Map + ReadOnly bool +} + +func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) { + return newDatabase(dbfile, readOnlyMode) +} + +/* + Create / Drop a table + Usage: + err := sysdb.NewTable("MyTable") + err := sysdb.DropTable("MyTable") +*/ + +func (d *Database) UpdateReadWriteMode(readOnly bool) { + d.ReadOnly = readOnly +} + +//Dump the whole db into a log file +func (d *Database) Dump(filename string) ([]string, error) { + return d.dump(filename) +} + +//Create a new table +func (d *Database) NewTable(tableName string) error { + return d.newTable(tableName) +} + +//Check is table exists +func (d *Database) TableExists(tableName string) bool { + return d.tableExists(tableName) +} + +//Drop the given table +func (d *Database) DropTable(tableName string) error { + return d.dropTable(tableName) +} + +/* + Write to database with given tablename and key. Example Usage: + type demo struct{ + content string + } + thisDemo := demo{ + content: "Hello World", + } + err := sysdb.Write("MyTable", "username/message",thisDemo); +*/ +func (d *Database) Write(tableName string, key string, value interface{}) error { + return d.write(tableName, key, value) +} + +/* + Read from database and assign the content to a given datatype. Example Usage: + + type demo struct{ + content string + } + thisDemo := new(demo) + err := sysdb.Read("MyTable", "username/message",&thisDemo); +*/ + +func (d *Database) Read(tableName string, key string, assignee interface{}) error { + return d.read(tableName, key, assignee) +} + +func (d *Database) KeyExists(tableName string, key string) bool { + return d.keyExists(tableName, key) +} + +/* + Delete a value from the database table given tablename and key + + err := sysdb.Delete("MyTable", "username/message"); +*/ +func (d *Database) Delete(tableName string, key string) error { + return d.delete(tableName, key) +} + +/* + //List table example usage + //Assume the value is stored as a struct named "groupstruct" + + entries, err := sysdb.ListTable("test") + if err != nil { + panic(err) + } + for _, keypairs := range entries{ + log.Println(string(keypairs[0])) + group := new(groupstruct) + json.Unmarshal(keypairs[1], &group) + log.Println(group); + } + +*/ + +func (d *Database) ListTable(tableName string) ([][][]byte, error) { + return d.listTable(tableName) +} + +func (d *Database) Close() { + d.close() +}
src/mod/database/database_openwrt.go+208 −0 added@@ -0,0 +1,208 @@ +//go:build mipsle || riscv64 +// +build mipsle riscv64 + +package database + +import ( + "encoding/json" + "errors" + "log" + "os" + "path/filepath" + "strings" + "sync" +) + +func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) { + dbRootPath := filepath.ToSlash(filepath.Clean(dbfile)) + dbRootPath = "fsdb/" + dbRootPath + err := os.MkdirAll(dbRootPath, 0755) + if err != nil { + return nil, err + } + + tableMap := sync.Map{} + //build the table list from file system + files, err := filepath.Glob(filepath.Join(dbRootPath, "/*")) + if err != nil { + return nil, err + } + + for _, file := range files { + if isDirectory(file) { + tableMap.Store(filepath.Base(file), "") + } + } + + log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath) + return &Database{ + Db: dbRootPath, + Tables: tableMap, + ReadOnly: readOnlyMode, + }, nil +} + +func (d *Database) dump(filename string) ([]string, error) { + //Get all file objects from root + rootfiles, err := filepath.Glob(filepath.Join(d.Db.(string), "/*")) + if err != nil { + return []string{}, err + } + + //Filter out the folders + rootFolders := []string{} + for _, file := range rootfiles { + if !isDirectory(file) { + rootFolders = append(rootFolders, filepath.Base(file)) + } + } + + return rootFolders, nil +} + +func (d *Database) newTable(tableName string) error { + if d.ReadOnly { + return errors.New("Operation rejected in ReadOnly mode") + } + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + if !fileExists(tablePath) { + return os.MkdirAll(tablePath, 0755) + } + return nil +} + +func (d *Database) tableExists(tableName string) bool { + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + if _, err := os.Stat(tablePath); errors.Is(err, os.ErrNotExist) { + return false + } + + if !isDirectory(tablePath) { + return false + } + + return true +} + +func (d *Database) dropTable(tableName string) error { + if d.ReadOnly { + return errors.New("Operation rejected in ReadOnly mode") + } + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + if d.tableExists(tableName) { + return os.RemoveAll(tablePath) + } else { + return errors.New("table not exists") + } + +} + +func (d *Database) write(tableName string, key string, value interface{}) error { + if d.ReadOnly { + return errors.New("Operation rejected in ReadOnly mode") + } + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + js, err := json.Marshal(value) + if err != nil { + return err + } + + key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-") + + return os.WriteFile(filepath.Join(tablePath, key+".entry"), js, 0755) +} + +func (d *Database) read(tableName string, key string, assignee interface{}) error { + if !d.keyExists(tableName, key) { + return errors.New("key not exists") + } + + key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-") + + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + entryPath := filepath.Join(tablePath, key+".entry") + content, err := os.ReadFile(entryPath) + if err != nil { + return err + } + + err = json.Unmarshal(content, &assignee) + return err +} + +func (d *Database) keyExists(tableName string, key string) bool { + key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-") + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + entryPath := filepath.Join(tablePath, key+".entry") + return fileExists(entryPath) +} + +func (d *Database) delete(tableName string, key string) error { + if d.ReadOnly { + return errors.New("Operation rejected in ReadOnly mode") + } + if !d.keyExists(tableName, key) { + return errors.New("key not exists") + } + key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-") + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + entryPath := filepath.Join(tablePath, key+".entry") + + return os.Remove(entryPath) +} + +func (d *Database) listTable(tableName string) ([][][]byte, error) { + if !d.tableExists(tableName) { + return [][][]byte{}, errors.New("table not exists") + } + tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) + entries, err := filepath.Glob(filepath.Join(tablePath, "/*.entry")) + if err != nil { + return [][][]byte{}, err + } + + var results [][][]byte = [][][]byte{} + for _, entry := range entries { + if !isDirectory(entry) { + //Read it + key := filepath.Base(entry) + key = strings.TrimSuffix(key, filepath.Ext(key)) + key = strings.ReplaceAll(key, "-SLASH_SIGN-", "/") + + bkey := []byte(key) + bval := []byte("") + c, err := os.ReadFile(entry) + if err != nil { + break + } + + bval = c + results = append(results, [][]byte{bkey, bval}) + } + } + return results, nil +} + +func (d *Database) close() { + //Nothing to close as it is file system +} + +func isDirectory(path string) bool { + fileInfo, err := os.Stat(path) + if err != nil { + return false + } + + return fileInfo.IsDir() +} + +func fileExists(name string) bool { + _, err := os.Stat(name) + if err == nil { + return true + } + if errors.Is(err, os.ErrNotExist) { + return false + } + return false +}
src/mod/dynamicproxy/domainsniff/domainsniff.go+23 −0 added@@ -0,0 +1,23 @@ +package domainsniff + +import ( + "net" + "time" +) + +//Check if the domain is reachable and return err if not reachable +func DomainReachableWithError(domain string) error { + timeout := 1 * time.Second + conn, err := net.DialTimeout("tcp", domain, timeout) + if err != nil { + return err + } + + conn.Close() + return nil +} + +//Check if domain reachable +func DomainReachable(domain string) bool { + return DomainReachableWithError(domain) == nil +}
src/mod/dynamicproxy/dpcore/dpcore.go+494 −0 added@@ -0,0 +1,494 @@ +package dpcore + +import ( + "errors" + "io" + "log" + "net" + "net/http" + "net/url" + "strings" + "sync" + "time" +) + +var onExitFlushLoop func() + +const ( + defaultTimeout = time.Minute * 5 +) + +// ReverseProxy is an HTTP Handler that takes an incoming request and +// sends it to another server, proxying the response back to the +// client, support http, also support https tunnel using http.hijacker +type ReverseProxy struct { + // Set the timeout of the proxy server, default is 5 minutes + Timeout time.Duration + + // Director must be a function which modifies + // the request into a new request to be sent + // using Transport. Its response is then copied + // back to the original client unmodified. + // Director must not access the provided Request + // after returning. + Director func(*http.Request) + + // The transport used to perform proxy requests. + // default is http.DefaultTransport. + Transport http.RoundTripper + + // FlushInterval specifies the flush interval + // to flush to the client while copying the + // response body. If zero, no periodic flushing is done. + FlushInterval time.Duration + + // ErrorLog specifies an optional logger for errors + // that occur when attempting to proxy the request. + // If nil, logging goes to os.Stderr via the log package's + // standard logger. + ErrorLog *log.Logger + + // ModifyResponse is an optional function that + // modifies the Response from the backend. + // If it returns an error, the proxy returns a StatusBadGateway error. + ModifyResponse func(*http.Response) error + + //Prepender is an optional prepend text for URL rewrite + // + Prepender string + + Verbal bool +} + +type ResponseRewriteRuleSet struct { + ProxyDomain string + OriginalHost string + UseTLS bool + PathPrefix string //Vdir prefix for root, / will be rewrite to this +} + +type requestCanceler interface { + CancelRequest(req *http.Request) +} + +func NewDynamicProxyCore(target *url.URL, prepender string) *ReverseProxy { + targetQuery := target.RawQuery + director := func(req *http.Request) { + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL) + if targetQuery == "" || req.URL.RawQuery == "" { + req.URL.RawQuery = targetQuery + req.URL.RawQuery + } else { + req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery + } + + if _, ok := req.Header["User-Agent"]; !ok { + req.Header.Set("User-Agent", "") + } + + } + + //Hack the default transporter to handle more connections + thisTransporter := http.DefaultTransport + thisTransporter.(*http.Transport).MaxIdleConns = 3000 + thisTransporter.(*http.Transport).MaxIdleConnsPerHost = 3000 + thisTransporter.(*http.Transport).IdleConnTimeout = 10 * time.Second + thisTransporter.(*http.Transport).MaxConnsPerHost = 0 + thisTransporter.(*http.Transport).DisableCompression = true + + return &ReverseProxy{ + Director: director, + Prepender: prepender, + Verbal: false, + Transport: thisTransporter, + } +} + +func singleJoiningSlash(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + switch { + case aslash && bslash: + return a + b[1:] + case !aslash && !bslash: + return a + "/" + b + } + return a + b +} + +func joinURLPath(a, b *url.URL) (path, rawpath string) { + + if a.RawPath == "" && b.RawPath == "" { + + return singleJoiningSlash(a.Path, b.Path), "" + + } + + // Same as singleJoiningSlash, but uses EscapedPath to determine + + // whether a slash should be added + + apath := a.EscapedPath() + + bpath := b.EscapedPath() + + aslash := strings.HasSuffix(apath, "/") + + bslash := strings.HasPrefix(bpath, "/") + + switch { + + case aslash && bslash: + + return a.Path + b.Path[1:], apath + bpath[1:] + + case !aslash && !bslash: + + return a.Path + "/" + b.Path, apath + "/" + bpath + + } + + return a.Path + b.Path, apath + bpath + +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +// Hop-by-hop headers. These are removed when sent to the backend. +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html +var hopHeaders = []string{ + //"Connection", + "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "Te", // canonicalized version of "TE" + "Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522 + "Transfer-Encoding", + //"Upgrade", +} + +func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) { + if p.FlushInterval != 0 { + if wf, ok := dst.(writeFlusher); ok { + mlw := &maxLatencyWriter{ + dst: wf, + latency: p.FlushInterval, + done: make(chan bool), + } + + go mlw.flushLoop() + defer mlw.stop() + dst = mlw + } + } + + io.Copy(dst, src) +} + +type writeFlusher interface { + io.Writer + http.Flusher +} + +type maxLatencyWriter struct { + dst writeFlusher + latency time.Duration + mu sync.Mutex + done chan bool +} + +func (m *maxLatencyWriter) Write(b []byte) (int, error) { + m.mu.Lock() + defer m.mu.Unlock() + return m.dst.Write(b) +} + +func (m *maxLatencyWriter) flushLoop() { + t := time.NewTicker(m.latency) + defer t.Stop() + for { + select { + case <-m.done: + if onExitFlushLoop != nil { + onExitFlushLoop() + } + return + case <-t.C: + m.mu.Lock() + m.dst.Flush() + m.mu.Unlock() + } + } +} + +func (m *maxLatencyWriter) stop() { + m.done <- true +} + +func (p *ReverseProxy) logf(format string, args ...interface{}) { + if p.ErrorLog != nil { + p.ErrorLog.Printf(format, args...) + } else { + log.Printf(format, args...) + } +} + +func removeHeaders(header http.Header) { + // Remove hop-by-hop headers listed in the "Connection" header. + if c := header.Get("Connection"); c != "" { + for _, f := range strings.Split(c, ",") { + if f = strings.TrimSpace(f); f != "" { + header.Del(f) + } + } + } + + // Remove hop-by-hop headers + for _, h := range hopHeaders { + if header.Get(h) != "" { + header.Del(h) + } + } + + if header.Get("A-Upgrade") != "" { + header.Set("Upgrade", header.Get("A-Upgrade")) + header.Del("A-Upgrade") + } +} + +func addXForwardedForHeader(req *http.Request) { + if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { + // If we aren't the first proxy retain prior + // X-Forwarded-For information as a comma+space + // separated list and fold multiple headers into one. + if prior, ok := req.Header["X-Forwarded-For"]; ok { + clientIP = strings.Join(prior, ", ") + ", " + clientIP + } + req.Header.Set("X-Forwarded-For", clientIP) + } +} + +func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error { + transport := p.Transport + if transport == nil { + transport = http.DefaultTransport + } + + outreq := new(http.Request) + // Shallow copies of maps, like header + *outreq = *req + + if cn, ok := rw.(http.CloseNotifier); ok { + if requestCanceler, ok := transport.(requestCanceler); ok { + // After the Handler has returned, there is no guarantee + // that the channel receives a value, so to make sure + reqDone := make(chan struct{}) + defer close(reqDone) + clientGone := cn.CloseNotify() + + go func() { + select { + case <-clientGone: + requestCanceler.CancelRequest(outreq) + case <-reqDone: + } + }() + } + } + + p.Director(outreq) + outreq.Close = false + + if !rrr.UseTLS { + //This seems to be routing to external sites + //Do not keep the original host + outreq.Host = rrr.OriginalHost + } + + // We may modify the header (shallow copied above), so we only copy it. + outreq.Header = make(http.Header) + copyHeader(outreq.Header, req.Header) + + // Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers. + removeHeaders(outreq.Header) + + // Add X-Forwarded-For Header. + addXForwardedForHeader(outreq) + + res, err := transport.RoundTrip(outreq) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + + //rw.WriteHeader(http.StatusBadGateway) + return err + } + + // Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers. + removeHeaders(res.Header) + + if p.ModifyResponse != nil { + if err := p.ModifyResponse(res); err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + + //rw.WriteHeader(http.StatusBadGateway) + return err + } + } + + //Custom header rewriter functions + if res.Header.Get("Location") != "" { + /* + fmt.Println(">>> REQ", req) + fmt.Println(">>> OUTR", outreq) + fmt.Println(">>> RESP", res) + */ + locationRewrite := res.Header.Get("Location") + originLocation := res.Header.Get("Location") + res.Header.Set("zr-origin-location", originLocation) + + if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") { + //Full path + //Replace the forwarded target with expected Host + lr, err := replaceLocationHost(locationRewrite, rrr.OriginalHost, req.TLS != nil) + if err == nil { + locationRewrite = lr + } + //locationRewrite = strings.ReplaceAll(locationRewrite, rrr.ProxyDomain, rrr.OriginalHost) + //locationRewrite = strings.ReplaceAll(locationRewrite, domainWithoutPort, rrr.OriginalHost) + } else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" { + //Back to the root of this proxy object + //fmt.Println(rrr.ProxyDomain, rrr.OriginalHost) + locationRewrite = strings.TrimSuffix(rrr.PathPrefix, "/") + originLocation + } else { + //Relative path. Do not modifiy location header + + } + + //Custom redirection to this rproxy relative path + res.Header.Set("Location", locationRewrite) + } + // Copy header from response to client. + copyHeader(rw.Header(), res.Header) + + // The "Trailer" header isn't included in the Transport's response, Build it up from Trailer. + if len(res.Trailer) > 0 { + trailerKeys := make([]string, 0, len(res.Trailer)) + for k := range res.Trailer { + trailerKeys = append(trailerKeys, k) + } + rw.Header().Add("Trailer", strings.Join(trailerKeys, ", ")) + } + + rw.WriteHeader(res.StatusCode) + if len(res.Trailer) > 0 { + // Force chunking if we saw a response trailer. + // This prevents net/http from calculating the length for short + // bodies and adding a Content-Length. + if fl, ok := rw.(http.Flusher); ok { + fl.Flush() + } + } + + p.copyResponse(rw, res.Body) + // close now, instead of defer, to populate res.Trailer + res.Body.Close() + copyHeader(rw.Header(), res.Trailer) + + return nil +} + +func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) error { + hij, ok := rw.(http.Hijacker) + if !ok { + p.logf("http server does not support hijacker") + return errors.New("http server does not support hijacker") + } + + clientConn, _, err := hij.Hijack() + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + proxyConn, err := net.Dial("tcp", req.URL.Host) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + + return err + } + + // The returned net.Conn may have read or write deadlines + // already set, depending on the configuration of the + // Server, to set or clear those deadlines as needed + // we set timeout to 5 minutes + deadline := time.Now() + if p.Timeout == 0 { + deadline = deadline.Add(time.Minute * 5) + } else { + deadline = deadline.Add(p.Timeout) + } + + err = clientConn.SetDeadline(deadline) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + err = proxyConn.SetDeadline(deadline) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + + return err + } + + _, err = clientConn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + + return err + } + + go func() { + io.Copy(clientConn, proxyConn) + clientConn.Close() + proxyConn.Close() + }() + + io.Copy(proxyConn, clientConn) + proxyConn.Close() + clientConn.Close() + + return nil +} + +func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error { + if req.Method == "CONNECT" { + err := p.ProxyHTTPS(rw, req) + return err + } else { + err := p.ProxyHTTP(rw, req, rrr) + return err + } +}
src/mod/dynamicproxy/dpcore/LICENSE+21 −0 added@@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-present tobychui + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
src/mod/dynamicproxy/dpcore/utils.go+21 −0 added@@ -0,0 +1,21 @@ +package dpcore + +import ( + "net/url" +) + +func replaceLocationHost(urlString string, newHost string, useTLS bool) (string, error) { + u, err := url.Parse(urlString) + if err != nil { + return "", err + } + + if useTLS { + u.Scheme = "https" + } else { + u.Scheme = "http" + } + + u.Host = newHost + return u.String(), nil +}
src/mod/dynamicproxy/dynamicproxy.go+373 −0 added@@ -0,0 +1,373 @@ +package dynamicproxy + +import ( + "context" + "crypto/tls" + "errors" + "log" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" + "imuslab.com/zoraxy/mod/dynamicproxy/redirection" + "imuslab.com/zoraxy/mod/geodb" + "imuslab.com/zoraxy/mod/reverseproxy" + "imuslab.com/zoraxy/mod/statistic" + "imuslab.com/zoraxy/mod/tlscert" +) + +/* +Zoraxy Dynamic Proxy +*/ +type RouterOption struct { + Port int + UseTls bool + ForceHttpsRedirect bool + TlsManager *tlscert.Manager + RedirectRuleTable *redirection.RuleTable + GeodbStore *geodb.Store + StatisticCollector *statistic.Collector +} + +type Router struct { + Option *RouterOption + ProxyEndpoints *sync.Map + SubdomainEndpoint *sync.Map + Running bool + Root *ProxyEndpoint + mux http.Handler + server *http.Server + tlsListener net.Listener + routingRules []*RoutingRule + + tlsRedirectStop chan bool +} + +type ProxyEndpoint struct { + Root string + Domain string + RequireTLS bool + Proxy *dpcore.ReverseProxy `json:"-"` +} + +type SubdomainEndpoint struct { + MatchingDomain string + Domain string + RequireTLS bool + Proxy *reverseproxy.ReverseProxy `json:"-"` +} + +type ProxyHandler struct { + Parent *Router +} + +func NewDynamicProxy(option RouterOption) (*Router, error) { + proxyMap := sync.Map{} + domainMap := sync.Map{} + thisRouter := Router{ + Option: &option, + ProxyEndpoints: &proxyMap, + SubdomainEndpoint: &domainMap, + Running: false, + server: nil, + routingRules: []*RoutingRule{}, + } + + thisRouter.mux = &ProxyHandler{ + Parent: &thisRouter, + } + + return &thisRouter, nil +} + +// Update TLS setting in runtime. Will restart the proxy server +// if it is already running in the background +func (router *Router) UpdateTLSSetting(tlsEnabled bool) { + router.Option.UseTls = tlsEnabled + router.Restart() +} + +// Update https redirect, which will require updates +func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) { + router.Option.ForceHttpsRedirect = useRedirect + router.Restart() +} + +// Start the dynamic routing +func (router *Router) StartProxyService() error { + //Create a new server object + if router.server != nil { + return errors.New("Reverse proxy server already running") + } + + if router.Root == nil { + return errors.New("Reverse proxy router root not set") + } + + config := &tls.Config{ + GetCertificate: router.Option.TlsManager.GetCert, + } + + if router.Option.UseTls { + //Serve with TLS mode + ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config) + if err != nil { + log.Println(err) + router.Running = false + return err + } + router.tlsListener = ln + router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux} + router.Running = true + + if router.Option.Port != 80 && router.Option.ForceHttpsRedirect { + //Add a 80 to 443 redirector + httpServer := &http.Server{ + Addr: ":80", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + protocol := "https://" + if router.Option.Port == 443 { + http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect) + } else { + http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect) + } + + }), + ReadTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, + IdleTimeout: 120 * time.Second, + } + + log.Println("Starting HTTP-to-HTTPS redirector (port 80)") + + //Create a redirection stop channel + stopChan := make(chan bool) + + //Start a blocking wait for shutting down the http to https redirection server + go func() { + <-stopChan + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + httpServer.Shutdown(ctx) + log.Println("HTTP to HTTPS redirection listener stopped") + }() + + //Start the http server that listens to port 80 and redirect to 443 + go func() { + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + //Unable to startup port 80 listener. Handle shutdown process gracefully + stopChan <- true + log.Fatalf("Could not start server: %v\n", err) + } + }() + router.tlsRedirectStop = stopChan + } + + //Start the TLS server + log.Println("Reverse proxy service started in the background (TLS mode)") + go func() { + if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed { + log.Fatalf("Could not start server: %v\n", err) + } + }() + } else { + //Serve with non TLS mode + router.tlsListener = nil + router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux} + router.Running = true + log.Println("Reverse proxy service started in the background (Plain HTTP mode)") + go func() { + router.server.ListenAndServe() + //log.Println("[DynamicProxy] " + err.Error()) + }() + } + + return nil +} + +func (router *Router) StopProxyService() error { + if router.server == nil { + return errors.New("Reverse proxy server already stopped") + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err := router.server.Shutdown(ctx) + if err != nil { + return err + } + + if router.tlsListener != nil { + router.tlsListener.Close() + } + + if router.tlsRedirectStop != nil { + router.tlsRedirectStop <- true + } + + //Discard the server object + router.tlsListener = nil + router.server = nil + router.Running = false + router.tlsRedirectStop = nil + return nil +} + +// Restart the current router if it is running. +// Startup the server if it is not running initially +func (router *Router) Restart() error { + //Stop the router if it is already running + if router.Running { + err := router.StopProxyService() + if err != nil { + return err + } + } + + //Start the server + err := router.StartProxyService() + return err +} + +/* + Check if a given request is accessed via a proxied subdomain +*/ + +func (router *Router) IsProxiedSubdomain(r *http.Request) bool { + hostname := r.Header.Get("X-Forwarded-Host") + if hostname == "" { + hostname = r.Host + } + hostname = strings.Split(hostname, ":")[0] + subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname) + return subdEndpoint != nil +} + +/* +Add an URL into a custom proxy services +*/ +func (router *Router) AddVirtualDirectoryProxyService(rootname string, domain string, requireTLS bool) error { + + if domain[len(domain)-1:] == "/" { + domain = domain[:len(domain)-1] + } + + /* + if rootname[len(rootname)-1:] == "/" { + rootname = rootname[:len(rootname)-1] + } + */ + + webProxyEndpoint := domain + if requireTLS { + webProxyEndpoint = "https://" + webProxyEndpoint + } else { + webProxyEndpoint = "http://" + webProxyEndpoint + } + //Create a new proxy agent for this root + path, err := url.Parse(webProxyEndpoint) + if err != nil { + return err + } + + proxy := dpcore.NewDynamicProxyCore(path, rootname) + + endpointObject := ProxyEndpoint{ + Root: rootname, + Domain: domain, + RequireTLS: requireTLS, + Proxy: proxy, + } + + router.ProxyEndpoints.Store(rootname, &endpointObject) + + log.Println("Adding Proxy Rule: ", rootname+" to "+domain) + return nil +} + +/* +Remove routing from RP +*/ +func (router *Router) RemoveProxy(ptype string, key string) error { + //fmt.Println(ptype, key) + if ptype == "vdir" { + router.ProxyEndpoints.Delete(key) + return nil + } else if ptype == "subd" { + router.SubdomainEndpoint.Delete(key) + return nil + } + return errors.New("invalid ptype") +} + +/* +Add an default router for the proxy server +*/ +func (router *Router) SetRootProxy(proxyLocation string, requireTLS bool) error { + if proxyLocation[len(proxyLocation)-1:] == "/" { + proxyLocation = proxyLocation[:len(proxyLocation)-1] + } + + webProxyEndpoint := proxyLocation + if requireTLS { + webProxyEndpoint = "https://" + webProxyEndpoint + } else { + webProxyEndpoint = "http://" + webProxyEndpoint + } + //Create a new proxy agent for this root + path, err := url.Parse(webProxyEndpoint) + if err != nil { + return err + } + + proxy := dpcore.NewDynamicProxyCore(path, "") + + rootEndpoint := ProxyEndpoint{ + Root: "/", + Domain: proxyLocation, + RequireTLS: requireTLS, + Proxy: proxy, + } + + router.Root = &rootEndpoint + return nil +} + +// Helpers to export the syncmap for easier processing +func (r *Router) GetSDProxyEndpointsAsMap() map[string]*SubdomainEndpoint { + m := make(map[string]*SubdomainEndpoint) + r.SubdomainEndpoint.Range(func(key, value interface{}) bool { + k, ok := key.(string) + if !ok { + return true + } + v, ok := value.(*SubdomainEndpoint) + if !ok { + return true + } + m[k] = v + return true + }) + return m +} + +func (r *Router) GetVDProxyEndpointsAsMap() map[string]*ProxyEndpoint { + m := make(map[string]*ProxyEndpoint) + r.ProxyEndpoints.Range(func(key, value interface{}) bool { + k, ok := key.(string) + if !ok { + return true + } + v, ok := value.(*ProxyEndpoint) + if !ok { + return true + } + m[k] = v + return true + }) + return m +}
src/mod/dynamicproxy/proxyRequestHandler.go+155 −0 added@@ -0,0 +1,155 @@ +package dynamicproxy + +import ( + "errors" + "log" + "net" + "net/http" + "net/url" + "strings" + + "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" + "imuslab.com/zoraxy/mod/geodb" + "imuslab.com/zoraxy/mod/statistic" + "imuslab.com/zoraxy/mod/websocketproxy" +) + +func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *ProxyEndpoint { + var targetProxyEndpoint *ProxyEndpoint = nil + router.ProxyEndpoints.Range(func(key, value interface{}) bool { + rootname := key.(string) + if strings.HasPrefix(requestURI, rootname) { + thisProxyEndpoint := value.(*ProxyEndpoint) + targetProxyEndpoint = thisProxyEndpoint + } + return true + }) + + return targetProxyEndpoint +} + +func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *SubdomainEndpoint { + var targetSubdomainEndpoint *SubdomainEndpoint = nil + ep, ok := router.SubdomainEndpoint.Load(hostname) + if ok { + targetSubdomainEndpoint = ep.(*SubdomainEndpoint) + } + + return targetSubdomainEndpoint +} + +func (router *Router) rewriteURL(rooturl string, requestURL string) string { + rewrittenURL := requestURL + rewrittenURL = strings.TrimPrefix(rewrittenURL, strings.TrimSuffix(rooturl, "/")) + return rewrittenURL +} + +func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *SubdomainEndpoint) { + r.Header.Set("X-Forwarded-Host", r.Host) + requestURL := r.URL.String() + if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { + //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin + r.Header.Set("A-Upgrade", "websocket") + wsRedirectionEndpoint := target.Domain + if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { + //Append / to the end of the redirection endpoint if not exists + wsRedirectionEndpoint = wsRedirectionEndpoint + "/" + } + if len(requestURL) > 0 && requestURL[:1] == "/" { + //Remove starting / from request URL if exists + requestURL = requestURL[1:] + } + u, _ := url.Parse("ws://" + wsRedirectionEndpoint + requestURL) + if target.RequireTLS { + u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL) + } + h.logRequest(r, true, 101, "subdomain-websocket", target.Domain) + wspHandler := websocketproxy.NewProxy(u) + wspHandler.ServeHTTP(w, r) + return + } + + r.Host = r.URL.Host + err := target.Proxy.ServeHTTP(w, r) + var dnsError *net.DNSError + if err != nil { + if errors.As(err, &dnsError) { + http.ServeFile(w, r, "./web/hosterror.html") + log.Println(err.Error()) + h.logRequest(r, false, 404, "subdomain-http", target.Domain) + } else { + http.ServeFile(w, r, "./web/rperror.html") + log.Println(err.Error()) + h.logRequest(r, false, 521, "subdomain-http", target.Domain) + } + } + + h.logRequest(r, true, 200, "subdomain-http", target.Domain) +} + +func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) { + rewriteURL := h.Parent.rewriteURL(target.Root, r.RequestURI) + r.URL, _ = url.Parse(rewriteURL) + + r.Header.Set("X-Forwarded-Host", r.Host) + if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { + //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin + r.Header.Set("A-Upgrade", "websocket") + wsRedirectionEndpoint := target.Domain + if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { + wsRedirectionEndpoint = wsRedirectionEndpoint + "/" + } + u, _ := url.Parse("ws://" + wsRedirectionEndpoint + r.URL.String()) + if target.RequireTLS { + u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String()) + } + h.logRequest(r, true, 101, "vdir-websocket", target.Domain) + wspHandler := websocketproxy.NewProxy(u) + wspHandler.ServeHTTP(w, r) + return + } + + originalHostHeader := r.Host + r.Host = r.URL.Host + err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ + ProxyDomain: target.Domain, + OriginalHost: originalHostHeader, + UseTLS: target.RequireTLS, + PathPrefix: target.Root, + }) + + var dnsError *net.DNSError + if err != nil { + if errors.As(err, &dnsError) { + http.ServeFile(w, r, "./web/hosterror.html") + log.Println(err.Error()) + h.logRequest(r, false, 404, "vdir-http", target.Domain) + } else { + http.ServeFile(w, r, "./web/rperror.html") + log.Println(err.Error()) + h.logRequest(r, false, 521, "vdir-http", target.Domain) + } + } + h.logRequest(r, true, 200, "vdir-http", target.Domain) + +} + +func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) { + if h.Parent.Option.StatisticCollector != nil { + go func() { + requestInfo := statistic.RequestInfo{ + IpAddr: geodb.GetRequesterIP(r), + RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r), + Succ: succ, + StatusCode: statusCode, + ForwardType: forwardType, + Referer: r.Referer(), + UserAgent: r.UserAgent(), + RequestURL: r.Host + r.RequestURI, + Target: target, + } + h.Parent.Option.StatisticCollector.RecordRequest(requestInfo) + }() + + } +}
src/mod/dynamicproxy/redirection/handler.go+56 −0 added@@ -0,0 +1,56 @@ +package redirection + +import ( + "log" + "net/http" + "strings" +) + +/* + handler.go + + This script store the handlers use for handling + redirection request +*/ + +// Check if a request URL is a redirectable URI +func (t *RuleTable) IsRedirectable(r *http.Request) bool { + requestPath := r.Host + r.URL.Path + rr := t.MatchRedirectRule(requestPath) + return rr != nil +} + +// Handle the redirect request, return after calling this function to prevent +// multiple write to the response writer +// Return the status code of the redirection handling +func (t *RuleTable) HandleRedirect(w http.ResponseWriter, r *http.Request) int { + requestPath := r.Host + r.URL.Path + rr := t.MatchRedirectRule(requestPath) + if rr != nil { + redirectTarget := rr.TargetURL + //Always pad a / at the back of the target URL + if redirectTarget[len(redirectTarget)-1:] != "/" { + redirectTarget += "/" + } + if rr.ForwardChildpath { + //Remove the first / in the path + redirectTarget += strings.TrimPrefix(r.URL.Path, "/") + if r.URL.RawQuery != "" { + redirectTarget += "?" + r.URL.RawQuery + } + } + + if !strings.HasPrefix(redirectTarget, "http://") && !strings.HasPrefix(redirectTarget, "https://") { + redirectTarget = "http://" + redirectTarget + } + + http.Redirect(w, r, redirectTarget, rr.StatusCode) + return rr.StatusCode + } else { + //Invalid usage + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("500 - Internal Server Error")) + log.Println("Target request URL do not have matching redirect rule. Check with IsRedirectable before calling HandleRedirect!") + return 500 + } +}
src/mod/dynamicproxy/redirection/redirection.go+162 −0 added@@ -0,0 +1,162 @@ +package redirection + +import ( + "encoding/json" + "log" + "os" + "path" + "path/filepath" + "strings" + "sync" + + "imuslab.com/zoraxy/mod/utils" +) + +type RuleTable struct { + configPath string //The location where the redirection rules is stored + rules sync.Map //Store the redirection rules for this reverse proxy instance +} + +type RedirectRules struct { + RedirectURL string //The matching URL to redirect + TargetURL string //The destination redirection url + ForwardChildpath bool //Also redirect the pathname + StatusCode int //Status Code for redirection +} + +func NewRuleTable(configPath string) (*RuleTable, error) { + thisRuleTable := RuleTable{ + rules: sync.Map{}, + configPath: configPath, + } + //Load all the rules from the config path + if !utils.FileExists(configPath) { + os.MkdirAll(configPath, 0775) + } + + // Load all the *.json from the configPath + files, err := filepath.Glob(filepath.Join(configPath, "*.json")) + if err != nil { + return nil, err + } + + // Parse the json content into RedirectRules + var rules []*RedirectRules + for _, file := range files { + b, err := os.ReadFile(file) + if err != nil { + continue + } + + thisRule := RedirectRules{} + + err = json.Unmarshal(b, &thisRule) + if err != nil { + continue + } + + rules = append(rules, &thisRule) + } + + //Map the rules into the sync map + for _, rule := range rules { + log.Println("Redirection rule added: " + rule.RedirectURL + " -> " + rule.TargetURL) + thisRuleTable.rules.Store(rule.RedirectURL, rule) + } + + return &thisRuleTable, nil +} + +func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardPathname bool, statusCode int) error { + // Create a new RedirectRules object with the given parameters + newRule := &RedirectRules{ + RedirectURL: redirectURL, + TargetURL: destURL, + ForwardChildpath: forwardPathname, + StatusCode: statusCode, + } + + // Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_" + filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json" + + // Create the full file path by joining the t.configPath with the filename + filepath := path.Join(t.configPath, filename) + + // Create a new file for writing the JSON data + file, err := os.Create(filepath) + if err != nil { + log.Printf("Error creating file %s: %s", filepath, err) + return err + } + defer file.Close() + + // Encode the RedirectRules object to JSON and write it to the file + err = json.NewEncoder(file).Encode(newRule) + if err != nil { + log.Printf("Error encoding JSON to file %s: %s", filepath, err) + return err + } + + // Store the RedirectRules object in the sync.Map + t.rules.Store(redirectURL, newRule) + + return nil +} + +func (t *RuleTable) DeleteRedirectRule(redirectURL string) error { + // Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_" + filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json" + + // Create the full file path by joining the t.configPath with the filename + filepath := path.Join(t.configPath, filename) + + // Check if the file exists + if _, err := os.Stat(filepath); os.IsNotExist(err) { + return nil // File doesn't exist, nothing to delete + } + + // Delete the file + if err := os.Remove(filepath); err != nil { + log.Printf("Error deleting file %s: %s", filepath, err) + return err + } + + // Delete the key-value pair from the sync.Map + t.rules.Delete(redirectURL) + + return nil +} + +// Get a list of all the redirection rules +func (t *RuleTable) GetAllRedirectRules() []*RedirectRules { + rules := []*RedirectRules{} + t.rules.Range(func(key, value interface{}) bool { + r, ok := value.(*RedirectRules) + if ok { + rules = append(rules, r) + } + return true + }) + return rules +} + +// Check if a given request URL matched any of the redirection rule +func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules { + // Iterate through all the keys in the rules map + var targetRedirectionRule *RedirectRules = nil + var maxMatch int = 0 + + t.rules.Range(func(key interface{}, value interface{}) bool { + // Check if the requested URL starts with the key as a prefix + if strings.HasPrefix(requestedURL, key.(string)) { + // This request URL matched the domain + if len(key.(string)) > maxMatch { + maxMatch = len(key.(string)) + targetRedirectionRule = value.(*RedirectRules) + } + } + return true + }) + + return targetRedirectionRule +}
src/mod/dynamicproxy/Server.go+85 −0 added@@ -0,0 +1,85 @@ +package dynamicproxy + +import ( + "net/http" + "os" + "strings" + + "imuslab.com/zoraxy/mod/geodb" +) + +/* + Server.go + + Main server for dynamic proxy core +*/ + +func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + //Check if this ip is in blacklist + clientIpAddr := geodb.GetRequesterIP(r) + if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusForbidden) + template, err := os.ReadFile("./web/forbidden.html") + if err != nil { + w.Write([]byte("403 - Forbidden")) + } else { + w.Write(template) + } + h.logRequest(r, false, 403, "blacklist", "") + return + } + + //Check if this is a redirection url + if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) { + statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r) + h.logRequest(r, statusCode != 500, statusCode, "redirect", "") + return + } + + //Check if there are external routing rule matches. + //If yes, route them via external rr + matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r) + if matchedRoutingRule != nil { + //Matching routing rule found. Let the sub-router handle it + matchedRoutingRule.Route(w, r) + return + } + + //Extract request host to see if it is virtual directory or subdomain + domainOnly := r.Host + if strings.Contains(r.Host, ":") { + hostPath := strings.Split(r.Host, ":") + domainOnly = hostPath[0] + } + + if strings.Contains(r.Host, ".") { + //This might be a subdomain. See if there are any subdomain proxy router for this + //Remove the port if any + + sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly) + if sep != nil { + h.subdomainRequest(w, r, sep) + return + } + } + + //Clean up the request URI + proxyingPath := strings.TrimSpace(r.RequestURI) + targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath) + if targetProxyEndpoint != nil { + h.proxyRequest(w, r, targetProxyEndpoint) + } else if !strings.HasSuffix(proxyingPath, "/") { + potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/") + + if potentialProxtEndpoint != nil { + //Missing tailing slash. Redirect to target proxy endpoint + http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect) + //h.proxyRequest(w, r, potentialProxtEndpoint) + } else { + h.proxyRequest(w, r, h.Parent.Root) + } + } else { + h.proxyRequest(w, r, h.Parent.Root) + } +}
src/mod/dynamicproxy/special.go+85 −0 added@@ -0,0 +1,85 @@ +package dynamicproxy + +import ( + "errors" + "net/http" +) + +/* + Special.go + + This script handle special routing rules + by external modules +*/ + +type RoutingRule struct { + ID string + MatchRule func(r *http.Request) bool + RoutingHandler http.Handler + Enabled bool +} + +//Router functions +//Check if a routing rule exists given its id +func (router *Router) GetRoutingRuleById(rrid string) (*RoutingRule, error) { + for _, rr := range router.routingRules { + if rr.ID == rrid { + return rr, nil + } + } + + return nil, errors.New("routing rule with given id not found") +} + +//Add a routing rule to the router +func (router *Router) AddRoutingRules(rr *RoutingRule) error { + _, err := router.GetRoutingRuleById(rr.ID) + if err != nil { + //routing rule with given id already exists + return err + } + + router.routingRules = append(router.routingRules, rr) + return nil +} + +//Remove a routing rule from the router +func (router *Router) RemoveRoutingRule(rrid string) { + newRoutingRules := []*RoutingRule{} + for _, rr := range router.routingRules { + if rr.ID != rrid { + newRoutingRules = append(newRoutingRules, rr) + } + } + + router.routingRules = newRoutingRules +} + +//Get all routing rules +func (router *Router) GetAllRoutingRules() []*RoutingRule { + return router.routingRules +} + +//Get the matching routing rule that describe this request. +//Return nil if no routing rule is match +func (router *Router) GetMatchingRoutingRule(r *http.Request) *RoutingRule { + for _, thisRr := range router.routingRules { + if thisRr.IsMatch(r) { + return thisRr + } + } + return nil +} + +//Routing Rule functions +//Check if a request object match the +func (e *RoutingRule) IsMatch(r *http.Request) bool { + if !e.Enabled { + return false + } + return e.MatchRule(r) +} + +func (e *RoutingRule) Route(w http.ResponseWriter, r *http.Request) { + e.RoutingHandler.ServeHTTP(w, r) +}
src/mod/dynamicproxy/subdomain.go+44 −0 added@@ -0,0 +1,44 @@ +package dynamicproxy + +import ( + "log" + "net/url" + + "imuslab.com/zoraxy/mod/reverseproxy" +) + +/* + Add an URL intoa custom subdomain service + +*/ + +func (router *Router) AddSubdomainRoutingService(hostnameWithSubdomain string, domain string, requireTLS bool) error { + if domain[len(domain)-1:] == "/" { + domain = domain[:len(domain)-1] + } + + webProxyEndpoint := domain + if requireTLS { + webProxyEndpoint = "https://" + webProxyEndpoint + } else { + webProxyEndpoint = "http://" + webProxyEndpoint + } + + //Create a new proxy agent for this root + path, err := url.Parse(webProxyEndpoint) + if err != nil { + return err + } + + proxy := reverseproxy.NewReverseProxy(path) + + router.SubdomainEndpoint.Store(hostnameWithSubdomain, &SubdomainEndpoint{ + MatchingDomain: hostnameWithSubdomain, + Domain: domain, + RequireTLS: requireTLS, + Proxy: proxy, + }) + + log.Println("Adding Subdomain Rule: ", hostnameWithSubdomain+" to "+domain) + return nil +}
src/mod/email/email.go+60 −0 added@@ -0,0 +1,60 @@ +package email + +import ( + "net/smtp" + "strconv" +) + +/* + Email.go + + This script handle mailing services using SMTP protocol +*/ + +type Sender struct { + Hostname string //E.g. mail.gandi.net + Domain string //E.g. arozos.com + Port int //E.g. 587 + Username string //Username of the email account + Password string //Password of the email account + SenderAddr string //e.g. admin@arozos.com +} + +//Create a new email sender object +func NewEmailSender(hostname string, domain string, port int, username string, password string, senderAddr string) *Sender { + return &Sender{ + Hostname: hostname, + Domain: domain, + Port: port, + Username: username, + Password: password, + SenderAddr: senderAddr, + } +} + +/* + Send a email to a reciving addr + Example Usage: + SendEmail( + test@example.com, + "Free donuts", + "Come get your free donuts on this Sunday!" + ) +*/ +func (s *Sender) SendEmail(to string, subject string, content string) error { + //Parse the email content + msg := []byte("To: " + to + "\n" + + "From: Zoraxy <" + s.SenderAddr + ">\n" + + "Subject: " + subject + "\n" + + "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" + + content + "\n\n") + + //Login to the SMTP server + auth := smtp.PlainAuth("", s.Username+"@"+s.Domain, s.Password, s.Hostname) + err := smtp.SendMail(s.Hostname+":"+strconv.Itoa(s.Port), auth, s.SenderAddr, []string{to}, msg) + if err != nil { + return err + } + + return nil +}
src/mod/ganserv/authkey.go+80 −0 added@@ -0,0 +1,80 @@ +package ganserv + +import ( + "errors" + "log" + "os" + "runtime" + "strings" +) + +func TryLoadorAskUserForAuthkey() (string, error) { + //Check for zt auth token + value, exists := os.LookupEnv("ZT_AUTH") + if !exists { + log.Println("Environment variable ZT_AUTH not defined. Trying to load authtoken from file.") + } else { + return value, nil + } + + authKey := "" + if runtime.GOOS == "windows" { + if isAdmin() { + //Read the secret file directly + b, err := os.ReadFile("C:\\ProgramData\\ZeroTier\\One\\authtoken.secret") + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = string(b) + } else { + log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error()) + } + } else { + //Elavate the permission to admin + ak, err := readAuthTokenAsAdmin() + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = ak + } else { + log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error()) + } + } + + } else if runtime.GOOS == "linux" { + if isAdmin() { + //Try to read from source using sudo + ak, err := readAuthTokenAsAdmin() + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = strings.TrimSpace(ak) + } else { + log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error()) + } + } else { + //Try read from source + b, err := os.ReadFile("/var/lib/zerotier-one/authtoken.secret") + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = string(b) + } else { + log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error()) + } + } + + } else if runtime.GOOS == "darwin" { + b, err := os.ReadFile("/Library/Application Support/ZeroTier/One/authtoken.secret") + if err == nil { + log.Println("Zerotier authkey loaded") + authKey = string(b) + } else { + log.Println("Unable to read authkey at /Library/Application Support/ZeroTier/One/authtoken.secret ", err.Error()) + } + } + + authKey = strings.TrimSpace(authKey) + + if authKey == "" { + return "", errors.New("Unable to load authkey from file") + } + + return authKey, nil +}
src/mod/ganserv/authkeyLinux.go+37 −0 added@@ -0,0 +1,37 @@ +//go:build linux +// +build linux + +package ganserv + +import ( + "os" + "os/exec" + "os/user" + "strings" + + "imuslab.com/zoraxy/mod/utils" +) + +func readAuthTokenAsAdmin() (string, error) { + if utils.FileExists("./authtoken.secret") { + authKey, err := os.ReadFile("./authtoken.secret") + if err == nil { + return strings.TrimSpace(string(authKey)), nil + } + } + + cmd := exec.Command("sudo", "cat", "/var/lib/zerotier-one/authtoken.secret") + output, err := cmd.Output() + if err != nil { + return "", err + } + return string(output), nil +} + +func isAdmin() bool { + currentUser, err := user.Current() + if err != nil { + return false + } + return currentUser.Username == "root" +}
src/mod/ganserv/authkeyWin.go+73 −0 added@@ -0,0 +1,73 @@ +//go:build windows +// +build windows + +package ganserv + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + "syscall" + "time" + + "golang.org/x/sys/windows" + "imuslab.com/zoraxy/mod/utils" +) + +// Use admin permission to read auth token on Windows +func readAuthTokenAsAdmin() (string, error) { + //Check if the previous startup already extracted the authkey + if utils.FileExists("./authtoken.secret") { + authKey, err := os.ReadFile("./authtoken.secret") + if err == nil { + return strings.TrimSpace(string(authKey)), nil + } + } + + verb := "runas" + exe := "cmd.exe" + cwd, _ := os.Getwd() + + output, _ := filepath.Abs(filepath.Join("./", "authtoken.secret")) + os.WriteFile(output, []byte(""), 0775) + args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"") + + verbPtr, _ := syscall.UTF16PtrFromString(verb) + exePtr, _ := syscall.UTF16PtrFromString(exe) + cwdPtr, _ := syscall.UTF16PtrFromString(cwd) + argPtr, _ := syscall.UTF16PtrFromString(args) + + var showCmd int32 = 1 //SW_NORMAL + + err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd) + if err != nil { + return "", err + } + + log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData") + retry := 0 + time.Sleep(3 * time.Second) + for !utils.FileExists("./authtoken.secret") && retry < 10 { + time.Sleep(3 * time.Second) + log.Println("Waiting for ZeroTier authtoken extraction...") + retry++ + } + + authKey, err := os.ReadFile("./authtoken.secret") + if err != nil { + return "", err + } + + return strings.TrimSpace(string(authKey)), nil +} + +// Check if admin on Windows +func isAdmin() bool { + _, err := os.Open("\\\\.\\PHYSICALDRIVE0") + if err != nil { + return false + } + return true +}
src/mod/ganserv/ganserv.go+128 −0 added@@ -0,0 +1,128 @@ +package ganserv + +import ( + "net" + + "imuslab.com/zoraxy/mod/database" +) + +/* + Global Area Network + Server side implementation + + This module do a few things to help manage + the system GANs + + - Provide DHCP assign to client + - Provide a list of connected nodes in the same VLAN + - Provide proxy of packet if the target VLAN is online but not reachable + + Also provide HTTP Handler functions for management + - Create Network + - Update Network Properties (Name / Desc) + - Delete Network + + - Authorize Node + - Deauthorize Node + - Set / Get Network Prefered Subnet Mask + - Handle Node ping +*/ + +type Node struct { + Auth bool //If the node is authorized in this network + ClientID string //The client ID + MAC string //The tap MAC this client is using + Name string //Name of the client in this network + Description string //Description text + ManagedIP net.IP //The IP address assigned by this network + LastSeen int64 //Last time it is seen from this host + ClientVersion string //Client application version + PublicIP net.IP //Public IP address as seen from this host +} + +type Network struct { + UID string //UUID of the network, must be a 16 char random ASCII string + Name string //Name of the network, ASCII only + Description string //Description of the network + CIDR string //The subnet masked use by this network + Nodes []*Node //The nodes currently attached in this network +} + +type NetworkManagerOptions struct { + Database *database.Database + AuthToken string + ApiPort int +} + +type NetworkMetaData struct { + Desc string +} + +type MemberMetaData struct { + Name string +} + +type NetworkManager struct { + authToken string + apiPort int + ControllerID string + option *NetworkManagerOptions + networksMetadata map[string]NetworkMetaData +} + +// Create a new GAN manager +func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager { + option.Database.NewTable("ganserv") + + //Load network metadata + networkMeta := map[string]NetworkMetaData{} + if option.Database.KeyExists("ganserv", "networkmeta") { + option.Database.Read("ganserv", "networkmeta", &networkMeta) + } + + //Start the zerotier instance if not exists + + //Get controller info + instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort) + if err != nil { + return &NetworkManager{ + authToken: option.AuthToken, + apiPort: option.ApiPort, + ControllerID: "", + option: option, + networksMetadata: networkMeta, + } + } + + return &NetworkManager{ + authToken: option.AuthToken, + apiPort: option.ApiPort, + ControllerID: instanceInfo.Address, + option: option, + networksMetadata: networkMeta, + } +} + +func (m *NetworkManager) GetNetworkMetaData(netid string) *NetworkMetaData { + md, ok := m.networksMetadata[netid] + if !ok { + return &NetworkMetaData{} + } + + return &md +} + +func (m *NetworkManager) WriteNetworkMetaData(netid string, meta *NetworkMetaData) { + m.networksMetadata[netid] = *meta + m.option.Database.Write("ganserv", "networkmeta", m.networksMetadata) +} + +func (m *NetworkManager) GetMemberMetaData(netid string, memid string) *MemberMetaData { + thisMemberData := MemberMetaData{} + m.option.Database.Read("ganserv", "memberdata_"+netid+"_"+memid, &thisMemberData) + return &thisMemberData +} + +func (m *NetworkManager) WriteMemeberMetaData(netid string, memid string, meta *MemberMetaData) { + m.option.Database.Write("ganserv", "memberdata_"+netid+"_"+memid, meta) +}
src/mod/ganserv/handlers.go+428 −0 added@@ -0,0 +1,428 @@ +package ganserv + +import ( + "encoding/json" + "net" + "net/http" + "regexp" + "strings" + + "imuslab.com/zoraxy/mod/utils" +) + +func (m *NetworkManager) HandleGetNodeID(w http.ResponseWriter, r *http.Request) { + if m.ControllerID == "" { + //Node id not exists. Check again + instanceInfo, err := getControllerInfo(m.option.AuthToken, m.option.ApiPort) + if err != nil { + utils.SendErrorResponse(w, "unable to access node id information") + return + } + + m.ControllerID = instanceInfo.Address + } + + js, _ := json.Marshal(m.ControllerID) + utils.SendJSONResponse(w, string(js)) +} + +func (m *NetworkManager) HandleAddNetwork(w http.ResponseWriter, r *http.Request) { + networkInfo, err := m.createNetwork() + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + //Network created. Assign it the standard network settings + err = m.configureNetwork(networkInfo.Nwid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24") + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + // Return the new network ID + js, _ := json.Marshal(networkInfo.Nwid) + utils.SendJSONResponse(w, string(js)) +} + +func (m *NetworkManager) HandleRemoveNetwork(w http.ResponseWriter, r *http.Request) { + networkID, err := utils.PostPara(r, "id") + if err != nil { + utils.SendErrorResponse(w, "invalid or empty network id given") + return + } + + if !m.networkExists(networkID) { + utils.SendErrorResponse(w, "network id not exists") + return + } + + err = m.deleteNetwork(networkID) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + } + + utils.SendOK(w) +} + +func (m *NetworkManager) HandleListNetwork(w http.ResponseWriter, r *http.Request) { + netid, _ := utils.GetPara(r, "netid") + if netid != "" { + targetNetInfo, err := m.getNetworkInfoById(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(targetNetInfo) + utils.SendJSONResponse(w, string(js)) + + } else { + // Return the list of networks as JSON + networkIds, err := m.listNetworkIds() + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + networkInfos := []*NetworkInfo{} + for _, id := range networkIds { + thisNetInfo, err := m.getNetworkInfoById(id) + if err == nil { + networkInfos = append(networkInfos, thisNetInfo) + } + } + + js, _ := json.Marshal(networkInfos) + utils.SendJSONResponse(w, string(js)) + } + +} + +func (m *NetworkManager) HandleNetworkNaming(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "network id not given") + return + } + + if !m.networkExists(netid) { + utils.SendErrorResponse(w, "network not eixsts") + } + + newName, _ := utils.PostPara(r, "name") + newDesc, _ := utils.PostPara(r, "desc") + if newName != "" && newDesc != "" { + //Strip away html from name and desc + re := regexp.MustCompile("<[^>]*>") + newName := re.ReplaceAllString(newName, "") + newDesc := re.ReplaceAllString(newDesc, "") + + //Set the new network name and desc + err = m.setNetworkNameAndDescription(netid, newName, newDesc) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) + } else { + //Get current name and description + name, desc, err := m.getNetworkNameAndDescription(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal([]string{name, desc}) + utils.SendJSONResponse(w, string(js)) + } +} + +func (m *NetworkManager) HandleNetworkDetails(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "netid not given") + return + } + + targetNetwork, err := m.getNetworkInfoById(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(targetNetwork) + utils.SendJSONResponse(w, string(js)) +} + +func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "netid not given") + return + } + cidr, err := utils.PostPara(r, "cidr") + if err != nil { + utils.SendErrorResponse(w, "cidr not given") + return + } + ipstart, err := utils.PostPara(r, "ipstart") + if err != nil { + utils.SendErrorResponse(w, "ipstart not given") + return + } + ipend, err := utils.PostPara(r, "ipend") + if err != nil { + utils.SendErrorResponse(w, "ipend not given") + return + } + + //Validate the CIDR is real, the ip range is within the CIDR range + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + utils.SendErrorResponse(w, "invalid cidr string given") + return + } + + startIP := net.ParseIP(ipstart) + endIP := net.ParseIP(ipend) + if startIP == nil || endIP == nil { + utils.SendErrorResponse(w, "invalid start or end ip given") + return + } + + withinRange := ipnet.Contains(startIP) && ipnet.Contains(endIP) + if !withinRange { + utils.SendErrorResponse(w, "given CIDR did not cover all of the start to end ip range") + return + } + + err = m.configureNetwork(netid, startIP.String(), endIP.String(), strings.TrimSpace(cidr)) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +//Handle listing of network members. Set details=true for listing all details +func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) { + netid, err := utils.GetPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "netid is empty") + return + } + + details, _ := utils.GetPara(r, "detail") + + memberIds, err := m.getNetworkMembers(netid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + if details == "" { + //Only show client ids + js, _ := json.Marshal(memberIds) + utils.SendJSONResponse(w, string(js)) + } else { + //Show detail members info + detailMemberInfo := []*MemberInfo{} + for _, thisMemberId := range memberIds { + memInfo, err := m.getNetworkMemberInfo(netid, thisMemberId) + if err == nil { + detailMemberInfo = append(detailMemberInfo, memInfo) + } + } + + js, _ := json.Marshal(detailMemberInfo) + utils.SendJSONResponse(w, string(js)) + } +} + +//Handle Authorization of members +func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + memberid, err := utils.PostPara(r, "memid") + if err != nil { + utils.SendErrorResponse(w, "memid not set") + return + } + + //Check if the target memeber exists + if !m.memberExistsInNetwork(netid, memberid) { + utils.SendErrorResponse(w, "member not exists in given network") + return + } + + setAuthorized, err := utils.PostPara(r, "auth") + if err != nil || setAuthorized == "" { + //Get the member authorization state + memberInfo, err := m.getNetworkMemberInfo(netid, memberid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(memberInfo.Authorized) + utils.SendJSONResponse(w, string(js)) + } else if setAuthorized == "true" { + m.AuthorizeMember(netid, memberid, true) + } else if setAuthorized == "false" { + m.AuthorizeMember(netid, memberid, false) + } else { + utils.SendErrorResponse(w, "unknown operation state: "+setAuthorized) + } +} + +//Handle Delete or Add IP for a member in a network +func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + memberid, err := utils.PostPara(r, "memid") + if err != nil { + utils.SendErrorResponse(w, "memid not set") + return + } + + opr, err := utils.PostPara(r, "opr") + if err != nil { + utils.SendErrorResponse(w, "opr not defined") + return + } + + targetip, _ := utils.PostPara(r, "ip") + + memberInfo, err := m.getNetworkMemberInfo(netid, memberid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + if opr == "add" { + if targetip == "" { + utils.SendErrorResponse(w, "ip not set") + return + } + + if !isValidIPAddr(targetip) { + utils.SendErrorResponse(w, "ip address not valid") + return + } + + newIpList := append(memberInfo.IPAssignments, targetip) + err = m.setAssignedIps(netid, memberid, newIpList) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + utils.SendOK(w) + + } else if opr == "del" { + if targetip == "" { + utils.SendErrorResponse(w, "ip not set") + return + } + + //Delete user ip from the list + newIpList := []string{} + for _, thisIp := range memberInfo.IPAssignments { + if thisIp != targetip { + newIpList = append(newIpList, thisIp) + } + } + + err = m.setAssignedIps(netid, memberid, newIpList) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + utils.SendOK(w) + } else if opr == "get" { + js, _ := json.Marshal(memberInfo.IPAssignments) + utils.SendJSONResponse(w, string(js)) + } else { + utils.SendErrorResponse(w, "unsupported opr type: "+opr) + } +} + +//Handle naming for members +func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + memberid, err := utils.PostPara(r, "memid") + if err != nil { + utils.SendErrorResponse(w, "memid not set") + return + } + + if !m.memberExistsInNetwork(netid, memberid) { + utils.SendErrorResponse(w, "target member not exists in given network") + return + } + + //Read memeber data + targetMemberData := m.GetMemberMetaData(netid, memberid) + + newname, err := utils.PostPara(r, "name") + if err != nil { + //Send over the member data + js, _ := json.Marshal(targetMemberData) + utils.SendJSONResponse(w, string(js)) + } else { + //Write member data + targetMemberData.Name = newname + m.WriteMemeberMetaData(netid, memberid, targetMemberData) + utils.SendOK(w) + } +} + +//Handle delete of a given memver +func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) { + netid, err := utils.PostPara(r, "netid") + if err != nil { + utils.SendErrorResponse(w, "net id not set") + return + } + + memberid, err := utils.PostPara(r, "memid") + if err != nil { + utils.SendErrorResponse(w, "memid not set") + return + } + + //Check if that member is authorized. + memberInfo, err := m.getNetworkMemberInfo(netid, memberid) + if err != nil { + utils.SendErrorResponse(w, "member not exists in given GANet") + return + } + + if memberInfo.Authorized { + //Deauthorized this member before deleting + m.AuthorizeMember(netid, memberid, false) + } + + //Remove the memeber + err = m.deleteMember(netid, memberid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +}
src/mod/ganserv/network.go+39 −0 added@@ -0,0 +1,39 @@ +package ganserv + +import ( + "fmt" + "math/rand" + "net" + "time" +) + +//Get a random free IP from the pool +func (n *Network) GetRandomFreeIP() (net.IP, error) { + // Get all IP addresses in the subnet + ips, err := GetAllAddressFromCIDR(n.CIDR) + if err != nil { + return nil, err + } + + // Filter out used IPs + usedIPs := make(map[string]bool) + for _, node := range n.Nodes { + usedIPs[node.ManagedIP.String()] = true + } + availableIPs := []string{} + for _, ip := range ips { + if !usedIPs[ip] { + availableIPs = append(availableIPs, ip) + } + } + + // Randomly choose an available IP + if len(availableIPs) == 0 { + return nil, fmt.Errorf("no available IP") + } + rand.Seed(time.Now().UnixNano()) + randIndex := rand.Intn(len(availableIPs)) + pickedFreeIP := availableIPs[randIndex] + + return net.ParseIP(pickedFreeIP), nil +}
src/mod/ganserv/network_test.go+55 −0 added@@ -0,0 +1,55 @@ +package ganserv_test + +import ( + "fmt" + "net" + "strconv" + "testing" + + "imuslab.com/zoraxy/mod/ganserv" +) + +func TestGetRandomFreeIP(t *testing.T) { + n := ganserv.Network{ + CIDR: "172.16.0.0/12", + Nodes: []*ganserv.Node{ + { + Name: "nodeC1", + ManagedIP: net.ParseIP("172.16.1.142"), + }, + { + Name: "nodeC2", + ManagedIP: net.ParseIP("172.16.5.174"), + }, + }, + } + + // Call the function for 10 times + for i := 0; i < 10; i++ { + freeIP, err := n.GetRandomFreeIP() + fmt.Println("["+strconv.Itoa(i)+"] Free IP address assigned: ", freeIP) + + // Assert that no error occurred + if err != nil { + t.Errorf("Unexpected error: %s", err.Error()) + } + + // Assert that the returned IP is a valid IPv4 address + if freeIP.To4() == nil { + t.Errorf("Invalid IP address format: %s", freeIP.String()) + } + + // Assert that the returned IP is not already used by a node + for _, node := range n.Nodes { + if freeIP.Equal(node.ManagedIP) { + t.Errorf("Returned IP is already in use: %s", freeIP.String()) + } + } + + n.Nodes = append(n.Nodes, &ganserv.Node{ + Name: "NodeT" + strconv.Itoa(i), + ManagedIP: freeIP, + }) + } + +}
src/mod/ganserv/utils.go+55 −0 added@@ -0,0 +1,55 @@ +package ganserv + +import ( + "net" +) + +//Generate all ip address from a CIDR +func GetAllAddressFromCIDR(cidr string) ([]string, error) { + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + + var ips []string + for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) { + ips = append(ips, ip.String()) + } + // remove network address and broadcast address + return ips[1 : len(ips)-1], nil +} + +func inc(ip net.IP) { + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] > 0 { + break + } + } +} + +func isValidIPAddr(ipAddr string) bool { + ip := net.ParseIP(ipAddr) + if ip == nil { + return false + } + + return true +} + +func ipWithinCIDR(ipAddr string, cidr string) bool { + // Parse the CIDR string + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + return false + } + + // Parse the IP address + ip := net.ParseIP(ipAddr) + if ip == nil { + return false + } + + // Check if the IP address is in the CIDR range + return ipNet.Contains(ip) +}
src/mod/ganserv/zerotier.go+622 −0 added@@ -0,0 +1,622 @@ +package ganserv + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" +) + +/* + zerotier.go + + This hold the functions that required to communicate with + a zerotier instance + + See more on + https://docs.zerotier.com/self-hosting/network-controllers/ + +*/ + +type NodeInfo struct { + Address string `json:"address"` + Clock int64 `json:"clock"` + Config struct { + Settings struct { + AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay"` + PortMappingEnabled bool `json:"portMappingEnabled"` + PrimaryPort int `json:"primaryPort"` + SoftwareUpdate string `json:"softwareUpdate"` + SoftwareUpdateChannel string `json:"softwareUpdateChannel"` + } `json:"settings"` + } `json:"config"` + Online bool `json:"online"` + PlanetWorldID int `json:"planetWorldId"` + PlanetWorldTimestamp int64 `json:"planetWorldTimestamp"` + PublicIdentity string `json:"publicIdentity"` + TCPFallbackActive bool `json:"tcpFallbackActive"` + Version string `json:"version"` + VersionBuild int `json:"versionBuild"` + VersionMajor int `json:"versionMajor"` + VersionMinor int `json:"versionMinor"` + VersionRev int `json:"versionRev"` +} + +type ErrResp struct { + Message string `json:"message"` +} + +type NetworkInfo struct { + AuthTokens []interface{} `json:"authTokens"` + AuthorizationEndpoint string `json:"authorizationEndpoint"` + Capabilities []interface{} `json:"capabilities"` + ClientID string `json:"clientId"` + CreationTime int64 `json:"creationTime"` + DNS []interface{} `json:"dns"` + EnableBroadcast bool `json:"enableBroadcast"` + ID string `json:"id"` + IPAssignmentPools []interface{} `json:"ipAssignmentPools"` + Mtu int `json:"mtu"` + MulticastLimit int `json:"multicastLimit"` + Name string `json:"name"` + Nwid string `json:"nwid"` + Objtype string `json:"objtype"` + Private bool `json:"private"` + RemoteTraceLevel int `json:"remoteTraceLevel"` + RemoteTraceTarget interface{} `json:"remoteTraceTarget"` + Revision int `json:"revision"` + Routes []interface{} `json:"routes"` + Rules []struct { + Not bool `json:"not"` + Or bool `json:"or"` + Type string `json:"type"` + } `json:"rules"` + RulesSource string `json:"rulesSource"` + SsoEnabled bool `json:"ssoEnabled"` + Tags []interface{} `json:"tags"` + V4AssignMode struct { + Zt bool `json:"zt"` + } `json:"v4AssignMode"` + V6AssignMode struct { + SixPlane bool `json:"6plane"` + Rfc4193 bool `json:"rfc4193"` + Zt bool `json:"zt"` + } `json:"v6AssignMode"` +} + +type MemberInfo struct { + ActiveBridge bool `json:"activeBridge"` + Address string `json:"address"` + AuthenticationExpiryTime int `json:"authenticationExpiryTime"` + Authorized bool `json:"authorized"` + Capabilities []interface{} `json:"capabilities"` + CreationTime int64 `json:"creationTime"` + ID string `json:"id"` + Identity string `json:"identity"` + IPAssignments []string `json:"ipAssignments"` + LastAuthorizedCredential interface{} `json:"lastAuthorizedCredential"` + LastAuthorizedCredentialType string `json:"lastAuthorizedCredentialType"` + LastAuthorizedTime int `json:"lastAuthorizedTime"` + LastDeauthorizedTime int `json:"lastDeauthorizedTime"` + NoAutoAssignIps bool `json:"noAutoAssignIps"` + Nwid string `json:"nwid"` + Objtype string `json:"objtype"` + RemoteTraceLevel int `json:"remoteTraceLevel"` + RemoteTraceTarget interface{} `json:"remoteTraceTarget"` + Revision int `json:"revision"` + SsoExempt bool `json:"ssoExempt"` + Tags []interface{} `json:"tags"` + VMajor int `json:"vMajor"` + VMinor int `json:"vMinor"` + VProto int `json:"vProto"` + VRev int `json:"vRev"` +} + +//Get the zerotier node info from local service +func getControllerInfo(token string, apiPort int) (*NodeInfo, error) { + url := "http://localhost:" + strconv.Itoa(apiPort) + "/status" + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("X-ZT1-AUTH", token) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + //Read from zerotier service instance + + defer resp.Body.Close() + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + //Parse the payload into struct + thisInstanceInfo := NodeInfo{} + err = json.Unmarshal(payload, &thisInstanceInfo) + if err != nil { + return nil, err + } + + return &thisInstanceInfo, nil +} + +/* + Network Functions +*/ +//Create a zerotier network +func (m *NetworkManager) createNetwork() (*NetworkInfo, error) { + url := fmt.Sprintf("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/%s______", m.ControllerID) + + data := []byte(`{}`) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + if err != nil { + return nil, err + } + + req.Header.Set("X-ZT1-AUTH", m.authToken) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + networkInfo := NetworkInfo{} + err = json.Unmarshal(payload, &networkInfo) + if err != nil { + return nil, err + } + + return &networkInfo, nil +} + +//List network details +func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) { + req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil) + if err != nil { + return nil, err + } + req.Header.Set("X-Zt1-Auth", m.authToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode)) + } + + thisNetworkInfo := NetworkInfo{} + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(payload, &thisNetworkInfo) + if err != nil { + return nil, err + } + + return &thisNetworkInfo, nil +} + +func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *NetworkInfo) error { + payloadBytes, err := json.Marshal(newNetworkInfo) + if err != nil { + return err + } + payloadBuffer := bytes.NewBuffer(payloadBytes) + + // Create the HTTP request + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/" + req, err := http.NewRequest("POST", url, payloadBuffer) + if err != nil { + return err + } + req.Header.Set("X-Zt1-Auth", m.authToken) + req.Header.Set("Content-Type", "application/json") + + // Send the HTTP request + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +//List network IDs +func (m *NetworkManager) listNetworkIds() ([]string, error) { + req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil) + if err != nil { + return []string{}, err + } + req.Header.Set("X-Zt1-Auth", m.authToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return []string{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return []string{}, errors.New("network error") + } + + networkIds := []string{} + payload, err := io.ReadAll(resp.Body) + if err != nil { + return []string{}, err + } + + err = json.Unmarshal(payload, &networkIds) + if err != nil { + return []string{}, err + } + + return networkIds, nil +} + +//wrapper for checking if a network id exists +func (m *NetworkManager) networkExists(networkId string) bool { + networkIds, err := m.listNetworkIds() + if err != nil { + return false + } + + for _, thisid := range networkIds { + if thisid == networkId { + return true + } + } + + return false +} + +//delete a network +func (m *NetworkManager) deleteNetwork(networkID string) error { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/" + client := &http.Client{} + + // Create a new DELETE request + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + + // Add the required authorization header + req.Header.Set("X-Zt1-Auth", m.authToken) + + // Send the request and get the response + resp, err := client.Do(req) + if err != nil { + return err + } + + // Close the response body when we're done + defer resp.Body.Close() + s, err := io.ReadAll(resp.Body) + fmt.Println(string(s), err, resp.StatusCode) + + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +//Configure network +//Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24") +func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/" + data := map[string]interface{}{ + "ipAssignmentPools": []map[string]string{ + { + "ipRangeStart": ipRangeStart, + "ipRangeEnd": ipRangeEnd, + }, + }, + "routes": []map[string]interface{}{ + { + "target": routeTarget, + "via": nil, + }, + }, + "v4AssignMode": "zt", + "private": true, + } + + payload, err := json.Marshal(data) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-ZT1-AUTH", m.authToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +func (m *NetworkManager) setAssignedIps(networkID string, memid string, newIps []string) error { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/member/" + memid + data := map[string]interface{}{ + "ipAssignments": newIps, + } + + payload, err := json.Marshal(data) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-ZT1-AUTH", m.authToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +func (m *NetworkManager) setNetworkNameAndDescription(netid string, name string, desc string) error { + // Convert string to rune slice + r := []rune(name) + + // Loop over runes and remove non-ASCII characters + for i, v := range r { + if v > 127 { + r[i] = ' ' + } + } + + // Convert back to string and trim whitespace + name = strings.TrimSpace(string(r)) + + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/" + data := map[string]interface{}{ + "name": name, + } + + payload, err := json.Marshal(data) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-ZT1-AUTH", m.authToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + // Print the response status code + if resp.StatusCode != 200 { + return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode)) + } + + meta := m.GetNetworkMetaData(netid) + if meta != nil { + meta.Desc = desc + m.WriteNetworkMetaData(netid, meta) + } + + return nil +} + +func (m *NetworkManager) getNetworkNameAndDescription(netid string) (string, string, error) { + //Get name from network info + netinfo, err := m.getNetworkInfoById(netid) + if err != nil { + return "", "", err + } + + name := netinfo.Name + + //Get description from meta + desc := "" + networkMeta := m.GetNetworkMetaData(netid) + if networkMeta != nil { + desc = networkMeta.Desc + } + + return name, desc, nil +} + +/* + Member functions +*/ + +func (m *NetworkManager) getNetworkMembers(networkId string) ([]string, error) { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/member" + reqBody := bytes.NewBuffer([]byte{}) + req, err := http.NewRequest("GET", url, reqBody) + if err != nil { + return nil, err + } + + req.Header.Set("X-ZT1-AUTH", m.authToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("failed to get network members") + } + + memberList := map[string]int{} + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(payload, &memberList) + if err != nil { + return nil, err + } + + members := make([]string, 0, len(memberList)) + for k := range memberList { + members = append(members, k) + } + + return members, nil +} + +func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool { + //Get a list of member + memberids, err := m.getNetworkMembers(netid) + if err != nil { + return false + } + for _, thisMemberId := range memberids { + if thisMemberId == memid { + return true + } + } + + return false +} + +//Get a network memeber info by netid and memberid +func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) { + req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil) + if err != nil { + return nil, err + } + req.Header.Set("X-Zt1-Auth", m.authToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + thisMemeberInfo := &MemberInfo{} + payload, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = json.Unmarshal(payload, &thisMemeberInfo) + if err != nil { + return nil, err + } + + return thisMemeberInfo, nil +} + +//Set the authorization state of a member +func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error { + url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid + payload := []byte(`{"authorized": true}`) + if !setAuthorized { + payload = []byte(`{"authorized": false}`) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) + if err != nil { + return err + } + req.Header.Set("X-ZT1-AUTH", m.authToken) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +} + +//Delete a member from the network +func (m *NetworkManager) deleteMember(netid string, memid string) error { + req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil) + if err != nil { + return err + } + req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode)) + } + + return nil +}
src/mod/geodb/geodb.go+250 −0 added@@ -0,0 +1,250 @@ +package geodb + +import ( + _ "embed" + "log" + "net" + "net/http" + "strings" + + "imuslab.com/zoraxy/mod/database" +) + +//go:embed geoipv4.csv +var geoipv4 []byte //Original embedded csv file + +type Store struct { + Enabled bool + geodb [][]string //Parsed geodb list + //geoipCache sync.Map + geotrie *trie + sysdb *database.Database +} + +type CountryInfo struct { + CountryIsoCode string + ContinetCode string +} + +func NewGeoDb(sysdb *database.Database) (*Store, error) { + parsedGeoData, err := parseCSV(geoipv4) + if err != nil { + return nil, err + } + + blacklistEnabled := false + if sysdb != nil { + err = sysdb.NewTable("blacklist-cn") + if err != nil { + return nil, err + } + + err = sysdb.NewTable("blacklist-ip") + if err != nil { + return nil, err + } + + err = sysdb.NewTable("blacklist") + if err != nil { + return nil, err + } + sysdb.Read("blacklist", "enabled", &blacklistEnabled) + } else { + log.Println("Database pointer set to nil: Entering debug mode") + } + + return &Store{ + Enabled: blacklistEnabled, + geodb: parsedGeoData, + //geoipCache: sync.Map{}, + geotrie: constrctTrieTree(parsedGeoData), + sysdb: sysdb, + }, nil +} + +func (s *Store) ToggleBlacklist(enabled bool) { + s.sysdb.Write("blacklist", "enabled", enabled) + s.Enabled = enabled +} + +func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) { + cc := s.search(ipstring) + return &CountryInfo{ + CountryIsoCode: cc, + ContinetCode: "", + }, nil +} + +func (s *Store) Close() { + +} + +func (s *Store) AddCountryCodeToBlackList(countryCode string) { + countryCode = strings.ToLower(countryCode) + s.sysdb.Write("blacklist-cn", countryCode, true) +} + +func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) { + countryCode = strings.ToLower(countryCode) + s.sysdb.Delete("blacklist-cn", countryCode) +} + +func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool { + countryCode = strings.ToLower(countryCode) + var isBlacklisted bool = false + s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted) + return isBlacklisted +} + +func (s *Store) GetAllBlacklistedCountryCode() []string { + bannedCountryCodes := []string{} + entries, err := s.sysdb.ListTable("blacklist-cn") + if err != nil { + return bannedCountryCodes + } + for _, keypairs := range entries { + ip := string(keypairs[0]) + bannedCountryCodes = append(bannedCountryCodes, ip) + } + + return bannedCountryCodes +} + +func (s *Store) AddIPToBlackList(ipAddr string) { + s.sysdb.Write("blacklist-ip", ipAddr, true) +} + +func (s *Store) RemoveIPFromBlackList(ipAddr string) { + s.sysdb.Delete("blacklist-ip", ipAddr) +} + +func (s *Store) IsIPBlacklisted(ipAddr string) bool { + var isBlacklisted bool = false + s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted) + if isBlacklisted { + return true + } + + //Check for IP wildcard and CIRD rules + AllBlacklistedIps := s.GetAllBlacklistedIp() + for _, blacklistRule := range AllBlacklistedIps { + wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule) + if wildcardMatch { + return true + } + + cidrMatch := MatchIpCIDR(ipAddr, blacklistRule) + if cidrMatch { + return true + } + } + + return false +} + +func (s *Store) GetAllBlacklistedIp() []string { + bannedIps := []string{} + entries, err := s.sysdb.ListTable("blacklist-ip") + if err != nil { + return bannedIps + } + + for _, keypairs := range entries { + ip := string(keypairs[0]) + bannedIps = append(bannedIps, ip) + } + + return bannedIps +} + +// Check if a IP address is blacklisted, in either country or IP blacklist +func (s *Store) IsBlacklisted(ipAddr string) bool { + if !s.Enabled { + //Blacklist not enabled. Always return false + return false + } + + if ipAddr == "" { + //Unable to get the target IP address + return false + } + + countryCode, err := s.ResolveCountryCodeFromIP(ipAddr) + if err != nil { + return false + } + + if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) { + return true + } + + if s.IsIPBlacklisted(ipAddr) { + return true + } + + return false +} + +func (s *Store) GetRequesterCountryISOCode(r *http.Request) string { + ipAddr := GetRequesterIP(r) + if ipAddr == "" { + return "" + } + countryCode, err := s.ResolveCountryCodeFromIP(ipAddr) + if err != nil { + return "" + } + + return countryCode.CountryIsoCode +} + +// Utilities function +func GetRequesterIP(r *http.Request) string { + ip := r.Header.Get("X-Forwarded-For") + if ip == "" { + ip = r.Header.Get("X-Real-IP") + if ip == "" { + ip = strings.Split(r.RemoteAddr, ":")[0] + } + } + return ip +} + +// Match the IP address with a wildcard string +func MatchIpWildcard(ipAddress, wildcard string) bool { + // Split IP address and wildcard into octets + ipOctets := strings.Split(ipAddress, ".") + wildcardOctets := strings.Split(wildcard, ".") + + // Check that both have 4 octets + if len(ipOctets) != 4 || len(wildcardOctets) != 4 { + return false + } + + // Check each octet to see if it matches the wildcard or is an exact match + for i := 0; i < 4; i++ { + if wildcardOctets[i] == "*" { + continue + } + if ipOctets[i] != wildcardOctets[i] { + return false + } + } + + return true +} + +// Match ip address with CIDR +func MatchIpCIDR(ip string, cidr string) bool { + // parse the CIDR string + _, cidrnet, err := net.ParseCIDR(cidr) + if err != nil { + return false + } + + // parse the IP address + ipAddr := net.ParseIP(ip) + + // check if the IP address is within the CIDR range + return cidrnet.Contains(ipAddr) +}
src/mod/geodb/geodb_test.go+73 −0 added@@ -0,0 +1,73 @@ +package geodb_test + +import ( + "testing" + + "imuslab.com/zoraxy/mod/geodb" +) + +/* +func TestTrieConstruct(t *testing.T) { + tt := geodb.NewTrie() + data := [][]string{ + {"1.0.16.0", "1.0.31.255", "JP"}, + {"1.0.32.0", "1.0.63.255", "CN"}, + {"1.0.64.0", "1.0.127.255", "JP"}, + {"1.0.128.0", "1.0.255.255", "TH"}, + {"1.1.0.0", "1.1.0.255", "CN"}, + {"1.1.1.0", "1.1.1.255", "AU"}, + {"1.1.2.0", "1.1.63.255", "CN"}, + {"1.1.64.0", "1.1.127.255", "JP"}, + {"1.1.128.0", "1.1.255.255", "TH"}, + {"1.2.0.0", "1.2.2.255", "CN"}, + {"1.2.3.0", "1.2.3.255", "AU"}, + } + + for _, entry := range data { + startIp := entry[0] + endIp := entry[1] + cc := entry[2] + tt.Insert(startIp, cc) + tt.Insert(endIp, cc) + } + + t.Log(tt.Search("1.0.16.20"), "== JP") //JP + t.Log(tt.Search("1.2.0.122"), "== CN") //CN + t.Log(tt.Search("1.2.1.0"), "== CN") //CN + t.Log(tt.Search("1.0.65.243"), "== JP") //JP + t.Log(tt.Search("1.0.62.243"), "== CN") //CN +} +*/ + +func TestResolveCountryCodeFromIP(t *testing.T) { + // Create a new store + store, err := geodb.NewGeoDb(nil) + if err != nil { + t.Errorf("error creating store: %v", err) + return + } + + // Test an IP address that should return a valid country code + ip := "8.8.8.8" + expected := "US" + info, err := store.ResolveCountryCodeFromIP(ip) + if err != nil { + t.Errorf("error resolving country code for IP %s: %v", ip, err) + return + } + if info.CountryIsoCode != expected { + t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip) + } + + // Test an IP address that should return an empty country code + ip = "127.0.0.1" + expected = "" + info, err = store.ResolveCountryCodeFromIP(ip) + if err != nil { + t.Errorf("error resolving country code for IP %s: %v", ip, err) + return + } + if info.CountryIsoCode != expected { + t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip) + } +}
src/mod/geodb/geoipv4.csv+261318 −0 addedsrc/mod/geodb/geoloader.go+89 −0 added@@ -0,0 +1,89 @@ +package geodb + +import ( + "bytes" + "encoding/csv" + "io" + "net" + "strings" +) + +func (s *Store) search(ip string) string { + if strings.Contains(ip, ",") { + //This is a CF proxied request. We only need the front part + //Example 219.71.102.145, 172.71.139.178 + ip = strings.Split(ip, ",")[0] + ip = strings.TrimSpace(ip) + } + //See if there are cached country code for this ip + /* + ccc, ok := s.geoipCache.Load(ip) + if ok { + return ccc.(string) + } + */ + + //Search in geotrie tree + cc := s.geotrie.search(ip) + /* + if cc != "" { + s.geoipCache.Store(ip, cc) + } + */ + return cc +} + +// Construct the trie data structure for quick lookup +func constrctTrieTree(data [][]string) *trie { + tt := newTrie() + for _, entry := range data { + startIp := entry[0] + endIp := entry[1] + cc := entry[2] + tt.insert(startIp, cc) + tt.insert(endIp, cc) + } + + return tt +} + +// Parse the embedded csv as ipstart, ipend and country code entries +func parseCSV(content []byte) ([][]string, error) { + var records [][]string + r := csv.NewReader(bytes.NewReader(content)) + for { + record, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + records = append(records, record) + } + return records, nil +} + +// Check if a ip string is within the range of two others +func isIPInRange(ip, start, end string) bool { + ipAddr := net.ParseIP(ip) + if ipAddr == nil { + return false + } + + startAddr := net.ParseIP(start) + if startAddr == nil { + return false + } + + endAddr := net.ParseIP(end) + if endAddr == nil { + return false + } + + if ipAddr.To4() == nil || startAddr.To4() == nil || endAddr.To4() == nil { + return false + } + + return bytes.Compare(ipAddr.To4(), startAddr.To4()) >= 0 && bytes.Compare(ipAddr.To4(), endAddr.To4()) <= 0 +}
src/mod/geodb/trie.go+131 −0 added@@ -0,0 +1,131 @@ +package geodb + +import ( + "fmt" + "net" + "strconv" + "strings" +) + +type trie_Node struct { + childrens [2]*trie_Node + ends bool + cc string +} + +// Initializing the root of the trie +type trie struct { + root *trie_Node +} + +func ipToBitString(ip string) string { + // Parse the IP address string into a net.IP object + parsedIP := net.ParseIP(ip) + + // Convert the IP address to a 4-byte slice + ipBytes := parsedIP.To4() + + // Convert each byte in the IP address to its 8-bit binary representation + var result []string + for _, b := range ipBytes { + result = append(result, fmt.Sprintf("%08b", b)) + } + + // Join the binary representation of each byte with dots to form the final bit string + return strings.Join(result, "") +} + +func bitStringToIp(bitString string) string { + // Split the bit string into four 8-bit segments + segments := []string{ + bitString[:8], + bitString[8:16], + bitString[16:24], + bitString[24:32], + } + + // Convert each segment to its decimal equivalent + var decimalSegments []int + for _, s := range segments { + i, _ := strconv.ParseInt(s, 2, 64) + decimalSegments = append(decimalSegments, int(i)) + } + + // Join the decimal segments with dots to form the IP address string + return fmt.Sprintf("%d.%d.%d.%d", decimalSegments[0], decimalSegments[1], decimalSegments[2], decimalSegments[3]) +} + +// inititlaizing a new trie +func newTrie() *trie { + t := new(trie) + t.root = new(trie_Node) + return t +} + +// Passing words to trie +func (t *trie) insert(ipAddr string, cc string) { + word := ipToBitString(ipAddr) + current := t.root + for _, wr := range word { + index := wr - '0' + if current.childrens[index] == nil { + current.childrens[index] = &trie_Node{ + childrens: [2]*trie_Node{}, + ends: false, + cc: cc, + } + } + current = current.childrens[index] + } + current.ends = true +} + +func isReservedIP(ip string) bool { + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + return false + } + // Check if the IP address is a loopback address + if parsedIP.IsLoopback() { + return true + } + // Check if the IP address is in the link-local address range + if parsedIP.IsLinkLocalUnicast() || parsedIP.IsLinkLocalMulticast() { + return true + } + // Check if the IP address is in the private address ranges + privateRanges := []*net.IPNet{ + {IP: net.ParseIP("10.0.0.0"), Mask: net.CIDRMask(8, 32)}, + {IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(12, 32)}, + {IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(16, 32)}, + } + for _, r := range privateRanges { + if r.Contains(parsedIP) { + return true + } + } + // If the IP address is not a reserved address, return false + return false +} + +// Initializing the search for word in node +func (t *trie) search(ipAddr string) string { + if isReservedIP(ipAddr) { + return "" + } + word := ipToBitString(ipAddr) + current := t.root + for _, wr := range word { + index := wr - '0' + if current.childrens[index] == nil { + return current.cc + } + current = current.childrens[index] + } + if current.ends { + return current.cc + } + + //Not found + return "" +}
src/mod/ipscan/ipscan.go+149 −0 added@@ -0,0 +1,149 @@ +package ipscan + +import ( + "bytes" + "fmt" + "net" + "sort" + "strconv" + "sync" + "time" + + "github.com/go-ping/ping" +) + +/* + IP Scanner + + This module scan the given network range and return a list + of nearby nodes. +*/ + +type DiscoveredHost struct { + IP string + Ping int + Hostname string + HttpPortDetected bool + HttpsPortDetected bool +} + +//Scan an IP range given the start and ending ip address +func ScanIpRange(start, end string) ([]*DiscoveredHost, error) { + ipStart := net.ParseIP(start) + ipEnd := net.ParseIP(end) + if ipStart == nil || ipEnd == nil { + return nil, fmt.Errorf("Invalid IP address") + } + + if bytes.Compare(ipStart, ipEnd) > 0 { + return nil, fmt.Errorf("Invalid IP range") + } + + var wg sync.WaitGroup + hosts := make([]*DiscoveredHost, 0) + for ip := ipStart; bytes.Compare(ip, ipEnd) <= 0; inc(ip) { + wg.Add(1) + thisIp := ip.String() + go func(thisIp string) { + defer wg.Done() + host := &DiscoveredHost{IP: thisIp} + if err := host.CheckPing(); err != nil { + // skip if the host is unreachable + host.Ping = -1 + hosts = append(hosts, host) + return + } + + host.CheckHostname() + host.CheckPort("http", 80, &host.HttpPortDetected) + host.CheckPort("https", 443, &host.HttpsPortDetected) + fmt.Println("OK", host) + hosts = append(hosts, host) + + }(thisIp) + } + + //Wait until all go routine done + wg.Wait() + sortByIP(hosts) + return hosts, nil +} + +func ScanCIDRRange(cidr string) ([]*DiscoveredHost, error) { + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + + ip := ipNet.IP.To4() + startIP := net.IPv4(ip[0], ip[1], ip[2], 1).String() + endIP := net.IPv4(ip[0], ip[1], ip[2], 254).String() + + return ScanIpRange(startIP, endIP) +} + +func inc(ip net.IP) { + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] > 0 { + break + } + } +} + +func sortByIP(discovered []*DiscoveredHost) { + sort.Slice(discovered, func(i, j int) bool { + return discovered[i].IP < discovered[j].IP + }) +} + +func (host *DiscoveredHost) CheckPing() error { + // ping the host and set the ping time in milliseconds + pinger, err := ping.NewPinger(host.IP) + if err != nil { + return err + } + pinger.Count = 4 + pinger.Timeout = time.Second + pinger.SetPrivileged(true) // This line may help on some systems + pinger.Run() + stats := pinger.Statistics() + if stats.PacketsRecv == 0 { + return fmt.Errorf("Host unreachable for " + host.IP) + } + host.Ping = int(stats.AvgRtt.Milliseconds()) + return nil +} + +func (host *DiscoveredHost) CheckHostname() { + // lookup the hostname for the IP address + names, err := net.LookupAddr(host.IP) + fmt.Println(names, err) + if err == nil && len(names) > 0 { + host.Hostname = names[0] + } +} + +func (host *DiscoveredHost) CheckPort(protocol string, port int, detected *bool) { + // try to connect to the specified port on the host + conn, err := net.DialTimeout("tcp", net.JoinHostPort(host.IP, strconv.Itoa(port)), 1*time.Second) + if err == nil { + conn.Close() + *detected = true + } +} + +func (host *DiscoveredHost) ScanPorts(startPort, endPort int) []int { + var openPorts []int + + for port := startPort; port <= endPort; port++ { + target := fmt.Sprintf("%s:%d", host.IP, port) + conn, err := net.DialTimeout("tcp", target, time.Millisecond*500) + if err == nil { + conn.Close() + openPorts = append(openPorts, port) + } + } + + return openPorts +}
src/mod/mdns/mdns.go+243 −0 added@@ -0,0 +1,243 @@ +package mdns + +import ( + "context" + "log" + "net" + "strings" + "time" + + "github.com/grandcat/zeroconf" + "imuslab.com/zoraxy/mod/utils" +) + +type MDNSHost struct { + MDNS *zeroconf.Server + Host *NetworkHost + IfaceOverride *net.Interface +} + +type NetworkHost struct { + HostName string + Port int + IPv4 []net.IP + Domain string + Model string + UUID string + Vendor string + BuildVersion string + MacAddr []string + Online bool +} + +// Create a new MDNS discoverer, set MacOverride to empty string for using the first NIC discovered +func NewMDNS(config NetworkHost, MacOverride string) (*MDNSHost, error) { + //Get host MAC Address + macAddress, err := getMacAddr() + if err != nil { + return nil, err + } + + macAddressBoardcast := "" + if err == nil { + macAddressBoardcast = strings.Join(macAddress, ",") + } else { + log.Println("[mDNS] Unable to get MAC Address: ", err.Error()) + } + + //Register the mds services + server, err := zeroconf.Register(config.HostName, "_http._tcp", "local.", config.Port, []string{"version_build=" + config.BuildVersion, "vendor=" + config.Vendor, "model=" + config.Model, "uuid=" + config.UUID, "domain=" + config.Domain, "mac_addr=" + macAddressBoardcast}, nil) + if err != nil { + log.Println("[mDNS] Error when registering zeroconf broadcast message", err.Error()) + return &MDNSHost{}, err + } + + //Discover the iface to override if exists + var overrideIface *net.Interface = nil + if MacOverride != "" { + ifaceIp := "" + ifaces, err := net.Interfaces() + if err != nil { + log.Println("[mDNS] Unable to override iface MAC: " + err.Error() + ". Resuming with default iface") + } + + foundMatching := false + for _, iface := range ifaces { + thisIfaceMac := iface.HardwareAddr.String() + thisIfaceMac = strings.ReplaceAll(thisIfaceMac, ":", "-") + MacOverride = strings.ReplaceAll(MacOverride, ":", "-") + if strings.EqualFold(thisIfaceMac, strings.TrimSpace(MacOverride)) { + //This is the correct iface to use + overrideIface = &iface + addrs, err := iface.Addrs() + + if err == nil && len(addrs) > 0 { + ifaceIp = addrs[0].String() + } + + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + + if ip.To4() != nil { + //This NIC have Ipv4 addr + ifaceIp = ip.String() + } + } + foundMatching = true + break + } + } + + if !foundMatching { + log.Println("[mDNS] Unable to find the target iface with MAC address: " + MacOverride + ". Resuming with default iface") + } else { + log.Println("[mDNS] Entering force MAC address mode, listening on: " + MacOverride + "(IP address: " + ifaceIp + ")") + } + } + + return &MDNSHost{ + MDNS: server, + Host: &config, + IfaceOverride: overrideIface, + }, nil +} + +func (m *MDNSHost) Close() { + if m != nil { + m.MDNS.Shutdown() + } + +} + +// Scan with given timeout and domain filter. Use m.Host.Domain for scanning similar typed devices +func (m *MDNSHost) Scan(timeout int, domainFilter string) []*NetworkHost { + // Discover all services on the network (e.g. _workstation._tcp) + + var zcoption zeroconf.ClientOption = nil + if m.IfaceOverride != nil { + zcoption = zeroconf.SelectIfaces([]net.Interface{*m.IfaceOverride}) + } + + resolver, err := zeroconf.NewResolver(zcoption) + if err != nil { + log.Fatalln("Failed to initialize resolver:", err.Error()) + } + + entries := make(chan *zeroconf.ServiceEntry) + //Create go routine to wait for the resolver + + discoveredHost := []*NetworkHost{} + + go func(results <-chan *zeroconf.ServiceEntry) { + for entry := range results { + if domainFilter == "" { + //This is a ArOZ Online Host + //Split the required information out of the text element + TEXT := entry.Text + properties := map[string]string{} + for _, v := range TEXT { + kv := strings.Split(v, "=") + if len(kv) == 2 { + properties[kv[0]] = kv[1] + } + } + + var macAddrs []string + val, ok := properties["mac_addr"] + if !ok || val == "" { + //No MacAddr found. Target node version too old + macAddrs = []string{} + } else { + macAddrs = strings.Split(properties["mac_addr"], ",") + } + + //log.Println(properties) + discoveredHost = append(discoveredHost, &NetworkHost{ + HostName: entry.HostName, + Port: entry.Port, + IPv4: entry.AddrIPv4, + Domain: properties["domain"], + Model: properties["model"], + UUID: properties["uuid"], + Vendor: properties["vendor"], + BuildVersion: properties["version_build"], + MacAddr: macAddrs, + Online: true, + }) + + } else { + if utils.StringInArray(entry.Text, "domain="+domainFilter) { + //This is generic scan request + //Split the required information out of the text element + TEXT := entry.Text + properties := map[string]string{} + for _, v := range TEXT { + kv := strings.Split(v, "=") + if len(kv) == 2 { + properties[kv[0]] = kv[1] + } + } + + var macAddrs []string + val, ok := properties["mac_addr"] + if !ok || val == "" { + //No MacAddr found. Target node version too old + macAddrs = []string{} + } else { + macAddrs = strings.Split(properties["mac_addr"], ",") + } + + //log.Println(properties) + discoveredHost = append(discoveredHost, &NetworkHost{ + HostName: entry.HostName, + Port: entry.Port, + IPv4: entry.AddrIPv4, + Domain: properties["domain"], + Model: properties["model"], + UUID: properties["uuid"], + Vendor: properties["vendor"], + BuildVersion: properties["version_build"], + MacAddr: macAddrs, + Online: true, + }) + + } + } + + } + }(entries) + + //Resolve each of the mDNS and pipe it back to the log functions + ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(timeout)) + defer cancel() + err = resolver.Browse(ctx, "_http._tcp", "local.", entries) + if err != nil { + log.Fatalln("Failed to browse:", err.Error()) + } + + //Update the master scan record + <-ctx.Done() + return discoveredHost +} + +//Get all mac address of all interfaces +func getMacAddr() ([]string, error) { + ifas, err := net.Interfaces() + if err != nil { + return nil, err + } + var as []string + for _, ifa := range ifas { + a := ifa.HardwareAddr.String() + if a != "" { + as = append(as, a) + } + } + return as, nil +}
src/mod/netstat/netstat.go+339 −0 added@@ -0,0 +1,339 @@ +package netstat + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "imuslab.com/zoraxy/mod/utils" +) + +// Float stat store the change of RX and TX +type FlowStat struct { + RX int64 + TX int64 +} + +// A new type of FloatStat that save the raw value from rx tx +type RawFlowStat struct { + RX int64 + TX int64 +} + +type NetStatBuffers struct { + StatRecordCount int //No. of record number to keep + PreviousStat *RawFlowStat //The value of the last instance of netstats + Stats []*FlowStat //Statistic of the flow + StopChan chan bool //Channel to stop the ticker + EventTicker *time.Ticker //Ticker for event logging +} + +// Get a new network statistic buffers +func NewNetStatBuffer(recordCount int) (*NetStatBuffers, error) { + + //Flood fill the stats with 0 + initialStats := []*FlowStat{} + for i := 0; i < recordCount; i++ { + initialStats = append(initialStats, &FlowStat{ + RX: 0, + TX: 0, + }) + } + + //Setup a timer to get the value from NIC accumulation stats + ticker := time.NewTicker(time.Second) + + //Setup a stop channel + stopCh := make(chan bool) + + currnetNetSpec := RawFlowStat{ + RX: 0, + TX: 0, + } + + thisNetBuffer := NetStatBuffers{ + StatRecordCount: recordCount, + PreviousStat: &currnetNetSpec, + Stats: initialStats, + StopChan: stopCh, + EventTicker: ticker, + } + + //Get the initial measurements of netstats + rx, tx, err := GetNetworkInterfaceStats() + if err != nil { + log.Println("Unable to get NIC stats: ", err.Error()) + } + + retryCount := 0 + for rx == 0 && tx == 0 && retryCount < 10 { + //Strange. Retry + log.Println("NIC stats return all 0. Retrying...") + rx, tx, err = GetNetworkInterfaceStats() + if err != nil { + log.Println("Unable to get NIC stats: ", err.Error()) + } + retryCount++ + } + + thisNetBuffer.PreviousStat = &RawFlowStat{ + RX: rx, + TX: tx, + } + + // Update the buffer every second + go func(n *NetStatBuffers) { + for { + select { + case <-n.StopChan: + fmt.Println("- Netstats listener stopped") + return + + case <-ticker.C: + if n.PreviousStat.RX == 0 && n.PreviousStat.TX == 0 { + //Initiation state is still not done. Ignore request + log.Println("No initial states. Waiting") + return + } + // Get the latest network interface stats + rx, tx, err := GetNetworkInterfaceStats() + if err != nil { + // Log the error, but don't stop the buffer + log.Printf("Failed to get network interface stats: %v", err) + continue + } + + //Calculate the difference between this and last values + drx := rx - n.PreviousStat.RX + dtx := tx - n.PreviousStat.TX + + // Push the new stats to the buffer + newStat := &FlowStat{ + RX: drx, + TX: dtx, + } + + //Set current rx tx as the previous rxtx + n.PreviousStat = &RawFlowStat{ + RX: rx, + TX: tx, + } + + newStats := n.Stats[1:] + newStats = append(newStats, newStat) + + n.Stats = newStats + } + } + }(&thisNetBuffer) + + return &thisNetBuffer, nil +} + +func (n *NetStatBuffers) HandleGetBufferedNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) { + arr, _ := utils.GetPara(r, "array") + if arr == "true" { + //Restructure it into array + rx := []int{} + tx := []int{} + + for _, state := range n.Stats { + rx = append(rx, int(state.RX)) + tx = append(tx, int(state.TX)) + } + + type info struct { + Rx []int + Tx []int + } + + js, _ := json.Marshal(info{ + Rx: rx, + Tx: tx, + }) + utils.SendJSONResponse(w, string(js)) + } else { + js, _ := json.Marshal(n.Stats) + utils.SendJSONResponse(w, string(js)) + } + +} + +func (n *NetStatBuffers) Close() { + n.StopChan <- true + time.Sleep(300 * time.Millisecond) + n.EventTicker.Stop() +} + +func HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) { + rx, tx, err := GetNetworkInterfaceStats() + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + currnetNetSpec := struct { + RX int64 + TX int64 + }{ + rx, + tx, + } + + js, _ := json.Marshal(currnetNetSpec) + utils.SendJSONResponse(w, string(js)) +} + +// Get network interface stats, return accumulated rx bits, tx bits and error if any +func GetNetworkInterfaceStats() (int64, int64, error) { + if runtime.GOOS == "windows" { + //Windows wmic sometime freeze and not respond. + //The safer way is to make a bypass mechanism + //when timeout with channel + + type wmicResult struct { + RX int64 + TX int64 + Err error + } + + callbackChan := make(chan wmicResult) + cmd := exec.Command("wmic", "path", "Win32_PerfRawData_Tcpip_NetworkInterface", "Get", "BytesReceivedPersec,BytesSentPersec,BytesTotalPersec") + //Execute the cmd in goroutine + go func() { + out, err := cmd.Output() + if err != nil { + callbackChan <- wmicResult{0, 0, err} + } + + //Filter out the first line + lines := strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n") + if len(lines) >= 2 && len(lines[1]) >= 0 { + dataLine := lines[1] + for strings.Contains(dataLine, " ") { + dataLine = strings.ReplaceAll(dataLine, " ", " ") + } + dataLine = strings.TrimSpace(dataLine) + info := strings.Split(dataLine, " ") + if len(info) != 3 { + callbackChan <- wmicResult{0, 0, errors.New("invalid wmic results length")} + } + rxString := info[0] + txString := info[1] + + rx := int64(0) + tx := int64(0) + if s, err := strconv.ParseInt(rxString, 10, 64); err == nil { + rx = s + } + + if s, err := strconv.ParseInt(txString, 10, 64); err == nil { + tx = s + } + + time.Sleep(100 * time.Millisecond) + callbackChan <- wmicResult{rx * 4, tx * 4, nil} + } else { + //Invalid data + callbackChan <- wmicResult{0, 0, errors.New("invalid wmic results")} + } + + }() + + go func() { + //Spawn a timer to terminate the cmd process if timeout + var timer *time.Timer + timer = time.AfterFunc(3*time.Second, func() { + timer.Stop() + if cmd != nil && cmd.Process != nil { + cmd.Process.Kill() + } + callbackChan <- wmicResult{0, 0, errors.New("wmic execution timeout")} + }) + }() + + result := wmicResult{} + result = <-callbackChan + if result.Err != nil { + log.Println("Unable to extract NIC info from wmic: " + result.Err.Error()) + } + return result.RX, result.TX, result.Err + } else if runtime.GOOS == "linux" { + allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes") + if err != nil { + //Permission denied + return 0, 0, errors.New("Access denied") + } + + if len(allIfaceRxByteFiles) == 0 { + return 0, 0, errors.New("No valid iface found") + } + + rxSum := int64(0) + txSum := int64(0) + for _, rxByteFile := range allIfaceRxByteFiles { + rxBytes, err := os.ReadFile(rxByteFile) + if err == nil { + rxBytesInt, err := strconv.Atoi(strings.TrimSpace(string(rxBytes))) + if err == nil { + rxSum += int64(rxBytesInt) + } + } + + //Usually the tx_bytes file is nearby it. Read it as well + txByteFile := filepath.Join(filepath.Dir(rxByteFile), "tx_bytes") + txBytes, err := os.ReadFile(txByteFile) + if err == nil { + txBytesInt, err := strconv.Atoi(strings.TrimSpace(string(txBytes))) + if err == nil { + txSum += int64(txBytesInt) + } + } + + } + + //Return value as bits + return rxSum * 8, txSum * 8, nil + + } else if runtime.GOOS == "darwin" { + cmd := exec.Command("netstat", "-ib") //get data from netstat -ib + out, err := cmd.Output() + if err != nil { + return 0, 0, err + } + + outStrs := string(out) //byte array to multi-line string + for _, outStr := range strings.Split(strings.TrimSuffix(outStrs, "\n"), "\n") { //foreach multi-line string + if strings.HasPrefix(outStr, "en") { //search for ethernet interface + if strings.Contains(outStr, "<Link#") { //search for the link with <Link#?> + outStrSplit := strings.Fields(outStr) //split by white-space + + rxSum, errRX := strconv.Atoi(outStrSplit[6]) //received bytes sum + if errRX != nil { + return 0, 0, errRX + } + + txSum, errTX := strconv.Atoi(outStrSplit[9]) //transmitted bytes sum + if errTX != nil { + return 0, 0, errTX + } + + return int64(rxSum) * 8, int64(txSum) * 8, nil + } + } + } + + return 0, 0, nil //no ethernet adapters with en*/<Link#*> + } + + return 0, 0, errors.New("Platform not supported") +}
src/mod/netstat/nic.go+55 −0 added@@ -0,0 +1,55 @@ +package netstat + +import ( + "encoding/json" + "net" + "net/http" + + "imuslab.com/zoraxy/mod/utils" +) + +type NetworkInterface struct { + Name string + ID int + IPs []string +} + +func HandleListNetworkInterfaces(w http.ResponseWriter, r *http.Request) { + nic, err := ListNetworkInterfaces() + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(nic) + utils.SendJSONResponse(w, string(js)) +} + +func ListNetworkInterfaces() ([]NetworkInterface, error) { + var interfaces []NetworkInterface + + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + for _, iface := range ifaces { + var ips []string + addrs, err := iface.Addrs() + if err != nil { + return nil, err + } + + for _, addr := range addrs { + ips = append(ips, addr.String()) + } + + interfaces = append(interfaces, NetworkInterface{ + Name: iface.Name, + ID: iface.Index, + IPs: ips, + }) + } + + return interfaces, nil +}
src/mod/reverseproxy/LICENSE+21 −0 added@@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-present tobychui + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
src/mod/reverseproxy/reverse.go+405 −0 added@@ -0,0 +1,405 @@ +package reverseproxy + +import ( + "errors" + "io" + "log" + "net" + "net/http" + "net/url" + "strings" + "sync" + "time" +) + +var onExitFlushLoop func() + +const ( + defaultTimeout = time.Minute * 5 +) + +// ReverseProxy is an HTTP Handler that takes an incoming request and +// sends it to another server, proxying the response back to the +// client, support http, also support https tunnel using http.hijacker +type ReverseProxy struct { + // Set the timeout of the proxy server, default is 5 minutes + Timeout time.Duration + + // Director must be a function which modifies + // the request into a new request to be sent + // using Transport. Its response is then copied + // back to the original client unmodified. + // Director must not access the provided Request + // after returning. + Director func(*http.Request) + + // The transport used to perform proxy requests. + // default is http.DefaultTransport. + Transport http.RoundTripper + + // FlushInterval specifies the flush interval + // to flush to the client while copying the + // response body. If zero, no periodic flushing is done. + FlushInterval time.Duration + + // ErrorLog specifies an optional logger for errors + // that occur when attempting to proxy the request. + // If nil, logging goes to os.Stderr via the log package's + // standard logger. + ErrorLog *log.Logger + + // ModifyResponse is an optional function that + // modifies the Response from the backend. + // If it returns an error, the proxy returns a StatusBadGateway error. + ModifyResponse func(*http.Response) error + + Verbal bool +} + +type requestCanceler interface { + CancelRequest(req *http.Request) +} + +// NewReverseProxy returns a new ReverseProxy that routes +// URLs to the scheme, host, and base path provided in target. If the +// target's path is "/base" and the incoming request was for "/dir", +// the target request will be for /base/dir. if the target's query is a=10 +// and the incoming request's query is b=100, the target's request's query +// will be a=10&b=100. +// NewReverseProxy does not rewrite the Host header. +// To rewrite Host headers, use ReverseProxy directly with a custom +// Director policy. +func NewReverseProxy(target *url.URL) *ReverseProxy { + targetQuery := target.RawQuery + director := func(req *http.Request) { + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) + + // If Host is empty, the Request.Write method uses + // the value of URL.Host. + // force use URL.Host + req.Host = req.URL.Host + if targetQuery == "" || req.URL.RawQuery == "" { + req.URL.RawQuery = targetQuery + req.URL.RawQuery + } else { + req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery + } + + if _, ok := req.Header["User-Agent"]; !ok { + req.Header.Set("User-Agent", "") + } + } + + return &ReverseProxy{Director: director, Verbal: false} +} + +func singleJoiningSlash(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + switch { + case aslash && bslash: + return a + b[1:] + case !aslash && !bslash: + return a + "/" + b + } + return a + b +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +// Hop-by-hop headers. These are removed when sent to the backend. +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html +var hopHeaders = []string{ + //"Connection", + "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "Te", // canonicalized version of "TE" + "Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522 + "Transfer-Encoding", + //"Upgrade", +} + +func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) { + if p.FlushInterval != 0 { + if wf, ok := dst.(writeFlusher); ok { + mlw := &maxLatencyWriter{ + dst: wf, + latency: p.FlushInterval, + done: make(chan bool), + } + + go mlw.flushLoop() + defer mlw.stop() + dst = mlw + } + } + + io.Copy(dst, src) +} + +type writeFlusher interface { + io.Writer + http.Flusher +} + +type maxLatencyWriter struct { + dst writeFlusher + latency time.Duration + mu sync.Mutex + done chan bool +} + +func (m *maxLatencyWriter) Write(b []byte) (int, error) { + m.mu.Lock() + defer m.mu.Unlock() + return m.dst.Write(b) +} + +func (m *maxLatencyWriter) flushLoop() { + t := time.NewTicker(m.latency) + defer t.Stop() + for { + select { + case <-m.done: + if onExitFlushLoop != nil { + onExitFlushLoop() + } + return + case <-t.C: + m.mu.Lock() + m.dst.Flush() + m.mu.Unlock() + } + } +} + +func (m *maxLatencyWriter) stop() { + m.done <- true +} + +func (p *ReverseProxy) logf(format string, args ...interface{}) { + if p.ErrorLog != nil { + p.ErrorLog.Printf(format, args...) + } else { + log.Printf(format, args...) + } +} + +func removeHeaders(header http.Header) { + // Remove hop-by-hop headers listed in the "Connection" header. + if c := header.Get("Connection"); c != "" { + for _, f := range strings.Split(c, ",") { + if f = strings.TrimSpace(f); f != "" { + header.Del(f) + } + } + } + + // Remove hop-by-hop headers + for _, h := range hopHeaders { + if header.Get(h) != "" { + header.Del(h) + } + } + + if header.Get("A-Upgrade") != "" { + header.Set("Upgrade", header.Get("A-Upgrade")) + header.Del("A-Upgrade") + } +} + +func addXForwardedForHeader(req *http.Request) { + if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { + // If we aren't the first proxy retain prior + // X-Forwarded-For information as a comma+space + // separated list and fold multiple headers into one. + if prior, ok := req.Header["X-Forwarded-For"]; ok { + clientIP = strings.Join(prior, ", ") + ", " + clientIP + } + req.Header.Set("X-Forwarded-For", clientIP) + } +} + +func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request) error { + transport := p.Transport + if transport == nil { + transport = http.DefaultTransport + } + + outreq := new(http.Request) + // Shallow copies of maps, like header + *outreq = *req + + if cn, ok := rw.(http.CloseNotifier); ok { + if requestCanceler, ok := transport.(requestCanceler); ok { + // After the Handler has returned, there is no guarantee + // that the channel receives a value, so to make sure + reqDone := make(chan struct{}) + defer close(reqDone) + clientGone := cn.CloseNotify() + + go func() { + select { + case <-clientGone: + requestCanceler.CancelRequest(outreq) + case <-reqDone: + } + }() + } + } + + p.Director(outreq) + outreq.Close = false + + // We may modify the header (shallow copied above), so we only copy it. + outreq.Header = make(http.Header) + copyHeader(outreq.Header, req.Header) + + // Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers. + removeHeaders(outreq.Header) + + // Add X-Forwarded-For Header. + addXForwardedForHeader(outreq) + + res, err := transport.RoundTrip(outreq) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + + //rw.WriteHeader(http.StatusBadGateway) + return err + } + + // Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers. + removeHeaders(res.Header) + + if p.ModifyResponse != nil { + if err := p.ModifyResponse(res); err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + //rw.WriteHeader(http.StatusBadGateway) + return err + } + } + + // Copy header from response to client. + copyHeader(rw.Header(), res.Header) + + // The "Trailer" header isn't included in the Transport's response, Build it up from Trailer. + if len(res.Trailer) > 0 { + trailerKeys := make([]string, 0, len(res.Trailer)) + for k := range res.Trailer { + trailerKeys = append(trailerKeys, k) + } + rw.Header().Add("Trailer", strings.Join(trailerKeys, ", ")) + } + + rw.WriteHeader(res.StatusCode) + if len(res.Trailer) > 0 { + // Force chunking if we saw a response trailer. + // This prevents net/http from calculating the length for short + // bodies and adding a Content-Length. + if fl, ok := rw.(http.Flusher); ok { + fl.Flush() + } + } + + p.copyResponse(rw, res.Body) + // close now, instead of defer, to populate res.Trailer + res.Body.Close() + copyHeader(rw.Header(), res.Trailer) + + return nil +} + +func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) error { + hij, ok := rw.(http.Hijacker) + if !ok { + p.logf("http server does not support hijacker") + return errors.New("http server does not support hijacker") + } + + clientConn, _, err := hij.Hijack() + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + proxyConn, err := net.Dial("tcp", req.URL.Host) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + // The returned net.Conn may have read or write deadlines + // already set, depending on the configuration of the + // Server, to set or clear those deadlines as needed + // we set timeout to 5 minutes + deadline := time.Now() + if p.Timeout == 0 { + deadline = deadline.Add(time.Minute * 5) + } else { + deadline = deadline.Add(p.Timeout) + } + + err = clientConn.SetDeadline(deadline) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + err = proxyConn.SetDeadline(deadline) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + _, err = clientConn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) + if err != nil { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + go func() { + io.Copy(clientConn, proxyConn) + clientConn.Close() + proxyConn.Close() + }() + + io.Copy(proxyConn, clientConn) + proxyConn.Close() + clientConn.Close() + + return nil +} + +func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) error { + if req.Method == "CONNECT" { + err := p.ProxyHTTPS(rw, req) + return err + } else { + err := p.ProxyHTTP(rw, req) + return err + } +}
src/mod/sshprox/gotty/.gotty+302 −0 added@@ -0,0 +1,302 @@ +// [string] Address to listen, all addresses will be used when empty +// address = "" + +// [string] Port to listen +// port = "8080" + +// [bool] Permit clients to write to the TTY +// permit_write = false + +// [bool] Enable basic authentication +// enable_basic_auth = false + +// [string] Default username and password of basic authentication (user:pass) +// To enable basic authentication, set `true` to `enable_basic_auth` +// credential = "user:pass" + +// [bool] Enable random URL generation +// enable_random_url = false + +// [int] Default length of random strings appended to URL +// To enable random URL generation, set `true` to `enable_random_url` +// random_url_length = 8 + +// [bool] Enable TLS/SSL +// enable_tls = false + +// [string] Default TLS certificate file path +// tls_crt_file = "~/.gotty.crt" + +// [string] Default TLS key file path +// tls_key_file = "~/.gotty.key" + +// [bool] Enable client certificate authentication +// enable_tls_client_auth = false + +// [string] Certificate file of CA for client certificates +// tls_ca_crt_file = "~/.gotty.ca.crt" + +// [string] Custom index.html file +// index_file = "" + +// [string] Title format of browser window +// Available variables are: +// Command Command string +// Pid PID of the process for the client +// Hostname Server hostname +// RemoteAddr Client IP address +// title_format = "GoTTY - {{ .Command }} ({{ .Hostname }})" + +// [bool] Enable client side reconnection when connection closed +// enable_reconnect = false + +// [int] Interval time to try reconnection (seconds) +// To enable reconnection, set `true` to `enable_reconnect` +// reconnect_time = 10 + +// [int] Timeout seconds for waiting a client (0 to disable) +// timeout = 60 + +// [int] Maximum connection to gotty, 0(default) means no limit. +// max_connection = 0 + +// [bool] Accept only one client and exit gotty once the client exits +// once = false + +// [bool] Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB) +// permit_arguments = false + +// [object] Client terminal (hterm) preferences +// preferences { + + // [enum(null, "none", "ctrl-alt", "left-alt", "right-alt")] + // Select an AltGr detection hack^Wheuristic. + // null: Autodetect based on navigator.language: "en-us" => "none", else => "right-alt" + // "none": Disable any AltGr related munging. + // "ctrl-alt": Assume Ctrl+Alt means AltGr. + // "left-alt": Assume left Alt means AltGr. + // "right-alt": Assume right Alt means AltGr. + // alt_gr_mode = null + + // [bool] If set, alt-backspace indeed is alt-backspace. + // alt_backspace_is_meta_backspace = false + + // [bool] Set whether the alt key acts as a meta key or as a distinct alt key. + // alt_is_meta = false + + // [enum("escape", "8-bit", "browser-key")] + // Controls how the alt key is handled. + // "escape"....... Send an ESC prefix. + // "8-bit"........ Add 128 to the unshifted character as in xterm. + // "browser-key".. Wait for the keypress event and see what the browser says. + // (This won't work well on platforms where the browser performs a default action for some alt sequences.) + // alt_sends_what = "escape" + + // [string] URL of the terminal bell sound. Empty string for no audible bell. + // audible_bell_sound = "lib-resource:hterm/audio/bell" + + // [bool] If true, terminal bells in the background will create a Web Notification. http://www.w3.org/TR/notifications/ + // Displaying notifications requires permission from the user. + // When this option is set to true, hterm will attempt to ask the user for permission if necessary. + // Note browsers may not show this permission request + // if it did not originate from a user action. + // desktop_notification_bell = false + + // [string] The background color for text with no other color attributes. + // background_color = "rgb(16, 16, 16)" + + // [string] CSS value of the background image. Empty string for no image. + // For example: + // "url(https://goo.gl/anedTK) linear-gradient(top bottom, blue, red)" + // background_image = "" + + // [string] CSS value of the background image size. Defaults to none. + // background_size = "" + + // [string] CSS value of the background image position. + // For example: + // "10% 10% center" + // background_position = "" + + // [bool] If true, the backspace should send BS ('\x08', aka ^H). Otherwise the backspace key should send '\x7f'. + // backspace_sends_backspace = false + + // [map[string]map[string]string] + // A nested map where each property is the character set code and the value is a map that is a sparse array itself. + // In that sparse array, each property is the received character and the value is the displayed character. + // For example: + // {"0" = {"+" = "\u2192" + // "," = "\u2190" + // "-" = "\u2191" + // "." = "\u2193" + // "0" = "\u2588"}} + // character_map_overrides = null + + // [bool] Whether or not to close the window when the command exits. + // close_on_exit = true + + // [bool] Whether or not to blink the cursor by default. + // cursor_blink = false + + // [2[int]] The cursor blink rate in milliseconds. + // A two element array, the first of which is how long the cursor should be on, second is how long it should be off. + // cursor_blink_cycle = [1000, 500] + + // [string] The color of the visible cursor. + // cursor_color = "rgba(255, 0, 0, 0.5)" + + // [[]string] + // Override colors in the default palette. + // This can be specified as an array or an object. + // Values can be specified as almost any css color value. + // This includes #RGB, #RRGGBB, rgb(...), rgba(...), and any color names that are also part of the stock X11 rgb.txt file. + // You can use 'null' to specify that the default value should be not be changed. + // This is useful for skipping a small number of indicies when the value is specified as an array. + // color_palette_overrides = null + + // [bool] Automatically copy mouse selection to the clipboard. + copy_on_select = true + + // [bool] Whether to use the default window copy behaviour + //use_default_window_copy = false + + // [bool] Whether to clear the selection after copying. + clear_selection_after_copy = false + + // [bool] If true, Ctrl-Plus/Minus/Zero controls zoom. + // If false, Ctrl-Shift-Plus/Minus/Zero controls zoom, Ctrl-Minus sends ^_, Ctrl-Plus/Zero do nothing. + // ctrl_plus_minus_zero_zoom = true + + // [bool] Ctrl+C copies if true, send ^C to host if false. + // Ctrl+Shift+C sends ^C to host if true, copies if false. + // ctrl_c_copy = true + + // [bool] Ctrl+V pastes if true, send ^V to host if false. + // Ctrl+Shift+V sends ^V to host if true, pastes if false. + // ctrl_v_paste = true + + // [bool] Set whether East Asian Ambiguous characters have two column width. + // east_asian_ambiguous_as_two_column = false + + // [bool] True to enable 8-bit control characters, false to ignore them. + // We'll respect the two-byte versions of these control characters regardless of this setting. + // enable_8_bit_control = false + + // [enum(null, true, false)] + // True if we should use bold weight font for text with the bold/bright attribute. + // False to use the normal weight font. + // Null to autodetect. + // enable_bold = null + + // [bool] True if we should use bright colors (8-15 on a 16 color palette) for any text with the bold attribute. + // False otherwise. + // enable_bold_as_bright = true + + // [bool] Show a message in the terminal when the host writes to the clipboard. + // enable_clipboard_notice = true + + // [bool] Allow the host to write directly to the system clipboard. + // enable_clipboard_write = true + + // [bool] Respect the host's attempt to change the cursor blink status using DEC Private Mode 12. + // enable_dec12 = false + + // [map[string]string] The default environment variables, as an object. + // environment = {"TERM" = "xterm-256color"} + + // [string] Default font family for the terminal text. + // font_family = "'DejaVu Sans Mono', 'Everson Mono', FreeMono, 'Menlo', 'Terminal', monospace" + + // [int] The default font size in pixels. + // font_size = 15 + + // [string] CSS font-smoothing property. + // font_smoothing = "antialiased" + + // [string] The foreground color for text with no other color attributes. + // foreground_color = "rgb(240, 240, 240)" + + // [bool] If true, home/end will control the terminal scrollbar and shift home/end will send the VT keycodes. + // If false then home/end sends VT codes and shift home/end scrolls. + // home_keys_scroll = false + + // [map[string]string] + // A map of key sequence to key actions. + // Key sequences include zero or more modifier keys followed by a key code. + // Key codes can be decimal or hexadecimal numbers, or a key identifier. + // Key actions can be specified a string to send to the host, or an action identifier. + // For a full list of key code and action identifiers, see https://goo.gl/8AoD09. + // Sample keybindings: + // {"Ctrl-Alt-K" = "clearScrollback" + // "Ctrl-Shift-L"= "PASS" + // "Ctrl-H" = "'HELLO\n'"} + // keybindings = null + + // [int] Max length of a DCS, OSC, PM, or APS sequence before we give up and ignore the code. + // max_string_sequence = 100000 + + // [bool] If true, convert media keys to their Fkey equivalent. + // If false, let the browser handle the keys. + // media_keys_are_fkeys = false + + // [bool] Set whether the meta key sends a leading escape or not. + // meta_sends_escape = true + + // [enum(null, 0, 1, 2, 3, 4, 5, 6] + // Mouse paste button, or null to autodetect. + // For autodetect, we'll try to enable middle button paste for non-X11 platforms. + // On X11 we move it to button 3. + // mouse_paste_button = null + + // [bool] If true, page up/down will control the terminal scrollbar and shift page up/down will send the VT keycodes. + // If false then page up/down sends VT codes and shift page up/down scrolls. + // page_keys_scroll = false + + // [enum(null, true, false)] + // Set whether we should pass Alt-1..9 to the browser. + // This is handy when running hterm in a browser tab, so that you don't lose Chrome's "switch to tab" keyboard accelerators. + // When not running in a tab it's better to send these keys to the host so they can be used in vim or emacs. + // If true, Alt-1..9 will be handled by the browser. + // If false, Alt-1..9 will be sent to the host. + // If null, autodetect based on browser platform and window type. + // pass_alt_number = null + + // [enum(null, true, false)] + // Set whether we should pass Ctrl-1..9 to the browser. + // This is handy when running hterm in a browser tab, so that you don't lose Chrome's "switch to tab" keyboard accelerators. + // When not running in a tab it's better to send these keys to the host so they can be used in vim or emacs. + // If true, Ctrl-1..9 will be handled by the browser. + // If false, Ctrl-1..9 will be sent to the host. + // If null, autodetect based on browser platform and window type. + // pass_ctrl_number = null + + // [enum(null, true, false)] + // Set whether we should pass Meta-1..9 to the browser. + // This is handy when running hterm in a browser tab, so that you don't lose Chrome's "switch to tab" keyboard accelerators. + // When not running in a tab it's better to send these keys to the host so they can be used in vim or emacs. + // If true, Meta-1..9 will be handled by the browser. + // If false, Meta-1..9 will be sent to the host. If null, autodetect based on browser platform and window type. + // pass_meta_number = null + + // [bool] Set whether meta-V gets passed to host. + // pass_meta_v = true + + // [bool] If true, scroll to the bottom on any keystroke. + // scroll_on_keystroke = true + + // [bool] If true, scroll to the bottom on terminal output. + // scroll_on_output = false + + // [bool] The vertical scrollbar mode. + // scrollbar_visible = true + + // [int] The multiplier for the pixel delta in mousewheel event caused by the scroll wheel. Alters how fast the page scrolls. + // scroll_wheel_move_multiplier = 1 + + // [bool] Shift + Insert pastes if true, sent to host if false. + // shift_insert_paste = true + + // [string] URL of user stylesheet to include in the terminal document. + // user_css = "" + +// }
src/mod/sshprox/gotty/gotty_linux_386+0 −0 addedsrc/mod/sshprox/gotty/gotty_linux_amd64+0 −0 addedsrc/mod/sshprox/gotty/gotty_linux_arm+0 −0 addedsrc/mod/sshprox/gotty/gotty_linux_arm64+0 −0 addedsrc/mod/sshprox/gotty/LICENSE+21 −0 added@@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2017 Iwasaki Yudai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.
src/mod/sshprox/sshprox.go+222 −0 added@@ -0,0 +1,222 @@ +package sshprox + +import ( + "embed" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/google/uuid" + "imuslab.com/zoraxy/mod/reverseproxy" + "imuslab.com/zoraxy/mod/utils" + "imuslab.com/zoraxy/mod/websocketproxy" +) + +/* + SSH Proxy + + This is a tool to bind gotty into Zoraxy + so that you can do something similar to + online ssh terminal +*/ + +/* + Bianry embedding + + Make sure when compile, gotty binary exists in static.gotty +*/ +var ( + //go:embed gotty/* + gotty embed.FS +) + +type Manager struct { + StartingPort int + Instances []*Instance +} + +type Instance struct { + UUID string + ExecPath string + RemoteAddr string + RemotePort int + AssignedPort int + conn *reverseproxy.ReverseProxy //HTTP proxy + tty *exec.Cmd //SSH connection ported to web interface + Parent *Manager +} + +func NewSSHProxyManager() *Manager { + return &Manager{ + StartingPort: 14810, + Instances: []*Instance{}, + } +} + +//Get the next free port in the list +func (m *Manager) GetNextPort() int { + nextPort := m.StartingPort + occupiedPort := make(map[int]bool) + for _, instance := range m.Instances { + occupiedPort[instance.AssignedPort] = true + } + for { + if !occupiedPort[nextPort] { + return nextPort + } + nextPort++ + } +} + +func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) { + targetInstance, err := m.GetInstanceById(instanceId) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + if targetInstance.tty == nil { + //Server side already closed + http.Error(w, "Connection already closed", http.StatusInternalServerError) + return + } + + r.Header.Set("X-Forwarded-Host", r.Host) + requestURL := r.URL.String() + if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { + //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin + r.Header.Set("A-Upgrade", "websocket") + requestURL = strings.TrimPrefix(requestURL, "/") + u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL) + wspHandler := websocketproxy.NewProxy(u) + wspHandler.ServeHTTP(w, r) + return + } + + targetInstance.conn.ProxyHTTP(w, r) +} + +func (m *Manager) GetInstanceById(instanceId string) (*Instance, error) { + for _, instance := range m.Instances { + if instance.UUID == instanceId { + return instance, nil + } + } + return nil, fmt.Errorf("instance not found: %s", instanceId) +} +func (m *Manager) NewSSHProxy(binaryRoot string) (*Instance, error) { + //Check if the binary exists in system/gotty/ + binary := "gotty_" + runtime.GOOS + "_" + runtime.GOARCH + + if runtime.GOOS == "windows" { + binary = binary + ".exe" + } + + //Extract it from embedfs if not exists locally + execPath := filepath.Join(binaryRoot, binary) + + //Create the storage folder structure + os.MkdirAll(filepath.Dir(execPath), 0775) + + //Create config file if not exists + if !utils.FileExists(filepath.Join(filepath.Dir(execPath), ".gotty")) { + configFile, _ := gotty.ReadFile("gotty/.gotty") + os.WriteFile(filepath.Join(filepath.Dir(execPath), ".gotty"), configFile, 0775) + } + + //Create web.ssh binary if not exists + if !utils.FileExists(execPath) { + //Try to extract it from embedded fs + executable, err := gotty.ReadFile("gotty/" + binary) + if err != nil { + //Binary not found in embedded + return nil, errors.New("platform not supported") + } + + //Extract to target location + err = os.WriteFile(execPath, executable, 0777) + if err != nil { + //Binary not found in embedded + log.Println("Extract web.ssh failed: " + err.Error()) + return nil, errors.New("web.ssh sub-program extract failed") + } + } + + //Convert the binary path to realpath + realpath, err := filepath.Abs(execPath) + if err != nil { + return nil, err + } + + thisInstance := Instance{ + UUID: uuid.New().String(), + ExecPath: realpath, + AssignedPort: -1, + Parent: m, + } + + m.Instances = append(m.Instances, &thisInstance) + + return &thisInstance, nil +} + +//Create a new Connection to target address +func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIpAddr string, remotePort int) error { + //Create a gotty instance + connAddr := remoteIpAddr + if username != "" { + connAddr = username + "@" + remoteIpAddr + } + configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty") + title := username + "@" + remoteIpAddr + if remotePort != 22 { + title = title + ":" + strconv.Itoa(remotePort) + } + + sshCommand := []string{"ssh", "-t", connAddr, "-p", strconv.Itoa(remotePort)} + cmd := exec.Command(i.ExecPath, "-w", "-p", strconv.Itoa(listenPort), "--once", "--config", configPath, "--title-format", title, "bash", "-c", strings.Join(sshCommand, " ")) + cmd.Dir = filepath.Dir(i.ExecPath) + cmd.Env = append(os.Environ(), "TERM=xterm") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + go func() { + cmd.Run() + i.Destroy() + }() + i.tty = cmd + i.AssignedPort = listenPort + i.RemoteAddr = remoteIpAddr + i.RemotePort = remotePort + + //Create a new proxy agent for this root + path, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(listenPort)) + if err != nil { + return err + } + + //Create new proxy objects to the proxy + proxy := reverseproxy.NewReverseProxy(path) + + i.conn = proxy + return nil +} + +func (i *Instance) Destroy() { + // Remove the instance from the Manager's Instances list + for idx, inst := range i.Parent.Instances { + if inst == i { + // Remove the instance from the slice by swapping it with the last instance and slicing the slice + i.Parent.Instances[len(i.Parent.Instances)-1], i.Parent.Instances[idx] = i.Parent.Instances[idx], i.Parent.Instances[len(i.Parent.Instances)-1] + i.Parent.Instances = i.Parent.Instances[:len(i.Parent.Instances)-1] + break + } + } +}
src/mod/sshprox/utils.go+72 −0 added@@ -0,0 +1,72 @@ +package sshprox + +import ( + "fmt" + "net" + "net/url" + "runtime" + "strings" + "time" +) + +//Rewrite url based on proxy root +func RewriteURL(rooturl string, requestURL string) (*url.URL, error) { + rewrittenURL := strings.TrimPrefix(requestURL, rooturl) + return url.Parse(rewrittenURL) +} + +//Check if the current platform support web.ssh function +func IsWebSSHSupported() bool { + //Check if the binary exists in system/gotty/ + binary := "gotty_" + runtime.GOOS + "_" + runtime.GOARCH + + if runtime.GOOS == "windows" { + binary = binary + ".exe" + } + + //Check if the target gotty terminal exists + f, err := gotty.Open("gotty/" + binary) + if err != nil { + return false + } + + f.Close() + return true +} + +//Check if a given domain and port is a valid ssh server +func IsSSHConnectable(ipOrDomain string, port int) bool { + timeout := time.Second * 3 + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ipOrDomain, port), timeout) + if err != nil { + return false + } + defer conn.Close() + + // Send an SSH version identification string to the server to check if it's SSH + _, err = conn.Write([]byte("SSH-2.0-Go\r\n")) + if err != nil { + return false + } + + // Wait for a response from the server + buf := make([]byte, 1024) + _, err = conn.Read(buf) + if err != nil { + return false + } + + // Check if the response starts with "SSH-2.0" + return string(buf[:7]) == "SSH-2.0" +} + +//Check if the port is used by other process or application +func isPortInUse(port int) bool { + address := fmt.Sprintf(":%d", port) + listener, err := net.Listen("tcp", address) + if err != nil { + return true + } + listener.Close() + return false +}
src/mod/statistic/analytic/analytic.go+128 −0 added@@ -0,0 +1,128 @@ +package analytic + +import ( + "encoding/json" + "net/http" + "strings" + "time" + + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/statistic" + "imuslab.com/zoraxy/mod/utils" +) + +type DataLoader struct { + Database *database.Database + StatisticCollector *statistic.Collector +} + +// Create a new data loader for loading statistic from database +func NewDataLoader(db *database.Database, sc *statistic.Collector) *DataLoader { + return &DataLoader{ + Database: db, + StatisticCollector: sc, + } +} + +func (d *DataLoader) HandleSummaryList(w http.ResponseWriter, r *http.Request) { + entries, err := d.Database.ListTable("stats") + if err != nil { + utils.SendErrorResponse(w, "unable to load data from database") + return + } + + entryDates := []string{} + for _, keypairs := range entries { + entryDates = append(entryDates, string(keypairs[0])) + } + + js, _ := json.MarshalIndent(entryDates, "", " ") + utils.SendJSONResponse(w, string(js)) +} + +func (d *DataLoader) HandleLoadTargetDaySummary(w http.ResponseWriter, r *http.Request) { + day, err := utils.GetPara(r, "id") + if err != nil { + utils.SendErrorResponse(w, "id cannot be empty") + return + } + + if strings.Contains(day, "-") { + //Must be underscore + day = strings.ReplaceAll(day, "-", "_") + } + + if !statistic.IsBeforeToday(day) { + utils.SendErrorResponse(w, "given date is in the future") + return + } + + var targetDailySummary statistic.DailySummaryExport + + if day == time.Now().Format("2006_01_02") { + targetDailySummary = *d.StatisticCollector.GetExportSummary() + } else { + //Not today data + err = d.Database.Read("stats", day, &targetDailySummary) + if err != nil { + utils.SendErrorResponse(w, "target day data not found") + return + } + } + + js, _ := json.Marshal(targetDailySummary) + utils.SendJSONResponse(w, string(js)) +} + +func (d *DataLoader) HandleLoadTargetRangeSummary(w http.ResponseWriter, r *http.Request) { + //Get the start date from POST para + start, err := utils.GetPara(r, "start") + if err != nil { + utils.SendErrorResponse(w, "start date cannot be empty") + return + } + if strings.Contains(start, "-") { + //Must be underscore + start = strings.ReplaceAll(start, "-", "_") + } + //Get end date from POST para + end, err := utils.GetPara(r, "end") + if err != nil { + utils.SendErrorResponse(w, "emd date cannot be empty") + return + } + if strings.Contains(end, "-") { + //Must be underscore + end = strings.ReplaceAll(end, "-", "_") + } + + //Generate all the dates in between the range + keys, err := generateDateRange(start, end) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + //Load all the data from database + dailySummaries := []*statistic.DailySummaryExport{} + for _, key := range keys { + thisStat := statistic.DailySummaryExport{} + err = d.Database.Read("stats", key, &thisStat) + if err == nil { + dailySummaries = append(dailySummaries, &thisStat) + } + } + + //Merge the summaries into one + mergedSummary := mergeDailySummaryExports(dailySummaries) + + js, _ := json.Marshal(struct { + Summary *statistic.DailySummaryExport + Records []*statistic.DailySummaryExport + }{ + Summary: mergedSummary, + Records: dailySummaries, + }) + + utils.SendJSONResponse(w, string(js)) +}
src/mod/statistic/analytic/utils.go+72 −0 added@@ -0,0 +1,72 @@ +package analytic + +import ( + "fmt" + "time" + + "imuslab.com/zoraxy/mod/statistic" +) + +// Generate all the record keys from a given start and end dates +func generateDateRange(startDate, endDate string) ([]string, error) { + layout := "2006_01_02" + start, err := time.Parse(layout, startDate) + if err != nil { + return nil, fmt.Errorf("error parsing start date: %v", err) + } + + end, err := time.Parse(layout, endDate) + if err != nil { + return nil, fmt.Errorf("error parsing end date: %v", err) + } + + var dateRange []string + for d := start; !d.After(end); d = d.AddDate(0, 0, 1) { + dateRange = append(dateRange, d.Format(layout)) + } + + return dateRange, nil +} + +func mergeDailySummaryExports(exports []*statistic.DailySummaryExport) *statistic.DailySummaryExport { + mergedExport := &statistic.DailySummaryExport{ + ForwardTypes: make(map[string]int), + RequestOrigin: make(map[string]int), + RequestClientIp: make(map[string]int), + Referer: make(map[string]int), + UserAgent: make(map[string]int), + RequestURL: make(map[string]int), + } + + for _, export := range exports { + mergedExport.TotalRequest += export.TotalRequest + mergedExport.ErrorRequest += export.ErrorRequest + mergedExport.ValidRequest += export.ValidRequest + + for key, value := range export.ForwardTypes { + mergedExport.ForwardTypes[key] += value + } + + for key, value := range export.RequestOrigin { + mergedExport.RequestOrigin[key] += value + } + + for key, value := range export.RequestClientIp { + mergedExport.RequestClientIp[key] += value + } + + for key, value := range export.Referer { + mergedExport.Referer[key] += value + } + + for key, value := range export.UserAgent { + mergedExport.UserAgent[key] += value + } + + for key, value := range export.RequestURL { + mergedExport.RequestURL[key] += value + } + } + + return mergedExport +}
src/mod/statistic/handler.go+40 −0 added@@ -0,0 +1,40 @@ +package statistic + +import ( + "encoding/json" + "net/http" + + "imuslab.com/zoraxy/mod/utils" +) + +/* + Handler.go + + This script handles incoming request for loading the statistic of the day + +*/ + +func (c *Collector) HandleTodayStatLoad(w http.ResponseWriter, r *http.Request) { + + fast, err := utils.GetPara(r, "fast") + if err != nil { + fast = "false" + } + d := c.DailySummary + if fast == "true" { + //Only return the counter + exported := DailySummaryExport{ + TotalRequest: d.TotalRequest, + ErrorRequest: d.ErrorRequest, + ValidRequest: d.ValidRequest, + } + js, _ := json.Marshal(exported) + utils.SendJSONResponse(w, string(js)) + } else { + //Return everything + exported := c.GetExportSummary() + js, _ := json.Marshal(exported) + utils.SendJSONResponse(w, string(js)) + } + +}
src/mod/statistic/statistic.go+237 −0 added@@ -0,0 +1,237 @@ +package statistic + +import ( + "path/filepath" + "strings" + "sync" + "time" + + "imuslab.com/zoraxy/mod/database" +) + +/* + Statistic Package + + This packet is designed to collection information + and store them for future analysis +*/ + +// Faststat, a interval summary for all collected data and avoid +// looping through every data everytime a overview is needed +type DailySummary struct { + TotalRequest int64 //Total request of the day + ErrorRequest int64 //Invalid request of the day, including error or not found + ValidRequest int64 //Valid request of the day + //Type counters + ForwardTypes *sync.Map //Map that hold the forward types + RequestOrigin *sync.Map //Map that hold [country ISO code]: visitor counter + RequestClientIp *sync.Map //Map that hold all unique request IPs + Referer *sync.Map //Map that store where the user was refered from + UserAgent *sync.Map //Map that store the useragent of the request + RequestURL *sync.Map //Request URL of the request object +} + +type RequestInfo struct { + IpAddr string + RequestOriginalCountryISOCode string + Succ bool + StatusCode int + ForwardType string + Referer string + UserAgent string + RequestURL string + Target string +} + +type CollectorOption struct { + Database *database.Database +} + +type Collector struct { + rtdataStopChan chan bool + DailySummary *DailySummary + Option *CollectorOption +} + +func NewStatisticCollector(option CollectorOption) (*Collector, error) { + option.Database.NewTable("stats") + + //Create the collector object + thisCollector := Collector{ + DailySummary: newDailySummary(), + Option: &option, + } + + //Load the stat if exists for today + //This will exists if the program was forcefully restarted + year, month, day := time.Now().Date() + summary := thisCollector.LoadSummaryOfDay(year, month, day) + if summary != nil { + thisCollector.DailySummary = summary + } + + //Schedule the realtime statistic clearing at midnight everyday + rtstatStopChan := thisCollector.ScheduleResetRealtimeStats() + thisCollector.rtdataStopChan = rtstatStopChan + + return &thisCollector, nil +} + +// Write the current in-memory summary to database file +func (c *Collector) SaveSummaryOfDay() { + //When it is called in 0:00am, make sure it is stored as yesterday key + t := time.Now().Add(-30 * time.Second) + summaryKey := t.Format("2006_01_02") + saveData := DailySummaryToExport(*c.DailySummary) + c.Option.Database.Write("stats", summaryKey, saveData) +} + +// Load the summary of a day given +func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *DailySummary { + date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local) + summaryKey := date.Format("2006_01_02") + targetSummaryExport := DailySummaryExport{} + c.Option.Database.Read("stats", summaryKey, &targetSummaryExport) + targetSummary := DailySummaryExportToSummary(targetSummaryExport) + return &targetSummary +} + +// This function gives the current slot in the 288- 5 minutes interval of the day +func (c *Collector) GetCurrentRealtimeStatIntervalId() int { + now := time.Now() + startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).Unix() + secondsSinceStartOfDay := now.Unix() - startOfDay + interval := secondsSinceStartOfDay / (5 * 60) + return int(interval) +} + +func (c *Collector) Close() { + //Stop the ticker + c.rtdataStopChan <- true + + //Write the buffered data into database + c.SaveSummaryOfDay() + +} + +// Main function to record all the inbound traffics +// Note that this function run in go routine and might have concurrent R/W issue +// Please make sure there is no racing paramters in this function +func (c *Collector) RecordRequest(ri RequestInfo) { + go func() { + c.DailySummary.TotalRequest++ + if ri.Succ { + c.DailySummary.ValidRequest++ + } else { + c.DailySummary.ErrorRequest++ + } + + //Store the request info into correct types of maps + ft, ok := c.DailySummary.ForwardTypes.Load(ri.ForwardType) + if !ok { + c.DailySummary.ForwardTypes.Store(ri.ForwardType, 1) + } else { + c.DailySummary.ForwardTypes.Store(ri.ForwardType, ft.(int)+1) + } + + originISO := strings.ToLower(ri.RequestOriginalCountryISOCode) + fo, ok := c.DailySummary.RequestOrigin.Load(originISO) + if !ok { + c.DailySummary.RequestOrigin.Store(originISO, 1) + } else { + c.DailySummary.RequestOrigin.Store(originISO, fo.(int)+1) + } + + //Filter out CF forwarded requests + if strings.Contains(ri.IpAddr, ",") { + ips := strings.Split(strings.TrimSpace(ri.IpAddr), ",") + if len(ips) >= 1 { + ri.IpAddr = ips[0] + } + } + + fi, ok := c.DailySummary.RequestClientIp.Load(ri.IpAddr) + if !ok { + c.DailySummary.RequestClientIp.Store(ri.IpAddr, 1) + } else { + c.DailySummary.RequestClientIp.Store(ri.IpAddr, fi.(int)+1) + } + + //Record the referer + rf, ok := c.DailySummary.Referer.Load(ri.Referer) + if !ok { + c.DailySummary.Referer.Store(ri.Referer, 1) + } else { + c.DailySummary.Referer.Store(ri.Referer, rf.(int)+1) + } + + //Record the UserAgent + ua, ok := c.DailySummary.UserAgent.Load(ri.UserAgent) + if !ok { + c.DailySummary.UserAgent.Store(ri.UserAgent, 1) + } else { + c.DailySummary.UserAgent.Store(ri.UserAgent, ua.(int)+1) + } + + //ADD MORE HERE IF NEEDED + + //Record request URL, if it is a page + ext := filepath.Ext(ri.RequestURL) + + if ext != "" && !isWebPageExtension(ext) { + return + } + + ru, ok := c.DailySummary.RequestURL.Load(ri.RequestURL) + if !ok { + c.DailySummary.RequestURL.Store(ri.RequestURL, 1) + } else { + c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1) + } + }() +} + +// nightly task +func (c *Collector) ScheduleResetRealtimeStats() chan bool { + doneCh := make(chan bool) + + go func() { + defer close(doneCh) + + for { + // calculate duration until next midnight + now := time.Now() + + // Get midnight of the next day in the local time zone + midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location()) + + // Calculate the duration until midnight + duration := midnight.Sub(now) + select { + case <-time.After(duration): + // store daily summary to database and reset summary + c.SaveSummaryOfDay() + c.DailySummary = newDailySummary() + case <-doneCh: + // stop the routine + return + } + } + }() + + return doneCh +} + +func newDailySummary() *DailySummary { + return &DailySummary{ + TotalRequest: 0, + ErrorRequest: 0, + ValidRequest: 0, + ForwardTypes: &sync.Map{}, + RequestOrigin: &sync.Map{}, + RequestClientIp: &sync.Map{}, + Referer: &sync.Map{}, + UserAgent: &sync.Map{}, + RequestURL: &sync.Map{}, + } +}
src/mod/statistic/structconv.go+108 −0 added@@ -0,0 +1,108 @@ +package statistic + +import "sync" + +type DailySummaryExport struct { + TotalRequest int64 //Total request of the day + ErrorRequest int64 //Invalid request of the day, including error or not found + ValidRequest int64 //Valid request of the day + + ForwardTypes map[string]int + RequestOrigin map[string]int + RequestClientIp map[string]int + Referer map[string]int + UserAgent map[string]int + RequestURL map[string]int +} + +func DailySummaryToExport(summary DailySummary) DailySummaryExport { + export := DailySummaryExport{ + TotalRequest: summary.TotalRequest, + ErrorRequest: summary.ErrorRequest, + ValidRequest: summary.ValidRequest, + ForwardTypes: make(map[string]int), + RequestOrigin: make(map[string]int), + RequestClientIp: make(map[string]int), + Referer: make(map[string]int), + UserAgent: make(map[string]int), + RequestURL: make(map[string]int), + } + + summary.ForwardTypes.Range(func(key, value interface{}) bool { + export.ForwardTypes[key.(string)] = value.(int) + return true + }) + + summary.RequestOrigin.Range(func(key, value interface{}) bool { + export.RequestOrigin[key.(string)] = value.(int) + return true + }) + + summary.RequestClientIp.Range(func(key, value interface{}) bool { + export.RequestClientIp[key.(string)] = value.(int) + return true + }) + + summary.Referer.Range(func(key, value interface{}) bool { + export.Referer[key.(string)] = value.(int) + return true + }) + + summary.UserAgent.Range(func(key, value interface{}) bool { + export.UserAgent[key.(string)] = value.(int) + return true + }) + + summary.RequestURL.Range(func(key, value interface{}) bool { + export.RequestURL[key.(string)] = value.(int) + return true + }) + + return export +} + +func DailySummaryExportToSummary(export DailySummaryExport) DailySummary { + summary := DailySummary{ + TotalRequest: export.TotalRequest, + ErrorRequest: export.ErrorRequest, + ValidRequest: export.ValidRequest, + ForwardTypes: &sync.Map{}, + RequestOrigin: &sync.Map{}, + RequestClientIp: &sync.Map{}, + Referer: &sync.Map{}, + UserAgent: &sync.Map{}, + RequestURL: &sync.Map{}, + } + + for k, v := range export.ForwardTypes { + summary.ForwardTypes.Store(k, v) + } + + for k, v := range export.RequestOrigin { + summary.RequestOrigin.Store(k, v) + } + + for k, v := range export.RequestClientIp { + summary.RequestClientIp.Store(k, v) + } + + for k, v := range export.Referer { + summary.Referer.Store(k, v) + } + + for k, v := range export.UserAgent { + summary.UserAgent.Store(k, v) + } + + for k, v := range export.RequestURL { + summary.RequestURL.Store(k, v) + } + + return summary +} + +// External object function call +func (c *Collector) GetExportSummary() *DailySummaryExport { + exportFormatDailySummary := DailySummaryToExport(*c.DailySummary) + return &exportFormatDailySummary +}
src/mod/statistic/utils.go+28 −0 added@@ -0,0 +1,28 @@ +package statistic + +import ( + "fmt" + "time" +) + +func isWebPageExtension(ext string) bool { + webPageExts := []string{".html", ".htm", ".php", ".jsp", ".aspx", ".js", ".jsx"} + for _, e := range webPageExts { + if e == ext { + return true + } + } + return false +} + +func IsBeforeToday(dateString string) bool { + layout := "2006_01_02" + date, err := time.Parse(layout, dateString) + if err != nil { + fmt.Println("Error parsing date:", err) + return false + } + + today := time.Now().UTC().Truncate(24 * time.Hour) + return date.Before(today) || dateString == time.Now().Format(layout) +}
src/mod/tcpprox/conn.go+324 −0 added@@ -0,0 +1,324 @@ +package tcpprox + +import ( + "errors" + "io" + "log" + "net" + "strconv" + "sync" + "time" +) + +func isValidIP(ip string) bool { + parsedIP := net.ParseIP(ip) + return parsedIP != nil +} + +func isValidPort(port string) bool { + portInt, err := strconv.Atoi(port) + if err != nil { + return false + } + + if portInt < 1 || portInt > 65535 { + return false + } + + return true +} + +func isReachable(target string) bool { + timeout := time.Duration(2 * time.Second) // Set the timeout value as per your requirement + conn, err := net.DialTimeout("tcp", target, timeout) + if err != nil { + return false + } + defer conn.Close() + return true +} + +func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *int64) { + io.Copy(conn1, conn2) + conn1.Close() + log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]") + //conn2.Close() + //log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]") + wg.Done() +} + +func forward(conn1 net.Conn, conn2 net.Conn, aTob *int64, bToa *int64) { + log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String()) + var wg sync.WaitGroup + // wait tow goroutines + wg.Add(2) + go connCopy(conn1, conn2, &wg, aTob) + go connCopy(conn2, conn1, &wg, bToa) + //blocking when the wg is locked + wg.Wait() +} + +func accept(listener net.Listener) (net.Conn, error) { + conn, err := listener.Accept() + if err != nil { + return nil, err + } + log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]") + return conn, err +} + +func startListener(address string) (net.Listener, error) { + log.Println("[+]", "try to start server on:["+address+"]") + server, err := net.Listen("tcp", address) + if err != nil { + return nil, errors.New("listen address [" + address + "] faild") + } + log.Println("[√]", "start listen at address:["+address+"]") + return server, nil +} + +/* + Config Functions +*/ + +// Config validator +func (c *ProxyRelayConfig) ValidateConfigs() error { + if c.Mode == ProxyMode_Transport { + //Port2Host: PortA int, PortB string + if !isValidPort(c.PortA) { + return errors.New("first address must be a valid port number") + } + + if !isReachable(c.PortB) { + return errors.New("second address is unreachable") + } + return nil + + } else if c.Mode == ProxyMode_Listen { + //Port2Port: Both port are port number + if !isValidPort(c.PortA) { + return errors.New("first address is not a valid port number") + } + + if !isValidPort(c.PortB) { + return errors.New("second address is not a valid port number") + } + + return nil + } else if c.Mode == ProxyMode_Starter { + //Host2Host: Both have to be hosts + if !isReachable(c.PortA) { + return errors.New("first address is unreachable") + } + + if !isReachable(c.PortB) { + return errors.New("second address is unreachable") + } + + return nil + } else { + return errors.New("invalid mode given") + } +} + +// Start a proxy if stopped +func (c *ProxyRelayConfig) Start() error { + if c.Running { + return errors.New("proxy already running") + } + + // Create a stopChan to control the loop + stopChan := make(chan bool) + c.stopChan = stopChan + + //Validate configs + err := c.ValidateConfigs() + if err != nil { + return err + } + + //Start the proxy service + go func() { + c.Running = true + if c.Mode == ProxyMode_Transport { + err = c.Port2host(c.PortA, c.PortB, stopChan) + } else if c.Mode == ProxyMode_Listen { + err = c.Port2port(c.PortA, c.PortB, stopChan) + } else if c.Mode == ProxyMode_Starter { + err = c.Host2host(c.PortA, c.PortB, stopChan) + } + if err != nil { + c.Running = false + log.Println("Error starting proxy service " + c.Name + "(" + c.UUID + "): " + err.Error()) + } + }() + + //Successfully spawned off the proxy routine + return nil +} + +// Stop a running proxy if running +func (c *ProxyRelayConfig) Stop() { + if c.Running || c.stopChan != nil { + c.stopChan <- true + time.Sleep(300 * time.Millisecond) + c.stopChan = nil + c.Running = false + } +} + +/* + Forwarder Functions +*/ + +/* +portA -> server +portB -> server +*/ +func (c *ProxyRelayConfig) Port2port(port1 string, port2 string, stopChan chan bool) error { + //Trim the Prefix of : if exists + listen1, err := startListener("0.0.0.0:" + port1) + if err != nil { + return err + } + listen2, err := startListener("0.0.0.0:" + port2) + if err != nil { + return err + } + + log.Println("[√]", "listen port:", port1, "and", port2, "success. waiting for client...") + c.Running = true + + go func() { + <-stopChan + log.Println("[x]", "Received stop signal. Exiting Port to Port forwarder") + c.Running = false + listen1.Close() + listen2.Close() + }() + + for { + conn1, err := accept(listen1) + if err != nil { + if !c.Running { + return nil + } + continue + } + + conn2, err := accept(listen2) + if err != nil { + if !c.Running { + return nil + } + continue + } + + if conn1 == nil || conn2 == nil { + log.Println("[x]", "accept client faild. retry in ", c.Timeout, " seconds. ") + time.Sleep(time.Duration(c.Timeout) * time.Second) + continue + } + forward(conn1, conn2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer) + } +} + +/* +portA -> server +server -> portB +*/ +func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, stopChan chan bool) error { + server, err := startListener("0.0.0.0:" + allowPort) + if err != nil { + return err + } + + //Start stop handler + go func() { + <-stopChan + log.Println("[x]", "Received stop signal. Exiting Port to Host forwarder") + c.Running = false + server.Close() + }() + + //Start blocking loop for accepting connections + for { + conn, err := accept(server) + if conn == nil || err != nil { + if !c.Running { + //Terminate by stop chan. Exit listener loop + return nil + } + + //Connection error. Retry + continue + } + + go func(targetAddress string) { + log.Println("[+]", "start connect host:["+targetAddress+"]") + target, err := net.Dial("tcp", targetAddress) + if err != nil { + // temporarily unavailable, don't use fatal. + log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", c.Timeout, "seconds. ") + conn.Close() + log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]") + time.Sleep(time.Duration(c.Timeout) * time.Second) + return + } + log.Println("[→]", "connect target address ["+targetAddress+"] success.") + forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer) + }(targetAddress) + } +} + +/* +server -> portA +server -> portB +*/ +func (c *ProxyRelayConfig) Host2host(address1, address2 string, stopChan chan bool) error { + c.Running = true + go func() { + <-stopChan + log.Println("[x]", "Received stop signal. Exiting Host to Host forwarder") + c.Running = false + }() + + for c.Running { + log.Println("[+]", "try to connect host:["+address1+"] and ["+address2+"]") + var host1, host2 net.Conn + var err error + for { + d := net.Dialer{Timeout: time.Duration(c.Timeout)} + host1, err = d.Dial("tcp", address1) + if err == nil { + log.Println("[→]", "connect ["+address1+"] success.") + break + } else { + log.Println("[x]", "connect target address ["+address1+"] faild. retry in ", c.Timeout, " seconds. ") + time.Sleep(time.Duration(c.Timeout) * time.Second) + } + + if !c.Running { + return nil + } + } + for { + d := net.Dialer{Timeout: time.Duration(c.Timeout)} + host2, err = d.Dial("tcp", address2) + if err == nil { + log.Println("[→]", "connect ["+address2+"] success.") + break + } else { + log.Println("[x]", "connect target address ["+address2+"] faild. retry in ", c.Timeout, " seconds. ") + time.Sleep(time.Duration(c.Timeout) * time.Second) + } + + if !c.Running { + return nil + } + } + forward(host1, host2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer) + } + + return nil +}
src/mod/tcpprox/handler.go+162 −0 added@@ -0,0 +1,162 @@ +package tcpprox + +import ( + "encoding/json" + "net/http" + "strconv" + + "imuslab.com/zoraxy/mod/utils" +) + +/* + Handler.go + Handlers for the tcprox. Remove this file + if your application do not need any http + handler. +*/ + +func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) { + name, err := utils.PostPara(r, "name") + if err != nil { + utils.SendErrorResponse(w, "name cannot be empty") + return + } + + portA, err := utils.PostPara(r, "porta") + if err != nil { + utils.SendErrorResponse(w, "first address cannot be empty") + return + } + + portB, err := utils.PostPara(r, "portb") + if err != nil { + utils.SendErrorResponse(w, "second address cannot be empty") + return + } + + timeoutStr, _ := utils.PostPara(r, "timeout") + timeout := m.Options.DefaultTimeout + if timeoutStr != "" { + timeout, err = strconv.Atoi(timeoutStr) + if err != nil { + utils.SendErrorResponse(w, "invalid timeout value: "+timeoutStr) + return + } + } + + modeValue := ProxyMode_Transport + mode, err := utils.PostPara(r, "mode") + if err != nil || mode == "" { + utils.SendErrorResponse(w, "no mode given") + } else if mode == "listen" { + modeValue = ProxyMode_Listen + } else if mode == "transport" { + modeValue = ProxyMode_Transport + } else if mode == "starter" { + modeValue = ProxyMode_Starter + } else { + utils.SendErrorResponse(w, "invalid mode given. Only support listen / transport / starter") + } + + //Create the target config + newConfigUUID := m.NewConfig(&ProxyRelayOptions{ + Name: name, + PortA: portA, + PortB: portB, + Timeout: timeout, + Mode: modeValue, + }) + + js, _ := json.Marshal(newConfigUUID) + utils.SendJSONResponse(w, string(js)) +} + +func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request) { + // Extract POST parameters using utils.PostPara + configUUID, err := utils.PostPara(r, "uuid") + if err != nil { + utils.SendErrorResponse(w, "config UUID cannot be empty") + return + } + + newName, _ := utils.PostPara(r, "name") + newPortA, _ := utils.PostPara(r, "porta") + newPortB, _ := utils.PostPara(r, "portb") + newModeStr, _ := utils.PostPara(r, "mode") + newMode := -1 + if newModeStr != "" { + if newModeStr == "listen" { + newMode = 0 + } else if newModeStr == "transport" { + newMode = 1 + } else if newModeStr == "starter" { + newMode = 2 + } else { + utils.SendErrorResponse(w, "invalid new mode value") + return + } + } + + newTimeoutStr, _ := utils.PostPara(r, "timeout") + newTimeout := -1 + if newTimeoutStr != "" { + newTimeout, err = strconv.Atoi(newTimeoutStr) + if err != nil { + utils.SendErrorResponse(w, "invalid newTimeout value: "+newTimeoutStr) + return + } + } + + // Call the EditConfig method to modify the configuration + err = m.EditConfig(configUUID, newName, newPortA, newPortB, newMode, newTimeout) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +func (m *Manager) HandleListConfigs(w http.ResponseWriter, r *http.Request) { + js, _ := json.Marshal(m.Configs) + utils.SendJSONResponse(w, string(js)) +} + +func (m *Manager) HandleGetProxyStatus(w http.ResponseWriter, r *http.Request) { + uuid, err := utils.GetPara(r, "uuid") + if err != nil { + utils.SendErrorResponse(w, "invalid uuid given") + return + } + + targetConfig, err := m.GetConfigByUUID(uuid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(targetConfig) + utils.SendJSONResponse(w, string(js)) +} + +func (m *Manager) HandleConfigValidate(w http.ResponseWriter, r *http.Request) { + uuid, err := utils.GetPara(r, "uuid") + if err != nil { + utils.SendErrorResponse(w, "invalid uuid given") + return + } + + targetConfig, err := m.GetConfigByUUID(uuid) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + err = targetConfig.ValidateConfigs() + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +}
src/mod/tcpprox/nb.go.ref+289 −0 added@@ -0,0 +1,289 @@ +package tcpprox + +import ( + "fmt" + "io" + "log" + "net" + "os" + "regexp" + "strconv" + "strings" + "sync" + "time" +) + +const timeout = 5 + +func main() { + //log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile) + log.SetFlags(log.Ldate | log.Lmicroseconds) + + printWelcome() + + args := os.Args + argc := len(os.Args) + if argc <= 2 { + printHelp() + os.Exit(0) + } + + //TODO:support UDP protocol + + /*var logFileError error + if argc > 5 && args[4] == "-log" { + logPath := args[5] + "/" + time.Now().Format("2006_01_02_15_04_05") // "2006-01-02 15:04:05" + logPath += args[1] + "-" + strings.Replace(args[2], ":", "_", -1) + "-" + args[3] + ".log" + logPath = strings.Replace(logPath, `\`, "/", -1) + logPath = strings.Replace(logPath, "//", "/", -1) + logFile, logFileError = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE, 0666) + if logFileError != nil { + log.Fatalln("[x]", "log file path error.", logFileError.Error()) + } + log.Println("[√]", "open test log file success. path:", logPath) + }*/ + + switch args[1] { + case "-listen": + if argc < 3 { + log.Fatalln(`-listen need two arguments, like "nb -listen 1997 2017".`) + } + port1 := checkPort(args[2]) + port2 := checkPort(args[3]) + log.Println("[√]", "start to listen port:", port1, "and port:", port2) + port2port(port1, port2) + break + case "-tran": + if argc < 3 { + log.Fatalln(`-tran need two arguments, like "nb -tran 1997 192.168.1.2:3389".`) + } + port := checkPort(args[2]) + var remoteAddress string + if checkIp(args[3]) { + remoteAddress = args[3] + } + split := strings.SplitN(remoteAddress, ":", 2) + log.Println("[√]", "start to transmit address:", remoteAddress, "to address:", split[0]+":"+port) + port2host(port, remoteAddress) + break + case "-slave": + if argc < 3 { + log.Fatalln(`-slave need two arguments, like "nb -slave 127.0.0.1:3389 8.8.8.8:1997".`) + } + var address1, address2 string + checkIp(args[2]) + if checkIp(args[2]) { + address1 = args[2] + } + checkIp(args[3]) + if checkIp(args[3]) { + address2 = args[3] + } + log.Println("[√]", "start to connect address:", address1, "and address:", address2) + host2host(address1, address2) + break + default: + printHelp() + } +} + +func printWelcome() { + fmt.Println("+----------------------------------------------------------------+") + fmt.Println("| Welcome to use NATBypass Ver1.0.0 . |") + fmt.Println("| Code by cw1997 at 2017-10-19 03:59:51 |") + fmt.Println("| If you have some problem when you use the tool, |") + fmt.Println("| please submit issue at : https://github.com/cw1997/NATBypass . |") + fmt.Println("+----------------------------------------------------------------+") + fmt.Println() + // sleep one second because the fmt is not thread-safety. + // if not to do this, fmt.Print will print after the log.Print. + time.Sleep(time.Second) +} +func printHelp() { + fmt.Println(`usage: "-listen port1 port2" example: "nb -listen 1997 2017" `) + fmt.Println(` "-tran port1 ip:port2" example: "nb -tran 1997 192.168.1.2:3389" `) + fmt.Println(` "-slave ip1:port1 ip2:port2" example: "nb -slave 127.0.0.1:3389 8.8.8.8:1997" `) + fmt.Println(`============================================================`) + fmt.Println(`optional argument: "-log logpath" . example: "nb -listen 1997 2017 -log d:/nb" `) + fmt.Println(`log filename format: Y_m_d_H_i_s-agrs1-args2-args3.log`) + fmt.Println(`============================================================`) + fmt.Println(`if you want more help, please read "README.md". `) +} + +func checkPort(port string) string { + PortNum, err := strconv.Atoi(port) + if err != nil { + log.Fatalln("[x]", "port should be a number") + } + if PortNum < 1 || PortNum > 65535 { + log.Fatalln("[x]", "port should be a number and the range is [1,65536)") + } + return port +} + +func checkIp(address string) bool { + ipAndPort := strings.Split(address, ":") + if len(ipAndPort) != 2 { + log.Fatalln("[x]", "address error. should be a string like [ip:port]. ") + } + ip := ipAndPort[0] + port := ipAndPort[1] + checkPort(port) + pattern := `^(\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])$` + ok, err := regexp.MatchString(pattern, ip) + if err != nil || !ok { + log.Fatalln("[x]", "ip error. ") + } + return ok +} + +func port2port(port1 string, port2 string) { + listen1 := start_server("0.0.0.0:" + port1) + listen2 := start_server("0.0.0.0:" + port2) + log.Println("[√]", "listen port:", port1, "and", port2, "success. waiting for client...") + for { + conn1 := accept(listen1) + conn2 := accept(listen2) + if conn1 == nil || conn2 == nil { + log.Println("[x]", "accept client faild. retry in ", timeout, " seconds. ") + time.Sleep(timeout * time.Second) + continue + } + forward(conn1, conn2) + } +} + +func port2host(allowPort string, targetAddress string) { + server := start_server("0.0.0.0:" + allowPort) + for { + conn := accept(server) + if conn == nil { + continue + } + //println(targetAddress) + go func(targetAddress string) { + log.Println("[+]", "start connect host:["+targetAddress+"]") + target, err := net.Dial("tcp", targetAddress) + if err != nil { + // temporarily unavailable, don't use fatal. + log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", timeout, "seconds. ") + conn.Close() + log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]") + time.Sleep(timeout * time.Second) + return + } + log.Println("[→]", "connect target address ["+targetAddress+"] success.") + forward(target, conn) + }(targetAddress) + } +} + +func host2host(address1, address2 string) { + for { + log.Println("[+]", "try to connect host:["+address1+"] and ["+address2+"]") + var host1, host2 net.Conn + var err error + for { + host1, err = net.Dial("tcp", address1) + if err == nil { + log.Println("[→]", "connect ["+address1+"] success.") + break + } else { + log.Println("[x]", "connect target address ["+address1+"] faild. retry in ", timeout, " seconds. ") + time.Sleep(timeout * time.Second) + } + } + for { + host2, err = net.Dial("tcp", address2) + if err == nil { + log.Println("[→]", "connect ["+address2+"] success.") + break + } else { + log.Println("[x]", "connect target address ["+address2+"] faild. retry in ", timeout, " seconds. ") + time.Sleep(timeout * time.Second) + } + } + forward(host1, host2) + } +} + +func start_server(address string) net.Listener { + log.Println("[+]", "try to start server on:["+address+"]") + server, err := net.Listen("tcp", address) + if err != nil { + log.Fatalln("[x]", "listen address ["+address+"] faild.") + } + log.Println("[√]", "start listen at address:["+address+"]") + return server + /*defer server.Close() + + for { + conn, err := server.Accept() + log.Println("accept a new client. remote address:[" + conn.RemoteAddr().String() + + "], local address:[" + conn.LocalAddr().String() + "]") + if err != nil { + log.Println("accept a new client faild.", err.Error()) + continue + } + //go recvConnMsg(conn) + }*/ +} + +func accept(listener net.Listener) net.Conn { + conn, err := listener.Accept() + if err != nil { + log.Println("[x]", "accept connect ["+conn.RemoteAddr().String()+"] faild.", err.Error()) + return nil + } + log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]") + return conn +} + +func forward(conn1 net.Conn, conn2 net.Conn) { + log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String()) + var wg sync.WaitGroup + // wait tow goroutines + wg.Add(2) + go connCopy(conn1, conn2, &wg) + go connCopy(conn2, conn1, &wg) + //blocking when the wg is locked + wg.Wait() +} + +func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup) { + //TODO:log, record the data from conn1 and conn2. + logFile := openLog(conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String()) + if logFile != nil { + w := io.MultiWriter(conn1, logFile) + io.Copy(w, conn2) + } else { + io.Copy(conn1, conn2) + } + conn1.Close() + log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]") + //conn2.Close() + //log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]") + wg.Done() +} +func openLog(address1, address2, address3, address4 string) *os.File { + args := os.Args + argc := len(os.Args) + var logFileError error + var logFile *os.File + if argc > 5 && args[4] == "-log" { + address1 = strings.Replace(address1, ":", "_", -1) + address2 = strings.Replace(address2, ":", "_", -1) + address3 = strings.Replace(address3, ":", "_", -1) + address4 = strings.Replace(address4, ":", "_", -1) + timeStr := time.Now().Format("2006_01_02_15_04_05") // "2006-01-02 15:04:05" + logPath := args[5] + "/" + timeStr + args[1] + "-" + address1 + "_" + address2 + "-" + address3 + "_" + address4 + ".log" + logPath = strings.Replace(logPath, `\`, "/", -1) + logPath = strings.Replace(logPath, "//", "/", -1) + logFile, logFileError = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE, 0666) + if logFileError != nil { + log.Fatalln("[x]", "log file path error.", logFileError.Error()) + } + log.Println("[√]", "open test log file success. path:", logPath) + } + return logFile +}
src/mod/tcpprox/tcpprox.go+159 −0 added@@ -0,0 +1,159 @@ +package tcpprox + +import ( + "errors" + + uuid "github.com/satori/go.uuid" + "imuslab.com/zoraxy/mod/database" +) + +/* + TCP Proxy + + Forward port from one port to another + Also accept active connection and passive + connection +*/ + +const ( + ProxyMode_Listen = 0 + ProxyMode_Transport = 1 + ProxyMode_Starter = 2 +) + +type ProxyRelayOptions struct { + Name string + PortA string + PortB string + Timeout int + Mode int +} + +type ProxyRelayConfig struct { + UUID string //A UUIDv4 representing this config + Name string //Name of the config + Running bool //If the service is running + PortA string //Ports A (config depends on mode) + PortB string //Ports B (config depends on mode) + Mode int //Operation Mode + Timeout int //Timeout for connection in sec + stopChan chan bool //Stop channel to stop the listener + aTobAccumulatedByteTransfer int64 //Accumulated byte transfer from A to B + bToaAccumulatedByteTransfer int64 //Accumulated byte transfer from B to A +} + +type Options struct { + Database *database.Database + DefaultTimeout int +} + +type Manager struct { + //Config and stores + Options *Options + Configs []*ProxyRelayConfig + + //Realtime Statistics + Connections int //currently connected connect counts +} + +func NewTCProxy(options *Options) *Manager { + options.Database.NewTable("tcprox") + + previousRules := []*ProxyRelayConfig{} + if options.Database.KeyExists("tcprox", "rules") { + options.Database.Read("tcprox", "rules", &previousRules) + } + + return &Manager{ + Options: options, + Configs: previousRules, + Connections: 0, + } +} + +func (m *Manager) NewConfig(config *ProxyRelayOptions) string { + //Generate a new config from options + configUUID := uuid.NewV4().String() + thisConfig := ProxyRelayConfig{ + UUID: configUUID, + Name: config.Name, + Running: false, + PortA: config.PortA, + PortB: config.PortB, + Mode: config.Mode, + Timeout: config.Timeout, + stopChan: nil, + aTobAccumulatedByteTransfer: 0, + bToaAccumulatedByteTransfer: 0, + } + m.Configs = append(m.Configs, &thisConfig) + m.SaveConfigToDatabase() + return configUUID +} + +func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error) { + // Find and return the config with the specified UUID + for _, config := range m.Configs { + if config.UUID == configUUID { + return config, nil + } + } + return nil, errors.New("config not found") +} + +// Edit the config based on config UUID, leave empty for unchange fields +func (m *Manager) EditConfig(configUUID string, newName string, newPortA string, newPortB string, newMode int, newTimeout int) error { + // Find the config with the specified UUID + foundConfig, err := m.GetConfigByUUID(configUUID) + if err != nil { + return err + } + + // Validate and update the fields + if newName != "" { + foundConfig.Name = newName + } + if newPortA != "" { + foundConfig.PortA = newPortA + } + if newPortB != "" { + foundConfig.PortB = newPortB + } + if newMode != -1 { + if newMode > 2 || newMode < 0 { + return errors.New("invalid mode given") + } + foundConfig.Mode = newMode + } + if newTimeout != -1 { + if newTimeout < 0 { + return errors.New("invalid timeout value given") + } + foundConfig.Timeout = newTimeout + } + + err = foundConfig.ValidateConfigs() + if err != nil { + return err + } + + m.SaveConfigToDatabase() + + return nil +} + +func (m *Manager) RemoveConfig(configUUID string) error { + // Find and remove the config with the specified UUID + for i, config := range m.Configs { + if config.UUID == configUUID { + m.Configs = append(m.Configs[:i], m.Configs[i+1:]...) + m.SaveConfigToDatabase() + return nil + } + } + return errors.New("config not found") +} + +func (m *Manager) SaveConfigToDatabase() { + m.Options.Database.Write("tcprox", "rules", m.Configs) +}
src/mod/tcpprox/tcpprox_test.go+43 −0 added@@ -0,0 +1,43 @@ +package tcpprox_test + +import ( + "testing" + "time" + + "imuslab.com/zoraxy/mod/tcpprox" +) + +func TestPort2Port(t *testing.T) { + // Create a stopChan to control the loop + stopChan := make(chan bool) + + // Create a ProxyRelayConfig with dummy values + config := &tcpprox.ProxyRelayConfig{ + Timeout: 1, + } + + // Run port2port in a separate goroutine + t.Log("Starting go routine for proxy service") + go func() { + err := config.Port2host("8080", "124.244.86.40:8080", stopChan) + if err != nil { + t.Errorf("port2port returned an error: %v", err) + } + }() + + // Let the goroutine run for a while + time.Sleep(20 * time.Second) + + // Send a stop signal to stopChan + t.Log("Sending over stop signal") + stopChan <- true + + // Allow some time for the goroutine to exit + time.Sleep(1 * time.Second) + + // If the goroutine is still running, it means it did not stop as expected + if config.Running { + t.Errorf("port2port did not stop as expected") + } + +}
src/mod/tlscert/helper.go+60 −0 added@@ -0,0 +1,60 @@ +package tlscert + +import ( + "path/filepath" + "strings" +) + +//This remove the certificates in the list where either the +//public key or the private key is missing +func getCertPairs(certFiles []string) []string { + crtMap := make(map[string]bool) + keyMap := make(map[string]bool) + + for _, filename := range certFiles { + if filepath.Ext(filename) == ".crt" { + crtMap[strings.TrimSuffix(filename, ".crt")] = true + } else if filepath.Ext(filename) == ".key" { + keyMap[strings.TrimSuffix(filename, ".key")] = true + } + } + + var result []string + for domain := range crtMap { + if keyMap[domain] { + result = append(result, domain) + } + } + + return result +} + +//Get the cloest subdomain certificate from a list of domains +func matchClosestDomainCertificate(subdomain string, domains []string) string { + var matchingDomain string = "" + maxLength := 0 + + for _, domain := range domains { + if strings.HasSuffix(subdomain, "."+domain) && len(domain) > maxLength { + matchingDomain = domain + maxLength = len(domain) + } + } + + return matchingDomain +} + +//Check if a requesting domain is a subdomain of a given domain +func isSubdomain(subdomain, domain string) bool { + subdomainParts := strings.Split(subdomain, ".") + domainParts := strings.Split(domain, ".") + if len(subdomainParts) < len(domainParts) { + return false + } + for i := range domainParts { + if subdomainParts[len(subdomainParts)-1-i] != domainParts[len(domainParts)-1-i] { + return false + } + } + return true +}
src/mod/tlscert/localhost.crt+34 −0 added@@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIUavNWjB6rlfRLpeXJ9TXb2FVrENYwDQYJKoZIhvcNAQEL +BQAwbjELMAkGA1UEBhMCR0wxEjAQBgNVBAgMCU1pbGt5IFdheTEOMAwGA1UEBwwF +RWFydGgxEDAOBgNVBAoMB2ltdXNsYWIxDzANBgNVBAsMBkFyb3pPUzEYMBYGA1UE +AwwPd3d3LmltdXNsYWIuY29tMB4XDTIxMDkxNzA4NTkyNFoXDTQ5MDIwMTA4NTky +NFowbjELMAkGA1UEBhMCR0wxEjAQBgNVBAgMCU1pbGt5IFdheTEOMAwGA1UEBwwF +RWFydGgxEDAOBgNVBAoMB2ltdXNsYWIxDzANBgNVBAsMBkFyb3pPUzEYMBYGA1UE +AwwPd3d3LmltdXNsYWIuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAsBpf9ufRYOfdKft+51EibpqhA9yw6YstxL5BNselx3ETVnu7vYRIlH0ypgPN +nKguZ+BcN4mJFjQ36N4VpN7ySVfOCSCZz7lPvPfLib9iukBodBYQNAzMkKcLjyoY +gS8MD99cqe7s48k4JKp6b2WOmn2OtVZIS7AKZvVsRJNblhy7C3LkLnASKF0jb/ia +MGRAE+QV/zznvGg9FhNgQWWUil2Oesx3elj4KwlcHNX+c9pZz6yVgJrerj0s94OD +EuueiqAFOWsZrpp754ffC45PbeTNiflQ1B3aqkTtl5bL88ESgwMdtb1JGWN5HIS1 +Tq2d/3PgqbtvUEhggaFDbe0OxG2V33HqEfeG3BpZpYhCB3I7FPpRC/Tp8PACY13N +HYB9P5hRU/DnINhHjMCLKxHsolhiphWuxSuNIIojRL62zj7JwjnBgcghQzVFJ4O4 +TBfeMDadLII3ndDtsmR1dIba7fg+CWWdv4Zs0XGqHOaiHNclc7BhJF8SgiQxjxjm +Fh1ZsJm3LxPsw/iCl7ILE7+1aBQlBjEj0yBvMttkEDhRbILxXFPMALG/qakPvW9O +7WWClAc03ei/JFdq2camuY62/Tf1HB+TSpGWYH+cSIqsu3V5u29jmdZjrjnuM7Fz +GEjNSCsrMhSLYLkMJmrDGdFQBB31x24o9IXtyrfKZiwxMlUCAwEAAaOBhjCBgzAL +BgNVHQ8EBAMCBeAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwQAYDVR0RBDkwN4IBKoIQ +aW11c2xhYi5pbnRlcm5hbIISbm90LmZvci5wcm9kdWN0aW9uggxkZXYudXNlLm9u +bHkwHQYDVR0OBBYEFISIH/rn8RX1hcNf4rQajJR7FEdMMA0GCSqGSIb3DQEBCwUA +A4ICAQBVldF/qjWyGJ5TiZMiXly/So9zR3Xq7O1qayqYxb5SxvhZYCtsFVrAl6Ux +5bTZ0XQagjck2VouHOG6s98DpaslWFw9N8ADAmljQ8WL1hT5Ij1LXs2sF0FqttFf +YgoT5BOjnHZGlN+FgzAkdF91cYrfZwLm63jvAQtIHwjMSeymy2Fq8gdEZxagYuwG +gLkZxw1YG+gP778CKHT2Ff232kH+5up460aGLHLvg+xHQIWBt2FNGdv68u57hWxh +XXji4/DewQ0RdJW1JdpSg4npebDNiXpo9pKY/SxU056raOtPA94U/h12cHVkszT7 +IxdFC2PszAblbSZhHKGE0C6SbATsqvK4gz6e4h7HWVuPPNWpPW2BNjvyenpijV/E +YsSe6F7uQE/I/iHp9VMcjWuwItqed9yKDeOfDH4+pidowbSJQ97xYfZge36ZEUHC +2ZdQsR0qS+t2h0KlEDN7FNxai3ikSB1bs2AjtU67ofGtoIz/HD70TT6zHKhISZgI +w/4/SY7Hd+P+AWSdJwo+ycZYZlXajqh/cxVJ0zVBr5vKC9KnJ+IjnQ/q7CLcxM4W +aAFC1jakdPz7qO+xNVLQRf8lVnPJNtI88OrlL4n02JlLS/QUSwELXFW0bOKP33jm +PIbPdeP8k0XVe9wlI7MzUQC8pCt+gQ77awTt83Nxp9Xdn1Zbqw== +-----END CERTIFICATE-----
src/mod/tlscert/localhost.key+52 −0 added@@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCwGl/259Fg590p ++37nUSJumqED3LDpiy3EvkE2x6XHcRNWe7u9hEiUfTKmA82cqC5n4Fw3iYkWNDfo +3hWk3vJJV84JIJnPuU+898uJv2K6QGh0FhA0DMyQpwuPKhiBLwwP31yp7uzjyTgk +qnpvZY6afY61VkhLsApm9WxEk1uWHLsLcuQucBIoXSNv+JowZEAT5BX/POe8aD0W +E2BBZZSKXY56zHd6WPgrCVwc1f5z2lnPrJWAmt6uPSz3g4MS656KoAU5axmumnvn +h98Ljk9t5M2J+VDUHdqqRO2XlsvzwRKDAx21vUkZY3kchLVOrZ3/c+Cpu29QSGCB +oUNt7Q7EbZXfceoR94bcGlmliEIHcjsU+lEL9Onw8AJjXc0dgH0/mFFT8Ocg2EeM +wIsrEeyiWGKmFa7FK40giiNEvrbOPsnCOcGByCFDNUUng7hMF94wNp0sgjed0O2y +ZHV0htrt+D4JZZ2/hmzRcaoc5qIc1yVzsGEkXxKCJDGPGOYWHVmwmbcvE+zD+IKX +sgsTv7VoFCUGMSPTIG8y22QQOFFsgvFcU8wAsb+pqQ+9b07tZYKUBzTd6L8kV2rZ +xqa5jrb9N/UcH5NKkZZgf5xIiqy7dXm7b2OZ1mOuOe4zsXMYSM1IKysyFItguQwm +asMZ0VAEHfXHbij0he3Kt8pmLDEyVQIDAQABAoICAATmtwUILqujyGQCu+V0PKEX +bKPO4J2fYga3xNjhdZu3afJePztnEx4O3foA4RgbFi+N7wMcsNQNYAD7LV8JVXT1 +HKbkYWOGpNF9lAyhZv4IDOAuPQU11fuwqoGxij0OMie+77VLEQzF7OoYVJAFI5Lp +K6+gVyLEI4X6DqlZ8JKc+he3euJP/DFjZjkXkjMGl0H2dyZDa6+ytwCGSYeIbDnt +oKmKR0kAcOfBuu6ShiJzUUyWYRLTPJ9c1IOPBXbhV+hDy+FtOanCYvBut6Z6r3s/ +gvj0F2vP6OYURQiTCdoe5YT/8TO9sOsj+Zrxlpo5+svBTd9reA2j9gulkVrd3itN +c2Ee7fyuyrCRnEcKoT6BI8/LqH5eWGQKKS9WhOz26VkrorcYYZN3g4ayv+MiiSIm +jeo/kAWCqT5ylvlw2gaCbPjB4kbx7yMI/myjgF0R4+aNQaHpXa2qqEORitGx40M7 +T1V2JIxnsa83TBwumunkYC2pX7bNS0a1VuCNxUafJRKEcvKhWmiRHaWddZn46G8N +E56qFzSaLbkd+J71jso9llK5joGIQTt2pbKUdV9LIm5Nsbtp2VgF9URIw5RZFftx +PfSm9XM9DtWuxheO4gNwAuOvtaOxztNMvSkQzhTOggSRpt15hFd7CeBrpK43feAH +b2pMequB8MHpUieyxlwBAoIBAQC5IRbaKx+fSEbYeIySUwbN8GCJQl+wmvc1gqCC +DflEQqxTvCGBB5SLHHurTT0ubhXkvbrtuS5f4IC+htzKSuwlqn3lS0aOXtFP2tT6 +D9iiMxLxIId5l6dD+PjMWtQcWc8wUQ7+ieRgxybDqiCWMyTbvNgwlkcIbRxmcqyN +4/LmmgzTnr5CH0DC/J7xpUJuX9LPVb4ZvBYjz5X++Yb7pCa+kXp0Z6yU48bG3sRe +yiUKp3Z4vDoOkMLHTPvTQLG81rQuJnBUw2uLWM0kg1AwteZcQ/gH1ilVbJzMBnKm +mtuJWtoPnM2zIhCsURngmBN+qxOb5kchMSvPzAQBCw7HBjWpAoIBAQDzhLQO434G +XhyDcdkdMRbDZ8Q8PqtOloAbczMuPGgwHV7rVe/BvnJS7HDDebwlJBD8nhGvgBrp +CsjNGHjSQC7ydUa8dP4Aw/46izdR8DsAwqGZq+tZhkY5CS88QpflUT5rftW0RObn +Cb/gDzdxHy35/scSICxa2HwcZnqXqfEwnbjkxFwBYFSt6hRiwNhDhd6ZxKa6gt56 +DS9uIxt1IhKgXZfIw1Vo0mHHFLsB7czGZ0O24ya31Es0bUWGgWIcxvKw6MqKhFWw +ncCakVg278UYUm/zt6Dcrn3XYnK7Pr944AiKO21PMQhG7Rb+OVwxgjMhk7/BCt+k +sPR1Dct5pqrNAoIBAAl2jYp9ZdJoiWaLUvQv1ks0nFqnz+hhI33SvY2oVTOODO0C +0tubnZY20IODITt8WRYmNKXuL1arTSlwD10v0z5hpqnP3T1tz1k7oGNf5/zyi2dT ++FjYza4FzgH0Kp+AX7zih9evCMOBqpOZ4KyM1Ld+wbZKGDtwCGGcPwHJwyLSgRFY +LfWHT3IoI5/KiMjHkSkUAvGh0afm9o3gB2xZibl4CkBlBEdgFUsZHASUZKxUvxOQ +247fC3XQk5bK2csDVpZ9VISgsKCg22ugYrr6sVnKB6Wu5tH9CU7MjZPCmrI8uKTP +qRwdA6krRB1c6LIy4H+5l600rD6k+Rdsj0bRJHECggEAeBXSrRzmAsHaEb/MryaL +8SR0krjcxU5WMjMm5AAJ6OAy9J5WMxZ1TgsmuF6Jt08HyWsxkXf8zTryNqGAwz2/ +aPUIQtr2fu4nqjsItrFeh0tzYVJ0JpueeXXcAz1bpkvgGiZbwB/SNdCK/DTExFX5 +2DQZewi+lrX2zhKDFdNKCw1cJgPm0w7r8y9hiilK/FFBqlZdWdA7Ybiq0Qci/Som +QUqmFOyua5iDeybv6U2ZE6XMsJ1ndHON+naAOIoJFePNvguuBYyorQW9+vr9o2mt +qgbNCkRdYTXy/ImhxlB1H2hrDa+sgcbOLBuyoP8sRYXNLRutDccM7iwNAMQiuQTF +aQKCAQEAiKPwUodT6LNu4lrSbsDAYIqWwlfM0wwUhudT5UTVHSYI3ap0QOiEuzOl +IJVdx+vx7rQW7l+JIL6s4shA7mzpzuTVlhRuDuGZx0qQLP7INVpCLzIEbYGI2dL7 +WLhJd4eYKltJ+BG7S51tq9/6rVcUDn5DKzyGNyeGhOnaYkk+eTm483+vpOP2/ITi +cbVv3mx4qE7zMPIxIufm+c8RonadJzYiq1uMk8t0TrcW/B9RTly/Y96kamjyU5b0 +OcLdRcx3ppKAxHD9AvwAR6SiuNLfNjM9KZM40zM5goMrCJJzwgb7UGeMuw2z7L9F ++iSj2pW0Rbdy7oOcFRF/iM2GwFYc1Q== +-----END PRIVATE KEY-----
src/mod/tlscert/tlscert.go+187 −0 added@@ -0,0 +1,187 @@ +package tlscert + +import ( + "crypto/tls" + "crypto/x509" + "embed" + "encoding/pem" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + "imuslab.com/zoraxy/mod/utils" +) + +type Manager struct { + CertStore string + verbal bool +} + +//go:embed localhost.crt localhost.key +var buildinCertStore embed.FS + +func NewManager(certStore string, verbal bool) (*Manager, error) { + if !utils.FileExists(certStore) { + os.MkdirAll(certStore, 0775) + } + + thisManager := Manager{ + CertStore: certStore, + verbal: verbal, + } + + return &thisManager, nil +} + +func (m *Manager) ListCertDomains() ([]string, error) { + filenames, err := m.ListCerts() + if err != nil { + return []string{}, err + } + + //Remove certificates where there are missing public key or private key + filenames = getCertPairs(filenames) + + return filenames, nil +} + +func (m *Manager) ListCerts() ([]string, error) { + certs, err := ioutil.ReadDir(m.CertStore) + if err != nil { + return []string{}, err + } + + filenames := make([]string, 0, len(certs)) + for _, cert := range certs { + if !cert.IsDir() { + filenames = append(filenames, cert.Name()) + } + } + + return filenames, nil +} + +func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) { + //Check if the domain corrisponding cert exists + pubKey := "./tmp/localhost.crt" + priKey := "./tmp/localhost.key" + + //Check if this is initial setup + if !utils.FileExists(pubKey) { + buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey)) + os.WriteFile(pubKey, buildInPubKey, 0775) + } + + if !utils.FileExists(priKey) { + buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey)) + os.WriteFile(priKey, buildInPriKey, 0775) + } + + if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".crt")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) { + pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".crt") + priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key") + + } else { + domainCerts, _ := m.ListCertDomains() + cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts) + if cloestDomainCert != "" { + //There is a matching parent domain for this subdomain. Use this instead. + pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt") + priKey = filepath.Join(m.CertStore, cloestDomainCert+".key") + } else if m.DefaultCertExists() { + //Use default.crt and default.key + pubKey = filepath.Join(m.CertStore, "default.crt") + priKey = filepath.Join(m.CertStore, "default.key") + if m.verbal { + log.Println("No matching certificate found. Serving with default") + } + } else { + if m.verbal { + log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName) + } + } + } + + //Load the cert and serve it + cer, err := tls.LoadX509KeyPair(pubKey, priKey) + if err != nil { + log.Println(err) + return nil, nil + } + + return &cer, nil +} + +// Check if both the default cert public key and private key exists +func (m *Manager) DefaultCertExists() bool { + return utils.FileExists(filepath.Join(m.CertStore, "default.crt")) && utils.FileExists(filepath.Join(m.CertStore, "default.key")) +} + +// Check if the default cert exists returning seperate results for pubkey and prikey +func (m *Manager) DefaultCertExistsSep() (bool, bool) { + return utils.FileExists(filepath.Join(m.CertStore, "default.crt")), utils.FileExists(filepath.Join(m.CertStore, "default.key")) +} + +// Delete the cert if exists +func (m *Manager) RemoveCert(domain string) error { + pubKey := filepath.Join(m.CertStore, domain+".crt") + priKey := filepath.Join(m.CertStore, domain+".key") + if utils.FileExists(pubKey) { + err := os.Remove(pubKey) + if err != nil { + return err + } + } + + if utils.FileExists(priKey) { + err := os.Remove(priKey) + if err != nil { + return err + } + } + + return nil +} + +// Check if the given file is a valid TLS file +func IsValidTLSFile(file io.Reader) bool { + // Read the contents of the uploaded file + contents, err := io.ReadAll(file) + if err != nil { + // Handle the error + return false + } + + // Parse the contents of the file as a PEM-encoded certificate or key + block, _ := pem.Decode(contents) + if block == nil { + // The file is not a valid PEM-encoded certificate or key + return false + } + + // Parse the certificate or key + if strings.Contains(block.Type, "CERTIFICATE") { + // The file contains a certificate + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + // Handle the error + return false + } + // Check if the certificate is a valid TLS/SSL certificate + return cert.IsCA == false && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 + } else if strings.Contains(block.Type, "PRIVATE KEY") { + // The file contains a private key + _, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + // Handle the error + return false + } + return true + } else { + return false + } + +}
src/mod/uptime/uptime.go+228 −0 added@@ -0,0 +1,228 @@ +package uptime + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "imuslab.com/zoraxy/mod/utils" +) + +type Record struct { + Timestamp int64 + ID string + Name string + URL string + Protocol string + Online bool + StatusCode int + Latency int64 +} + +type Target struct { + ID string + Name string + URL string + Protocol string +} + +type Config struct { + Targets []*Target + Interval int + MaxRecordsStore int +} + +type Monitor struct { + Config *Config + OnlineStatusLog map[string][]*Record +} + +// Default configs +var exampleTarget = Target{ + ID: "example", + Name: "Example", + URL: "example.com", + Protocol: "https", +} + +// Create a new uptime monitor +func NewUptimeMonitor(config *Config) (*Monitor, error) { + //Create new monitor object + thisMonitor := Monitor{ + Config: config, + OnlineStatusLog: map[string][]*Record{}, + } + //Start the endpoint listener + ticker := time.NewTicker(time.Duration(config.Interval) * time.Second) + done := make(chan bool) + + //Start the uptime check once first before entering loop + thisMonitor.ExecuteUptimeCheck() + + go func() { + for { + select { + case <-done: + return + case t := <-ticker.C: + log.Println("Uptime updated - ", t.Unix()) + thisMonitor.ExecuteUptimeCheck() + } + } + }() + + return &thisMonitor, nil +} + +func (m *Monitor) ExecuteUptimeCheck() { + for _, target := range m.Config.Targets { + //For each target to check online, do the following + var thisRecord Record + if target.Protocol == "http" || target.Protocol == "https" { + online, laterncy, statusCode := getWebsiteStatusWithLatency(target.URL) + thisRecord = Record{ + Timestamp: time.Now().Unix(), + ID: target.ID, + Name: target.Name, + URL: target.URL, + Protocol: target.Protocol, + Online: online, + StatusCode: statusCode, + Latency: laterncy, + } + + //fmt.Println(thisRecord) + + } else { + log.Println("Unknown protocol: " + target.Protocol + ". Skipping") + continue + } + + thisRecords, ok := m.OnlineStatusLog[target.ID] + if !ok { + //First record. Create the array + m.OnlineStatusLog[target.ID] = []*Record{&thisRecord} + } else { + //Append to the previous record + thisRecords = append(thisRecords, &thisRecord) + + //Check if the record is longer than the logged record. If yes, clear out the old records + if len(thisRecords) > m.Config.MaxRecordsStore { + thisRecords = thisRecords[1:] + } + + m.OnlineStatusLog[target.ID] = thisRecords + } + } + + //TODO: Write results to db +} + +func (m *Monitor) AddTargetToMonitor(target *Target) { + // Add target to Config + m.Config.Targets = append(m.Config.Targets, target) + + // Add target to OnlineStatusLog + m.OnlineStatusLog[target.ID] = []*Record{} +} + +func (m *Monitor) RemoveTargetFromMonitor(targetId string) { + // Remove target from Config + for i, target := range m.Config.Targets { + if target.ID == targetId { + m.Config.Targets = append(m.Config.Targets[:i], m.Config.Targets[i+1:]...) + break + } + } + + // Remove target from OnlineStatusLog + delete(m.OnlineStatusLog, targetId) +} + +// Scan the config target. If a target exists in m.OnlineStatusLog no longer +// exists in m.Monitor.Config.Targets, it remove it from the log as well. +func (m *Monitor) CleanRecords() { + // Create a set of IDs for all targets in the config + targetIDs := make(map[string]bool) + for _, target := range m.Config.Targets { + targetIDs[target.ID] = true + } + + // Iterate over all log entries and remove any that have a target ID that + // is not in the set of current target IDs + newStatusLog := m.OnlineStatusLog + for id, _ := range m.OnlineStatusLog { + _, idExistsInTargets := targetIDs[id] + if !idExistsInTargets { + delete(newStatusLog, id) + } + } + + m.OnlineStatusLog = newStatusLog +} + +/* + Web Interface Handler +*/ + +func (m *Monitor) HandleUptimeLogRead(w http.ResponseWriter, r *http.Request) { + id, _ := utils.GetPara(r, "id") + if id == "" { + js, _ := json.Marshal(m.OnlineStatusLog) + w.Header().Set("Content-Type", "application/json") + w.Write(js) + } else { + //Check if that id exists + log, ok := m.OnlineStatusLog[id] + if !ok { + http.NotFound(w, r) + return + } + + js, _ := json.MarshalIndent(log, "", " ") + w.Header().Set("Content-Type", "application/json") + w.Write(js) + } + +} + +/* + Utilities +*/ + +// Get website stauts with latency given URL, return is conn succ and its latency and status code +func getWebsiteStatusWithLatency(url string) (bool, int64, int) { + start := time.Now().UnixNano() / int64(time.Millisecond) + statusCode, err := getWebsiteStatus(url) + end := time.Now().UnixNano() / int64(time.Millisecond) + if err != nil { + log.Println(err.Error()) + return false, 0, 0 + } else { + diff := end - start + succ := false + if statusCode >= 200 && statusCode < 300 { + //OK + succ = true + } else if statusCode >= 300 && statusCode < 400 { + //Redirection code + succ = true + } else { + succ = false + } + + return succ, diff, statusCode + } + +} + +func getWebsiteStatus(url string) (int, error) { + resp, err := http.Get(url) + if err != nil { + return 0, err + } + status_code := resp.StatusCode + resp.Body.Close() + return status_code, nil +}
src/mod/utils/conv.go+16 −0 added@@ -0,0 +1,16 @@ +package utils + +import "strconv" + +func StringToInt64(number string) (int64, error) { + i, err := strconv.ParseInt(number, 10, 64) + if err != nil { + return -1, err + } + return i, nil +} + +func Int64ToString(number int64) string { + convedNumber := strconv.FormatInt(number, 10) + return convedNumber +}
src/mod/utils/template.go+19 −0 added@@ -0,0 +1,19 @@ +package utils + +import ( + "net/http" +) + +/* + Web Template Generator + + This is the main system core module that perform function similar to what PHP did. + To replace part of the content of any file, use {{paramter}} to replace it. + + +*/ + +func SendHTMLResponse(w http.ResponseWriter, msg string) { + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(msg)) +}
src/mod/utils/utils.go+175 −0 added@@ -0,0 +1,175 @@ +package utils + +import ( + "bufio" + "encoding/base64" + "errors" + "io" + "log" + "net/http" + "os" + "strings" + "time" +) + +/* + Common + + Some commonly used functions in ArozOS + +*/ + +// Response related +func SendTextResponse(w http.ResponseWriter, msg string) { + w.Write([]byte(msg)) +} + +// Send JSON response, with an extra json header +func SendJSONResponse(w http.ResponseWriter, json string) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(json)) +} + +func SendErrorResponse(w http.ResponseWriter, errMsg string) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{\"error\":\"" + errMsg + "\"}")) +} + +func SendOK(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("\"OK\"")) +} + +/* + The paramter move function (mv) + + You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in + r (HTTP Request Object) + getParamter (string, aka $_GET['This string]) + + Will return + Paramter string (if any) + Error (if error) + +*/ +/* +func Mv(r *http.Request, getParamter string, postMode bool) (string, error) { + if postMode == false { + //Access the paramter via GET + keys, ok := r.URL.Query()[getParamter] + + if !ok || len(keys[0]) < 1 { + //log.Println("Url Param " + getParamter +" is missing") + return "", errors.New("GET paramter " + getParamter + " not found or it is empty") + } + + // Query()["key"] will return an array of items, + // we only want the single item. + key := keys[0] + return string(key), nil + } else { + //Access the parameter via POST + r.ParseForm() + x := r.Form.Get(getParamter) + if len(x) == 0 || x == "" { + return "", errors.New("POST paramter " + getParamter + " not found or it is empty") + } + return string(x), nil + } + +} +*/ + +// Get GET parameter +func GetPara(r *http.Request, key string) (string, error) { + keys, ok := r.URL.Query()[key] + if !ok || len(keys[0]) < 1 { + return "", errors.New("invalid " + key + " given") + } else { + return keys[0], nil + } +} + +// Get POST paramter +func PostPara(r *http.Request, key string) (string, error) { + r.ParseForm() + x := r.Form.Get(key) + if x == "" { + return "", errors.New("invalid " + key + " given") + } else { + return x, nil + } +} + +func FileExists(filename string) bool { + _, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return true +} + +func IsDir(path string) bool { + if FileExists(path) == false { + return false + } + fi, err := os.Stat(path) + if err != nil { + log.Fatal(err) + return false + } + switch mode := fi.Mode(); { + case mode.IsDir(): + return true + case mode.IsRegular(): + return false + } + return false +} + +func TimeToString(targetTime time.Time) string { + return targetTime.Format("2006-01-02 15:04:05") +} + +func LoadImageAsBase64(filepath string) (string, error) { + if !FileExists(filepath) { + return "", errors.New("File not exists") + } + f, _ := os.Open(filepath) + reader := bufio.NewReader(f) + content, _ := io.ReadAll(reader) + encoded := base64.StdEncoding.EncodeToString(content) + return string(encoded), nil +} + +// Use for redirections +func ConstructRelativePathFromRequestURL(requestURI string, redirectionLocation string) string { + if strings.Count(requestURI, "/") == 1 { + //Already root level + return redirectionLocation + } + for i := 0; i < strings.Count(requestURI, "/")-1; i++ { + redirectionLocation = "../" + redirectionLocation + } + + return redirectionLocation +} + +// Check if given string in a given slice +func StringInArray(arr []string, str string) bool { + for _, a := range arr { + if a == str { + return true + } + } + return false +} + +func StringInArrayIgnoreCase(arr []string, str string) bool { + smallArray := []string{} + for _, item := range arr { + smallArray = append(smallArray, strings.ToLower(item)) + } + + return StringInArray(smallArray, strings.ToLower(str)) +}
src/mod/wakeonlan/wakeonlan.go+68 −0 added@@ -0,0 +1,68 @@ +package wakeonlan + +import ( + "errors" + "net" + "time" +) + +/* + Wake On Lan + Author: tobychui + + This module send wake on LAN signal to a given MAC address + and do nothing else +*/ + +type magicPacket [102]byte + +func WakeTarget(macAddr string) error { + packet := magicPacket{} + mac, err := net.ParseMAC(macAddr) + if err != nil { + return err + } + + if len(mac) != 6 { + return errors.New("invalid MAC address") + } + + //Initialize the packet with all F + copy(packet[0:], []byte{255, 255, 255, 255, 255, 255}) + offset := 6 + + for i := 0; i < 16; i++ { + copy(packet[offset:], mac) + offset += 6 + } + + //Most devices listen to either port 7 or 9, send to both of them + err = sendPacket("255.255.255.255:7", packet) + if err != nil { + return err + } + + time.Sleep(30 * time.Millisecond) + + err = sendPacket("255.255.255.255:9", packet) + if err != nil { + return err + } + return nil +} + +func sendPacket(addr string, packet magicPacket) error { + conn, err := net.Dial("udp", addr) + if err != nil { + return err + } + defer conn.Close() + + _, err = conn.Write(packet[:]) + return err +} + +func IsValidMacAddress(macaddr string) bool { + _, err := net.ParseMAC(macaddr) + return err == nil +}
src/mod/websocketproxy/LICENSE.md+20 −0 added@@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Koding, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
src/mod/websocketproxy/README.md+54 −0 added@@ -0,0 +1,54 @@ +# WebsocketProxy [](https://godoc.org/github.com/koding/websocketproxy) [](https://travis-ci.org/koding/websocketproxy) + +WebsocketProxy is an http.Handler interface build on top of +[gorilla/websocket](https://github.com/gorilla/websocket) that you can plug +into your existing Go webserver to provide WebSocket reverse proxy. + +## Install + +```bash +go get github.com/koding/websocketproxy +``` + +## Example + +Below is a simple server that proxies to the given backend URL + +```go +package main + +import ( + "flag" + "net/http" + "net/url" + + "github.com/koding/websocketproxy" +) + +var ( + flagBackend = flag.String("backend", "", "Backend URL for proxying") +) + +func main() { + u, err := url.Parse(*flagBackend) + if err != nil { + log.Fatalln(err) + } + + err = http.ListenAndServe(":80", websocketproxy.NewProxy(u)) + if err != nil { + log.Fatalln(err) + } +} +``` + +Save it as `proxy.go` and run as: + +```bash +go run proxy.go -backend ws://example.com:3000 +``` + +Now all incoming WebSocket requests coming to this server will be proxied to +`ws://example.com:3000` + +
src/mod/websocketproxy/websocketproxy.go+239 −0 added@@ -0,0 +1,239 @@ +// Package websocketproxy is a reverse proxy for WebSocket connections. +package websocketproxy + +import ( + "fmt" + "io" + "log" + "net" + "net/http" + "net/url" + "strings" + + "github.com/gorilla/websocket" +) + +var ( + // DefaultUpgrader specifies the parameters for upgrading an HTTP + // connection to a WebSocket connection. + DefaultUpgrader = &websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } + + // DefaultDialer is a dialer with all fields set to the default zero values. + DefaultDialer = websocket.DefaultDialer +) + +// WebsocketProxy is an HTTP Handler that takes an incoming WebSocket +// connection and proxies it to another server. +type WebsocketProxy struct { + // Director, if non-nil, is a function that may copy additional request + // headers from the incoming WebSocket connection into the output headers + // which will be forwarded to another server. + Director func(incoming *http.Request, out http.Header) + + // Backend returns the backend URL which the proxy uses to reverse proxy + // the incoming WebSocket connection. Request is the initial incoming and + // unmodified request. + Backend func(*http.Request) *url.URL + + // Upgrader specifies the parameters for upgrading a incoming HTTP + // connection to a WebSocket connection. If nil, DefaultUpgrader is used. + Upgrader *websocket.Upgrader + + // Dialer contains options for connecting to the backend WebSocket server. + // If nil, DefaultDialer is used. + Dialer *websocket.Dialer + + Verbal bool +} + +// ProxyHandler returns a new http.Handler interface that reverse proxies the +// request to the given target. +func ProxyHandler(target *url.URL) http.Handler { return NewProxy(target) } + +// NewProxy returns a new Websocket reverse proxy that rewrites the +// URL's to the scheme, host and base path provider in target. +func NewProxy(target *url.URL) *WebsocketProxy { + backend := func(r *http.Request) *url.URL { + // Shallow copy + u := *target + u.Fragment = r.URL.Fragment + u.Path = r.URL.Path + u.RawQuery = r.URL.RawQuery + return &u + } + return &WebsocketProxy{Backend: backend, Verbal: false} +} + +// ServeHTTP implements the http.Handler that proxies WebSocket connections. +func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if w.Backend == nil { + log.Println("websocketproxy: backend function is not defined") + http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError) + return + } + + backendURL := w.Backend(req) + if backendURL == nil { + log.Println("websocketproxy: backend URL is nil") + http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError) + return + } + + dialer := w.Dialer + if w.Dialer == nil { + dialer = DefaultDialer + } + + // Pass headers from the incoming request to the dialer to forward them to + // the final destinations. + requestHeader := http.Header{} + if origin := req.Header.Get("Origin"); origin != "" { + requestHeader.Add("Origin", origin) + } + for _, prot := range req.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] { + requestHeader.Add("Sec-WebSocket-Protocol", prot) + } + for _, cookie := range req.Header[http.CanonicalHeaderKey("Cookie")] { + requestHeader.Add("Cookie", cookie) + } + if req.Host != "" { + requestHeader.Set("Host", req.Host) + } + + // Pass X-Forwarded-For headers too, code below is a part of + // httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For + // for more information + // TODO: use RFC7239 http://tools.ietf.org/html/rfc7239 + if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { + // If we aren't the first proxy retain prior + // X-Forwarded-For information as a comma+space + // separated list and fold multiple headers into one. + if prior, ok := req.Header["X-Forwarded-For"]; ok { + clientIP = strings.Join(prior, ", ") + ", " + clientIP + } + requestHeader.Set("X-Forwarded-For", clientIP) + } + + // Set the originating protocol of the incoming HTTP request. The SSL might + // be terminated on our site and because we doing proxy adding this would + // be helpful for applications on the backend. + requestHeader.Set("X-Forwarded-Proto", "http") + if req.TLS != nil { + requestHeader.Set("X-Forwarded-Proto", "https") + } + + // Enable the director to copy any additional headers it desires for + // forwarding to the remote server. + if w.Director != nil { + w.Director(req, requestHeader) + } + + // Connect to the backend URL, also pass the headers we get from the requst + // together with the Forwarded headers we prepared above. + // TODO: support multiplexing on the same backend connection instead of + // opening a new TCP connection time for each request. This should be + // optional: + // http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01 + connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader) + if err != nil { + log.Printf("websocketproxy: couldn't dial to remote backend url %s", err) + if resp != nil { + // If the WebSocket handshake fails, ErrBadHandshake is returned + // along with a non-nil *http.Response so that callers can handle + // redirects, authentication, etcetera. + if err := copyResponse(rw, resp); err != nil { + log.Printf("websocketproxy: couldn't write response after failed remote backend handshake: %s", err) + } + } else { + http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) + } + return + } + defer connBackend.Close() + + upgrader := w.Upgrader + if w.Upgrader == nil { + upgrader = DefaultUpgrader + } + + // Only pass those headers to the upgrader. + upgradeHeader := http.Header{} + if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" { + upgradeHeader.Set("Sec-Websocket-Protocol", hdr) + } + if hdr := resp.Header.Get("Set-Cookie"); hdr != "" { + upgradeHeader.Set("Set-Cookie", hdr) + } + + // Now upgrade the existing incoming request to a WebSocket connection. + // Also pass the header that we gathered from the Dial handshake. + connPub, err := upgrader.Upgrade(rw, req, upgradeHeader) + if err != nil { + log.Printf("websocketproxy: couldn't upgrade %s", err) + return + } + defer connPub.Close() + + errClient := make(chan error, 1) + errBackend := make(chan error, 1) + replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { + for { + msgType, msg, err := src.ReadMessage() + if err != nil { + m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) + if e, ok := err.(*websocket.CloseError); ok { + if e.Code != websocket.CloseNoStatusReceived { + m = websocket.FormatCloseMessage(e.Code, e.Text) + } + } + errc <- err + dst.WriteMessage(websocket.CloseMessage, m) + break + } + err = dst.WriteMessage(msgType, msg) + if err != nil { + errc <- err + break + } + } + } + + go replicateWebsocketConn(connPub, connBackend, errClient) + go replicateWebsocketConn(connBackend, connPub, errBackend) + + var message string + select { + case err = <-errClient: + message = "websocketproxy: Error when copying from backend to client: %v" + case err = <-errBackend: + message = "websocketproxy: Error when copying from client to backend: %v" + + } + if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure { + if w.Verbal { + //Only print message on verbal mode + log.Printf(message, err) + } + + } +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +func copyResponse(rw http.ResponseWriter, resp *http.Response) error { + copyHeader(rw.Header(), resp.Header) + rw.WriteHeader(resp.StatusCode) + defer resp.Body.Close() + + _, err := io.Copy(rw, resp.Body) + return err +}
src/mod/websocketproxy/websocketproxy_test.go+130 −0 added@@ -0,0 +1,130 @@ +package websocketproxy + +import ( + "log" + "net/http" + "net/url" + "testing" + "time" + + "github.com/gorilla/websocket" +) + +var ( + serverURL = "ws://127.0.0.1:7777" + backendURL = "ws://127.0.0.1:8888" +) + +func TestProxy(t *testing.T) { + // websocket proxy + supportedSubProtocols := []string{"test-protocol"} + upgrader := &websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, + CheckOrigin: func(r *http.Request) bool { + return true + }, + Subprotocols: supportedSubProtocols, + } + + u, _ := url.Parse(backendURL) + proxy := NewProxy(u) + proxy.Upgrader = upgrader + + mux := http.NewServeMux() + mux.Handle("/proxy", proxy) + go func() { + if err := http.ListenAndServe(":7777", mux); err != nil { + t.Fatal("ListenAndServe: ", err) + } + }() + + time.Sleep(time.Millisecond * 100) + + // backend echo server + go func() { + mux2 := http.NewServeMux() + mux2.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Don't upgrade if original host header isn't preserved + if r.Host != "127.0.0.1:7777" { + log.Printf("Host header set incorrectly. Expecting 127.0.0.1:7777 got %s", r.Host) + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + + messageType, p, err := conn.ReadMessage() + if err != nil { + return + } + + if err = conn.WriteMessage(messageType, p); err != nil { + return + } + }) + + err := http.ListenAndServe(":8888", mux2) + if err != nil { + t.Fatal("ListenAndServe: ", err) + } + }() + + time.Sleep(time.Millisecond * 100) + + // let's us define two subprotocols, only one is supported by the server + clientSubProtocols := []string{"test-protocol", "test-notsupported"} + h := http.Header{} + for _, subprot := range clientSubProtocols { + h.Add("Sec-WebSocket-Protocol", subprot) + } + + // frontend server, dial now our proxy, which will reverse proxy our + // message to the backend websocket server. + conn, resp, err := websocket.DefaultDialer.Dial(serverURL+"/proxy", h) + if err != nil { + t.Fatal(err) + } + + // check if the server really accepted only the first one + in := func(desired string) bool { + for _, prot := range resp.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] { + if desired == prot { + return true + } + } + return false + } + + if !in("test-protocol") { + t.Error("test-protocol should be available") + } + + if in("test-notsupported") { + t.Error("test-notsupported should be not recevied from the server.") + } + + // now write a message and send it to the backend server (which goes trough + // proxy..) + msg := "hello kite" + err = conn.WriteMessage(websocket.TextMessage, []byte(msg)) + if err != nil { + t.Error(err) + } + + messageType, p, err := conn.ReadMessage() + if err != nil { + t.Error(err) + } + + if messageType != websocket.TextMessage { + t.Error("incoming message type is not Text") + } + + if msg != string(p) { + t.Errorf("expecting: %s, got: %s", msg, string(p)) + } +}
src/redirect.go+75 −0 added@@ -0,0 +1,75 @@ +package main + +import ( + "encoding/json" + "net/http" + "strconv" + + "imuslab.com/zoraxy/mod/utils" +) + +/* + Redirect.go + + This script handle all the http handlers + related to redirection function in the reverse proxy +*/ + +func handleListRedirectionRules(w http.ResponseWriter, r *http.Request) { + rules := redirectTable.GetAllRedirectRules() + js, _ := json.Marshal(rules) + utils.SendJSONResponse(w, string(js)) +} + +func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) { + redirectUrl, err := utils.PostPara(r, "redirectUrl") + if err != nil { + utils.SendErrorResponse(w, "redirect url cannot be empty") + return + } + destUrl, err := utils.PostPara(r, "destUrl") + if err != nil { + utils.SendErrorResponse(w, "destination url cannot be empty") + } + + forwardChildpath, err := utils.PostPara(r, "forwardChildpath") + if err != nil { + //Assume true + forwardChildpath = "true" + } + + redirectTypeString, err := utils.PostPara(r, "redirectType") + if err != nil { + redirectTypeString = "307" + } + + redirectionStatusCode, err := strconv.Atoi(redirectTypeString) + if err != nil { + utils.SendErrorResponse(w, "invalid status code number") + return + } + + err = redirectTable.AddRedirectRule(redirectUrl, destUrl, forwardChildpath == "true", redirectionStatusCode) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +} + +func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) { + redirectUrl, err := utils.PostPara(r, "redirectUrl") + if err != nil { + utils.SendErrorResponse(w, "redirect url cannot be empty") + return + } + + err = redirectTable.DeleteRedirectRule(redirectUrl) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) +}
src/reverseproxy.go+350 −0 added@@ -0,0 +1,350 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "imuslab.com/zoraxy/mod/dynamicproxy" + "imuslab.com/zoraxy/mod/uptime" + "imuslab.com/zoraxy/mod/utils" +) + +var ( + dynamicProxyRouter *dynamicproxy.Router +) + +// Add user customizable reverse proxy +func ReverseProxtInit() { + inboundPort := 80 + if sysdb.KeyExists("settings", "inbound") { + sysdb.Read("settings", "inbound", &inboundPort) + log.Println("Serving inbound port ", inboundPort) + } else { + log.Println("Inbound port not set. Using default (80)") + } + + useTls := false + sysdb.Read("settings", "usetls", &useTls) + if useTls { + log.Println("TLS mode enabled. Serving proxxy request with TLS") + } else { + log.Println("TLS mode disabled. Serving proxy request with plain http") + } + + forceHttpsRedirect := false + sysdb.Read("settings", "redirect", &forceHttpsRedirect) + if forceHttpsRedirect { + log.Println("Force HTTPS mode enabled") + } else { + log.Println("Force HTTPS mode disabled") + } + + dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{ + Port: inboundPort, + UseTls: useTls, + ForceHttpsRedirect: forceHttpsRedirect, + TlsManager: tlsCertManager, + RedirectRuleTable: redirectTable, + GeodbStore: geodbStore, + StatisticCollector: statisticCollector, + }) + if err != nil { + log.Println(err.Error()) + return + } + + dynamicProxyRouter = dprouter + + //Load all conf from files + confs, _ := filepath.Glob("./conf/*.config") + for _, conf := range confs { + record, err := LoadReverseProxyConfig(conf) + if err != nil { + log.Println("Failed to load "+filepath.Base(conf), err.Error()) + return + } + + if record.ProxyType == "root" { + dynamicProxyRouter.SetRootProxy(record.ProxyTarget, record.UseTLS) + } else if record.ProxyType == "subd" { + dynamicProxyRouter.AddSubdomainRoutingService(record.Rootname, record.ProxyTarget, record.UseTLS) + } else if record.ProxyType == "vdir" { + dynamicProxyRouter.AddVirtualDirectoryProxyService(record.Rootname, record.ProxyTarget, record.UseTLS) + } else { + log.Println("Unsupported endpoint type: " + record.ProxyType + ". Skipping " + filepath.Base(conf)) + } + } + + /* + dynamicProxyRouter.SetRootProxy("192.168.0.107:8080", false) + dynamicProxyRouter.AddSubdomainRoutingService("aroz.localhost", "192.168.0.107:8080/private/AOB/", false) + dynamicProxyRouter.AddSubdomainRoutingService("loopback.localhost", "localhost:8080", false) + dynamicProxyRouter.AddSubdomainRoutingService("git.localhost", "mc.alanyeung.co:3000", false) + dynamicProxyRouter.AddVirtualDirectoryProxyService("/git/server/", "mc.alanyeung.co:3000", false) + */ + + //Start Service + //Not sure why but delay must be added if you have another + //reverse proxy server in front of this service + time.Sleep(300 * time.Millisecond) + dynamicProxyRouter.StartProxyService() + log.Println("Dynamic Reverse Proxy service started") + + //Add all proxy services to uptime monitor + //Create a uptime monitor service + go func() { + //This must be done in go routine to prevent blocking on system startup + uptimeMonitor, _ = uptime.NewUptimeMonitor(&uptime.Config{ + Targets: GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter), + Interval: 300, //5 minutes + MaxRecordsStore: 288, //1 day + }) + log.Println("Uptime Monitor background service started") + }() + +} + +func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) { + + enable, _ := utils.PostPara(r, "enable") //Support root, vdir and subd + if enable == "true" { + err := dynamicProxyRouter.StartProxyService() + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + } else { + //Check if it is loopback + if dynamicProxyRouter.IsProxiedSubdomain(r) { + //Loopback routing. Turning it off will make the user lost control + //of the whole system. Do not allow shutdown + utils.SendErrorResponse(w, "Unable to shutdown in loopback rp mode. Remove proxy rules for management interface and retry.") + return + } + + err := dynamicProxyRouter.StopProxyService() + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + } + + utils.SendOK(w) +} + +func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { + eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd + if err != nil { + utils.SendErrorResponse(w, "type not defined") + return + } + + endpoint, err := utils.PostPara(r, "ep") + if err != nil { + utils.SendErrorResponse(w, "endpoint not defined") + return + } + + tls, _ := utils.PostPara(r, "tls") + if tls == "" { + tls = "false" + } + + useTLS := (tls == "true") + rootname := "" + if eptype == "vdir" { + vdir, err := utils.PostPara(r, "rootname") + if err != nil { + utils.SendErrorResponse(w, "vdir not defined") + return + } + + //Vdir must start with / + if !strings.HasPrefix(vdir, "/") { + vdir = "/" + vdir + } + rootname = vdir + dynamicProxyRouter.AddVirtualDirectoryProxyService(vdir, endpoint, useTLS) + + } else if eptype == "subd" { + subdomain, err := utils.PostPara(r, "rootname") + if err != nil { + utils.SendErrorResponse(w, "subdomain not defined") + return + } + rootname = subdomain + dynamicProxyRouter.AddSubdomainRoutingService(subdomain, endpoint, useTLS) + } else if eptype == "root" { + rootname = "root" + dynamicProxyRouter.SetRootProxy(endpoint, useTLS) + } else { + //Invalid eptype + utils.SendErrorResponse(w, "Invalid endpoint type") + return + } + + //Save it + SaveReverseProxyConfig(eptype, rootname, endpoint, useTLS) + + //Update utm if exists + if uptimeMonitor != nil { + uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter) + uptimeMonitor.CleanRecords() + } + + utils.SendOK(w) + +} + +func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) { + ep, err := utils.GetPara(r, "ep") + if err != nil { + utils.SendErrorResponse(w, "Invalid ep given") + } + + ptype, err := utils.PostPara(r, "ptype") + if err != nil { + utils.SendErrorResponse(w, "Invalid ptype given") + } + + err = dynamicProxyRouter.RemoveProxy(ptype, ep) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + } + + RemoveReverseProxyConfig(ep) + + //Update utm if exists + if uptimeMonitor != nil { + uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter) + uptimeMonitor.CleanRecords() + } + + utils.SendOK(w) +} + +func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) { + js, _ := json.Marshal(dynamicProxyRouter) + utils.SendJSONResponse(w, string(js)) +} + +func ReverseProxyList(w http.ResponseWriter, r *http.Request) { + eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd + if err != nil { + utils.SendErrorResponse(w, "type not defined") + return + } + + if eptype == "vdir" { + results := []*dynamicproxy.ProxyEndpoint{} + dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool { + results = append(results, value.(*dynamicproxy.ProxyEndpoint)) + return true + }) + + sort.Slice(results, func(i, j int) bool { + return results[i].Domain < results[j].Domain + }) + + js, _ := json.Marshal(results) + utils.SendJSONResponse(w, string(js)) + } else if eptype == "subd" { + results := []*dynamicproxy.SubdomainEndpoint{} + dynamicProxyRouter.SubdomainEndpoint.Range(func(key, value interface{}) bool { + results = append(results, value.(*dynamicproxy.SubdomainEndpoint)) + return true + }) + + sort.Slice(results, func(i, j int) bool { + return results[i].MatchingDomain < results[j].MatchingDomain + }) + + js, _ := json.Marshal(results) + utils.SendJSONResponse(w, string(js)) + } else if eptype == "root" { + js, _ := json.Marshal(dynamicProxyRouter.Root) + utils.SendJSONResponse(w, string(js)) + } else { + utils.SendErrorResponse(w, "Invalid type given") + } +} + +// Handle https redirect +func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) { + useRedirect, err := utils.GetPara(r, "set") + if err != nil { + currentRedirectToHttps := false + //Load the current status + err = sysdb.Read("settings", "redirect", ¤tRedirectToHttps) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + js, _ := json.Marshal(currentRedirectToHttps) + utils.SendJSONResponse(w, string(js)) + } else { + if useRedirect == "true" { + sysdb.Write("settings", "redirect", true) + log.Println("Updating force HTTPS redirection to true") + dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true) + } else if useRedirect == "false" { + sysdb.Write("settings", "redirect", false) + log.Println("Updating force HTTPS redirection to false") + dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false) + } + + utils.SendOK(w) + } +} + +// Handle checking if the current user is accessing via the reverse proxied interface +// Of the management interface. +func HandleManagementProxyCheck(w http.ResponseWriter, r *http.Request) { + isProxied := dynamicProxyRouter.IsProxiedSubdomain(r) + js, _ := json.Marshal(isProxied) + utils.SendJSONResponse(w, string(js)) +} + +// Handle incoming port set. Change the current proxy incoming port +func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) { + newIncomingPort, err := utils.PostPara(r, "incoming") + if err != nil { + utils.SendErrorResponse(w, "invalid incoming port given") + return + } + + newIncomingPortInt, err := strconv.Atoi(newIncomingPort) + if err != nil { + utils.SendErrorResponse(w, "invalid incoming port given") + return + } + + //Check if it is identical as proxy root (recursion!) + proxyRoot := strings.TrimSuffix(dynamicProxyRouter.Root.Domain, "/") + if strings.HasPrefix(proxyRoot, "localhost:"+strconv.Itoa(newIncomingPortInt)) || strings.HasPrefix(proxyRoot, "127.0.0.1:"+strconv.Itoa(newIncomingPortInt)) { + //Listening port is same as proxy root + //Not allow recursive settings + utils.SendErrorResponse(w, "Recursive listening port! Check your proxy root settings.") + return + } + + //Stop and change the setting of the reverse proxy service + if dynamicProxyRouter.Running { + dynamicProxyRouter.StopProxyService() + dynamicProxyRouter.Option.Port = newIncomingPortInt + dynamicProxyRouter.StartProxyService() + } else { + //Only change setting but not starting the proxy service + dynamicProxyRouter.Option.Port = newIncomingPortInt + } + + sysdb.Write("settings", "inbound", newIncomingPortInt) + + utils.SendOK(w) +}
src/router.go+88 −0 added@@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "net/http" + "net/url" + "path/filepath" + "strings" + + "imuslab.com/zoraxy/mod/sshprox" +) + +/* + router.go + + This script holds the static resources router + for the reverse proxy service +*/ + +func FSHandler(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + /* + Development Mode Override + => Web root is located in / + */ + if development && strings.HasPrefix(r.URL.Path, "/web/") { + u, _ := url.Parse(strings.TrimPrefix(r.URL.Path, "/web")) + r.URL = u + } + + /* + Production Mode Override + => Web root is located in /web + */ + if !development && r.URL.Path == "/" { + //Redirect to web UI + http.Redirect(w, r, "/web/", http.StatusTemporaryRedirect) + return + } + + // Allow access to /script/*, /img/pubic/* and /login.html without authentication + if strings.HasPrefix(r.URL.Path, ppf("/script/")) || strings.HasPrefix(r.URL.Path, ppf("/img/public/")) || r.URL.Path == ppf("/login.html") || r.URL.Path == ppf("/reset.html") || r.URL.Path == ppf("/favicon.png") { + handler.ServeHTTP(w, r) + return + } + + // check authentication + if !authAgent.CheckAuth(r) && requireAuth { + http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect) + return + } + + //For WebSSH Routing + //Example URL Path: /web.ssh/{{instance_uuid}}/* + if strings.HasPrefix(r.URL.Path, "/web.ssh/") { + requestPath := r.URL.Path + parts := strings.Split(requestPath, "/") + if !strings.HasSuffix(requestPath, "/") && len(parts) == 3 { + http.Redirect(w, r, requestPath+"/", http.StatusTemporaryRedirect) + return + } + if len(parts) > 2 { + //Extract the instance ID from the request path + instanceUUID := parts[2] + fmt.Println(instanceUUID) + + //Rewrite the url so the proxy knows how to serve stuffs + r.URL, _ = sshprox.RewriteURL("/web.ssh/"+instanceUUID, r.RequestURI) + webSshManager.HandleHttpByInstanceId(instanceUUID, w, r) + } else { + fmt.Println(parts) + http.Error(w, "Invalid Usage", http.StatusInternalServerError) + } + return + } + + //Authenticated + handler.ServeHTTP(w, r) + }) +} + +// Production path fix wrapper. Fix the path on production or development environment +func ppf(relativeFilepath string) string { + if !development { + return strings.ReplaceAll(filepath.Join("/web/", relativeFilepath), "\\", "/") + } + return relativeFilepath +}
src/start.go+182 −0 added@@ -0,0 +1,182 @@ +package main + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" + "time" + + "imuslab.com/zoraxy/mod/auth" + "imuslab.com/zoraxy/mod/database" + "imuslab.com/zoraxy/mod/dynamicproxy/redirection" + "imuslab.com/zoraxy/mod/ganserv" + "imuslab.com/zoraxy/mod/geodb" + "imuslab.com/zoraxy/mod/mdns" + "imuslab.com/zoraxy/mod/netstat" + "imuslab.com/zoraxy/mod/sshprox" + "imuslab.com/zoraxy/mod/statistic" + "imuslab.com/zoraxy/mod/statistic/analytic" + "imuslab.com/zoraxy/mod/tcpprox" + "imuslab.com/zoraxy/mod/tlscert" +) + +/* + Startup Sequence + + This function starts the startup sequence of all + required modules +*/ + +var ( + /* + MDNS related + */ + previousmdnsScanResults = []*mdns.NetworkHost{} + mdnsTickerStop chan bool +) + +func startupSequence() { + //Create database + db, err := database.NewDatabase("sys.db", false) + if err != nil { + log.Fatal(err) + } + sysdb = db + //Create tables for the database + sysdb.NewTable("settings") + + //Create tmp folder + os.MkdirAll("./tmp", 0775) + + //Create an auth agent + sessionKey, err := auth.GetSessionKey(sysdb) + if err != nil { + log.Fatal(err) + } + authAgent = auth.NewAuthenticationAgent(name, []byte(sessionKey), sysdb, true, func(w http.ResponseWriter, r *http.Request) { + //Not logged in. Redirecting to login page + http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect) + }) + + //Create a TLS certificate manager + tlsCertManager, err = tlscert.NewManager("./certs", development) + if err != nil { + panic(err) + } + + //Create a redirection rule table + redirectTable, err = redirection.NewRuleTable("./rules") + if err != nil { + panic(err) + } + + //Create a geodb store + geodbStore, err = geodb.NewGeoDb(sysdb) + if err != nil { + panic(err) + } + + //Create a statistic collector + statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{ + Database: sysdb, + }) + if err != nil { + panic(err) + } + + if err != nil { + panic(err) + } + + //Create a netstat buffer + netstatBuffers, err = netstat.NewNetStatBuffer(300) + if err != nil { + log.Println("Failed to load network statistic info") + panic(err) + } + + /* + MDNS Discovery Service + + This discover nearby ArozOS Nodes or other services + that provide mDNS discovery with domain (e.g. Synology NAS) + */ + portInt, err := strconv.Atoi(strings.Split(handler.Port, ":")[1]) + if err != nil { + portInt = 8000 + } + mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{ + HostName: "zoraxy_" + nodeUUID, + Port: portInt, + Domain: "zoraxy.imuslab.com", + Model: "Network Gateway", + UUID: nodeUUID, + Vendor: "imuslab.com", + BuildVersion: version, + }, "") + if err != nil { + panic(err) + } + + //Start initial scanning + go func() { + hosts := mdnsScanner.Scan(30, "") + previousmdnsScanResults = hosts + log.Println("mDNS Startup scan completed") + }() + + //Create a ticker to update mDNS results every 5 minutes + ticker := time.NewTicker(15 * time.Minute) + stopChan := make(chan bool) + go func() { + for { + select { + case <-stopChan: + ticker.Stop() + case <-ticker.C: + hosts := mdnsScanner.Scan(30, "") + previousmdnsScanResults = hosts + log.Println("mDNS scan result updated") + } + } + }() + mdnsTickerStop = stopChan + + /* + Global Area Network + + Require zerotier token to work + */ + usingZtAuthToken := *ztAuthToken + if usingZtAuthToken == "" { + usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey() + if err != nil { + log.Println("Failed to load ZeroTier controller API authtoken") + } + } + ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{ + AuthToken: usingZtAuthToken, + ApiPort: *ztAPIPort, + Database: sysdb, + }) + + //Create WebSSH Manager + webSshManager = sshprox.NewSSHProxyManager() + + //Create TCP Proxy Manager + tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{ + Database: sysdb, + }) + + //Create WoL MAC storage table + sysdb.NewTable("wolmac") + + //Create an email sender if SMTP config exists + sysdb.NewTable("smtp") + EmailSender = loadSMTPConfig() + + //Create an analytic loader + AnalyticLoader = analytic.NewDataLoader(sysdb, statisticCollector) +}
src/webssh.go+107 −0 added@@ -0,0 +1,107 @@ +package main + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" + + "imuslab.com/zoraxy/mod/sshprox" + "imuslab.com/zoraxy/mod/utils" +) + +/* + webssh.go + + This script handle the establish of a new ssh proxy object +*/ + +func HandleCreateProxySession(w http.ResponseWriter, r *http.Request) { + //Get what ip address and port to connect to + ipaddr, err := utils.PostPara(r, "ipaddr") + if err != nil { + http.Error(w, "Invalid Usage", http.StatusInternalServerError) + return + } + + portString, err := utils.PostPara(r, "port") + if err != nil { + portString = "22" + } + + username, err := utils.PostPara(r, "username") + if err != nil { + username = "" + } + + port, err := strconv.Atoi(portString) + if err != nil { + utils.SendErrorResponse(w, "invalid port number given") + return + } + + if !*allowSshLoopback { + //Not allow loopback connections + if strings.EqualFold(strings.TrimSpace(ipaddr), "localhost") || strings.TrimSpace(ipaddr) == "127.0.0.1" { + //Request target is loopback + utils.SendErrorResponse(w, "loopback web ssh connection is not enabled on this host") + return + } + } + + //Check if the target is a valid ssh endpoint + if !sshprox.IsSSHConnectable(ipaddr, port) { + utils.SendErrorResponse(w, ipaddr+":"+strconv.Itoa(port)+" is not a valid SSH server") + return + } + + //Create a new proxy instance + instance, err := webSshManager.NewSSHProxy("./tmp/gotty") + if err != nil { + utils.SendErrorResponse(w, strings.ReplaceAll(err.Error(), "\\", "/")) + return + } + + //Create an ssh process to the target address + err = instance.CreateNewConnection(webSshManager.GetNextPort(), username, ipaddr, port) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + //Return the instance uuid + js, _ := json.Marshal(instance.UUID) + utils.SendJSONResponse(w, string(js)) +} + +//Check if the host support ssh, or if the target domain (and port, optional) support ssh +func HandleWebSshSupportCheck(w http.ResponseWriter, r *http.Request) { + domain, err := utils.PostPara(r, "domain") + if err != nil { + //Check if ssh supported on this host + isSupport := sshprox.IsWebSSHSupported() + js, _ := json.Marshal(isSupport) + utils.SendJSONResponse(w, string(js)) + } else { + //Domain is given. Check if port is given + portString, err := utils.PostPara(r, "port") + if err != nil { + portString = "22" + } + + port, err := strconv.Atoi(portString) + if err != nil { + utils.SendErrorResponse(w, "invalid port number given") + return + } + + if port < 1 || port > 65534 { + utils.SendErrorResponse(w, "invalid port number given") + return + } + + looksLikeSSHServer := sshprox.IsSSHConnectable(domain, port) + js, _ := json.Marshal(looksLikeSSHServer) + utils.SendJSONResponse(w, string(js)) + } +}
src/wrappers.go+299 −0 added@@ -0,0 +1,299 @@ +package main + +/* + Wrappers.go + + This script provide wrapping functions + for modules that do not provide + handler interface within the modules + + --- NOTES --- + If your module have more than one layer + or require state keeping, please move + the abstraction up one layer into + your own module. Do not keep state on + the global scope other than single + Manager instance +*/ + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "time" + + "imuslab.com/zoraxy/mod/dynamicproxy" + "imuslab.com/zoraxy/mod/ipscan" + "imuslab.com/zoraxy/mod/mdns" + "imuslab.com/zoraxy/mod/uptime" + "imuslab.com/zoraxy/mod/utils" + "imuslab.com/zoraxy/mod/wakeonlan" +) + +/* + Proxy Utils +*/ +//Check if site support TLS +func HandleCheckSiteSupportTLS(w http.ResponseWriter, r *http.Request) { + targetURL, err := utils.PostPara(r, "url") + if err != nil { + utils.SendErrorResponse(w, "invalid url given") + return + } + + httpsUrl := fmt.Sprintf("https://%s", targetURL) + httpUrl := fmt.Sprintf("http://%s", targetURL) + + client := http.Client{Timeout: 5 * time.Second} + + resp, err := client.Head(httpsUrl) + if err == nil && resp.StatusCode == http.StatusOK { + js, _ := json.Marshal("https") + utils.SendJSONResponse(w, string(js)) + return + } + + resp, err = client.Head(httpUrl) + if err == nil && resp.StatusCode == http.StatusOK { + js, _ := json.Marshal("http") + utils.SendJSONResponse(w, string(js)) + return + } + + utils.SendErrorResponse(w, "invalid url given") +} + +/* + Statistic Summary +*/ + +//Handle conversion of statistic daily summary to country summary +func HandleCountryDistrSummary(w http.ResponseWriter, r *http.Request) { + requestClientCountry := map[string]int{} + statisticCollector.DailySummary.RequestClientIp.Range(func(key, value interface{}) bool { + //Get this client country of original + clientIp := key.(string) + //requestCount := value.(int) + + ci, err := geodbStore.ResolveCountryCodeFromIP(clientIp) + if err != nil { + return true + } + + isoCode := ci.CountryIsoCode + if isoCode == "" { + //local or reserved addr + isoCode = "local" + } + uc, ok := requestClientCountry[isoCode] + if !ok { + //Create the counter + requestClientCountry[isoCode] = 1 + } else { + requestClientCountry[isoCode] = uc + 1 + } + return true + }) + + js, _ := json.Marshal(requestClientCountry) + utils.SendJSONResponse(w, string(js)) +} + +/* + Up Time Monitor +*/ +//Generate uptime monitor targets from reverse proxy rules +func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Target { + subds := dp.GetSDProxyEndpointsAsMap() + vdirs := dp.GetVDProxyEndpointsAsMap() + + UptimeTargets := []*uptime.Target{} + for subd, target := range subds { + url := "http://" + target.Domain + protocol := "http" + if target.RequireTLS { + url = "https://" + target.Domain + protocol = "https" + } + UptimeTargets = append(UptimeTargets, &uptime.Target{ + ID: subd, + Name: subd, + URL: url, + Protocol: protocol, + }) + } + + for vdir, target := range vdirs { + url := "http://" + target.Domain + protocol := "http" + if target.RequireTLS { + url = "https://" + target.Domain + protocol = "https" + } + UptimeTargets = append(UptimeTargets, &uptime.Target{ + ID: vdir, + Name: "*" + vdir, + URL: url, + Protocol: protocol, + }) + } + + return UptimeTargets +} + +//Handle rendering up time monitor data +func HandleUptimeMonitorListing(w http.ResponseWriter, r *http.Request) { + if uptimeMonitor != nil { + uptimeMonitor.HandleUptimeLogRead(w, r) + } else { + http.Error(w, "500 - Internal Server Error", http.StatusInternalServerError) + return + } +} + +//Handle listing current registered mdns nodes +func HandleMdnsListing(w http.ResponseWriter, r *http.Request) { + js, _ := json.Marshal(previousmdnsScanResults) + utils.SendJSONResponse(w, string(js)) +} + +func HandleMdnsScanning(w http.ResponseWriter, r *http.Request) { + domain, err := utils.PostPara(r, "domain") + var hosts []*mdns.NetworkHost + if err != nil { + //Search for arozos node + hosts = mdnsScanner.Scan(30, "") + previousmdnsScanResults = hosts + } else { + //Search for other nodes + hosts = mdnsScanner.Scan(30, domain) + } + + js, _ := json.Marshal(hosts) + utils.SendJSONResponse(w, string(js)) +} + +//handle ip scanning +func HandleIpScan(w http.ResponseWriter, r *http.Request) { + cidr, err := utils.PostPara(r, "cidr") + if err != nil { + //Ip range mode + start, err := utils.PostPara(r, "start") + if err != nil { + utils.SendErrorResponse(w, "missing start ip") + return + } + + end, err := utils.PostPara(r, "end") + if err != nil { + utils.SendErrorResponse(w, "missing end ip") + return + } + + discoveredHosts, err := ipscan.ScanIpRange(start, end) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(discoveredHosts) + utils.SendJSONResponse(w, string(js)) + } else { + //CIDR mode + discoveredHosts, err := ipscan.ScanCIDRRange(cidr) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + js, _ := json.Marshal(discoveredHosts) + utils.SendJSONResponse(w, string(js)) + } +} + +/* + Handle wake on LAN + Support following methods + /?set=xxx&name=xxx Record a new MAC address into the database + /?wake=xxx Wake a server given its MAC address + /?del=xxx Delete a server given its MAC address + / Default: list all recorded WoL MAC address + +*/ +func HandleWakeOnLan(w http.ResponseWriter, r *http.Request) { + set, _ := utils.PostPara(r, "set") + del, _ := utils.PostPara(r, "del") + wake, _ := utils.PostPara(r, "wake") + if set != "" { + //Get the name of the describing server + servername, err := utils.PostPara(r, "name") + if err != nil { + utils.SendErrorResponse(w, "invalid server name given") + return + } + + //Check if the given mac address is a valid mac address + set = strings.TrimSpace(set) + if !wakeonlan.IsValidMacAddress(set) { + utils.SendErrorResponse(w, "invalid mac address given") + return + } + + //Store this into the database + sysdb.Write("wolmac", set, servername) + + utils.SendOK(w) + } else if wake != "" { + //Wake the target up by MAC address + if !wakeonlan.IsValidMacAddress(wake) { + utils.SendErrorResponse(w, "invalid mac address given") + return + } + + log.Println("[WoL] Sending Wake on LAN magic packet to " + wake) + err := wakeonlan.WakeTarget(wake) + if err != nil { + utils.SendErrorResponse(w, err.Error()) + return + } + + utils.SendOK(w) + } else if del != "" { + if !wakeonlan.IsValidMacAddress(del) { + utils.SendErrorResponse(w, "invalid mac address given") + return + } + + sysdb.Delete("wolmac", del) + utils.SendOK(w) + } else { + //List all the saved WoL MAC Address + entries, err := sysdb.ListTable("wolmac") + if err != nil { + utils.SendErrorResponse(w, "unknown error occured") + return + } + + type MacAddrRecord struct { + ServerName string + MacAddr string + } + + results := []*MacAddrRecord{} + for _, keypairs := range entries { + macAddr := string(keypairs[0]) + serverName := "" + json.Unmarshal(keypairs[1], &serverName) + + results = append(results, &MacAddrRecord{ + ServerName: serverName, + MacAddr: macAddr, + }) + } + + js, _ := json.Marshal(results) + utils.SendJSONResponse(w, string(js)) + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-7hpf-g48v-hw3jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-52010ghsaADVISORY
- github.com/tobychui/zoraxy/commit/2e9bc77a5d832bff1093058d42ce7a61382e4bc6nvdWEB
- github.com/tobychui/zoraxy/commit/c07d5f85dfc37bd32819358ed7d4bc32c604e8f0nvdWEB
- github.com/tobychui/zoraxy/security/advisories/GHSA-7hpf-g48v-hw3jnvdWEB
- pkg.go.dev/vuln/GO-2024-3267ghsaWEB
News mentions
0No linked articles in our index yet.