mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-23 07:53:05 +02:00
v2 init commit
This commit is contained in:
parent
5ac0fdde1d
commit
c07d5f85df
60
src/Makefile
Normal file
60
src/Makefile
Normal file
@ -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
|
203
src/api.go
Normal file
203
src/api.go
Normal file
@ -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, "")
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
102
src/blacklist.go
Normal file
102
src/blacklist.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
189
src/cert.go
Normal file
189
src/cert.go
Normal file
@ -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())
|
||||||
|
}
|
||||||
|
}
|
86
src/config.go
Normal file
86
src/config.go
Normal file
@ -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
|
||||||
|
}
|
298
src/emails.go
Normal file
298
src/emails.go
Normal file
@ -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)
|
||||||
|
}
|
39
src/geoip.go
Normal file
39
src/geoip.go
Normal file
@ -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
|
||||||
|
}
|
16
src/go.mod
Normal file
16
src/go.mod
Normal file
@ -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
|
||||||
|
)
|
94
src/go.sum
Normal file
94
src/go.sum
Normal file
@ -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=
|
158
src/main.go
Normal file
158
src/main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
76
src/mod/aroz/aroz.go
Normal file
76
src/mod/aroz/aroz.go
Normal file
@ -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
|
||||||
|
}
|
BIN
src/mod/aroz/doc.txt
Normal file
BIN
src/mod/aroz/doc.txt
Normal file
Binary file not shown.
478
src/mod/auth/auth.go
Normal file
478
src/mod/auth/auth.go
Normal file
@ -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))
|
||||||
|
}
|
53
src/mod/auth/router.go
Normal file
53
src/mod/auth/router.go
Normal file
@ -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
|
||||||
|
}
|
120
src/mod/database/database.go
Normal file
120
src/mod/database/database.go
Normal file
@ -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()
|
||||||
|
}
|
186
src/mod/database/database_core.go
Normal file
186
src/mod/database/database_core.go
Normal file
@ -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()
|
||||||
|
}
|
208
src/mod/database/database_openwrt.go
Normal file
208
src/mod/database/database_openwrt.go
Normal file
@ -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
|
||||||
|
}
|
85
src/mod/dynamicproxy/Server.go
Normal file
85
src/mod/dynamicproxy/Server.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
23
src/mod/dynamicproxy/domainsniff/domainsniff.go
Normal file
23
src/mod/dynamicproxy/domainsniff/domainsniff.go
Normal file
@ -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
|
||||||
|
}
|
21
src/mod/dynamicproxy/dpcore/LICENSE
Normal file
21
src/mod/dynamicproxy/dpcore/LICENSE
Normal file
@ -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.
|
494
src/mod/dynamicproxy/dpcore/dpcore.go
Normal file
494
src/mod/dynamicproxy/dpcore/dpcore.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
21
src/mod/dynamicproxy/dpcore/utils.go
Normal file
21
src/mod/dynamicproxy/dpcore/utils.go
Normal file
@ -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
|
||||||
|
}
|
373
src/mod/dynamicproxy/dynamicproxy.go
Normal file
373
src/mod/dynamicproxy/dynamicproxy.go
Normal file
@ -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
|
||||||
|
}
|
155
src/mod/dynamicproxy/proxyRequestHandler.go
Normal file
155
src/mod/dynamicproxy/proxyRequestHandler.go
Normal file
@ -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)
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
56
src/mod/dynamicproxy/redirection/handler.go
Normal file
56
src/mod/dynamicproxy/redirection/handler.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
162
src/mod/dynamicproxy/redirection/redirection.go
Normal file
162
src/mod/dynamicproxy/redirection/redirection.go
Normal file
@ -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
|
||||||
|
}
|
85
src/mod/dynamicproxy/special.go
Normal file
85
src/mod/dynamicproxy/special.go
Normal file
@ -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)
|
||||||
|
}
|
44
src/mod/dynamicproxy/subdomain.go
Normal file
44
src/mod/dynamicproxy/subdomain.go
Normal file
@ -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
|
||||||
|
}
|
60
src/mod/email/email.go
Normal file
60
src/mod/email/email.go
Normal file
@ -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
|
||||||
|
}
|
80
src/mod/ganserv/authkey.go
Normal file
80
src/mod/ganserv/authkey.go
Normal file
@ -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
|
||||||
|
}
|
37
src/mod/ganserv/authkeyLinux.go
Normal file
37
src/mod/ganserv/authkeyLinux.go
Normal file
@ -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"
|
||||||
|
}
|
73
src/mod/ganserv/authkeyWin.go
Normal file
73
src/mod/ganserv/authkeyWin.go
Normal file
@ -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
|
||||||
|
}
|
128
src/mod/ganserv/ganserv.go
Normal file
128
src/mod/ganserv/ganserv.go
Normal file
@ -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)
|
||||||
|
}
|
428
src/mod/ganserv/handlers.go
Normal file
428
src/mod/ganserv/handlers.go
Normal file
@ -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)
|
||||||
|
}
|
39
src/mod/ganserv/network.go
Normal file
39
src/mod/ganserv/network.go
Normal file
@ -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
|
||||||
|
}
|
55
src/mod/ganserv/network_test.go
Normal file
55
src/mod/ganserv/network_test.go
Normal file
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
55
src/mod/ganserv/utils.go
Normal file
55
src/mod/ganserv/utils.go
Normal file
@ -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)
|
||||||
|
}
|
622
src/mod/ganserv/zerotier.go
Normal file
622
src/mod/ganserv/zerotier.go
Normal file
@ -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
|
||||||
|
}
|
250
src/mod/geodb/geodb.go
Normal file
250
src/mod/geodb/geodb.go
Normal file
@ -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)
|
||||||
|
}
|
73
src/mod/geodb/geodb_test.go
Normal file
73
src/mod/geodb/geodb_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
261318
src/mod/geodb/geoipv4.csv
Normal file
261318
src/mod/geodb/geoipv4.csv
Normal file
File diff suppressed because it is too large
Load Diff
89
src/mod/geodb/geoloader.go
Normal file
89
src/mod/geodb/geoloader.go
Normal file
@ -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
|
||||||
|
}
|
131
src/mod/geodb/trie.go
Normal file
131
src/mod/geodb/trie.go
Normal file
@ -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 ""
|
||||||
|
}
|
149
src/mod/ipscan/ipscan.go
Normal file
149
src/mod/ipscan/ipscan.go
Normal file
@ -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
|
||||||
|
}
|
243
src/mod/mdns/mdns.go
Normal file
243
src/mod/mdns/mdns.go
Normal file
@ -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
|
||||||
|
}
|
339
src/mod/netstat/netstat.go
Normal file
339
src/mod/netstat/netstat.go
Normal file
@ -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")
|
||||||
|
}
|
55
src/mod/netstat/nic.go
Normal file
55
src/mod/netstat/nic.go
Normal file
@ -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
|
||||||
|
}
|
21
src/mod/reverseproxy/LICENSE
Normal file
21
src/mod/reverseproxy/LICENSE
Normal file
@ -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.
|
405
src/mod/reverseproxy/reverse.go
Normal file
405
src/mod/reverseproxy/reverse.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
302
src/mod/sshprox/gotty/.gotty
Normal file
302
src/mod/sshprox/gotty/.gotty
Normal file
@ -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 = ""
|
||||||
|
|
||||||
|
// }
|
21
src/mod/sshprox/gotty/LICENSE
Normal file
21
src/mod/sshprox/gotty/LICENSE
Normal file
@ -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.
|
BIN
src/mod/sshprox/gotty/gotty_linux_386
Normal file
BIN
src/mod/sshprox/gotty/gotty_linux_386
Normal file
Binary file not shown.
BIN
src/mod/sshprox/gotty/gotty_linux_amd64
Normal file
BIN
src/mod/sshprox/gotty/gotty_linux_amd64
Normal file
Binary file not shown.
BIN
src/mod/sshprox/gotty/gotty_linux_arm
Normal file
BIN
src/mod/sshprox/gotty/gotty_linux_arm
Normal file
Binary file not shown.
BIN
src/mod/sshprox/gotty/gotty_linux_arm64
Normal file
BIN
src/mod/sshprox/gotty/gotty_linux_arm64
Normal file
Binary file not shown.
222
src/mod/sshprox/sshprox.go
Normal file
222
src/mod/sshprox/sshprox.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
src/mod/sshprox/utils.go
Normal file
72
src/mod/sshprox/utils.go
Normal file
@ -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
|
||||||
|
}
|
128
src/mod/statistic/analytic/analytic.go
Normal file
128
src/mod/statistic/analytic/analytic.go
Normal file
@ -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))
|
||||||
|
}
|
72
src/mod/statistic/analytic/utils.go
Normal file
72
src/mod/statistic/analytic/utils.go
Normal file
@ -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
|
||||||
|
}
|
40
src/mod/statistic/handler.go
Normal file
40
src/mod/statistic/handler.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
237
src/mod/statistic/statistic.go
Normal file
237
src/mod/statistic/statistic.go
Normal file
@ -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{},
|
||||||
|
}
|
||||||
|
}
|
108
src/mod/statistic/structconv.go
Normal file
108
src/mod/statistic/structconv.go
Normal file
@ -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
|
||||||
|
}
|
28
src/mod/statistic/utils.go
Normal file
28
src/mod/statistic/utils.go
Normal file
@ -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)
|
||||||
|
}
|
324
src/mod/tcpprox/conn.go
Normal file
324
src/mod/tcpprox/conn.go
Normal file
@ -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
|
||||||
|
}
|
162
src/mod/tcpprox/handler.go
Normal file
162
src/mod/tcpprox/handler.go
Normal file
@ -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)
|
||||||
|
}
|
289
src/mod/tcpprox/nb.go.ref
Normal file
289
src/mod/tcpprox/nb.go.ref
Normal file
@ -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
|
||||||
|
}
|
159
src/mod/tcpprox/tcpprox.go
Normal file
159
src/mod/tcpprox/tcpprox.go
Normal file
@ -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)
|
||||||
|
}
|
43
src/mod/tcpprox/tcpprox_test.go
Normal file
43
src/mod/tcpprox/tcpprox_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
60
src/mod/tlscert/helper.go
Normal file
60
src/mod/tlscert/helper.go
Normal file
@ -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
|
||||||
|
}
|
34
src/mod/tlscert/localhost.crt
Normal file
34
src/mod/tlscert/localhost.crt
Normal file
@ -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-----
|
52
src/mod/tlscert/localhost.key
Normal file
52
src/mod/tlscert/localhost.key
Normal file
@ -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-----
|
187
src/mod/tlscert/tlscert.go
Normal file
187
src/mod/tlscert/tlscert.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
228
src/mod/uptime/uptime.go
Normal file
228
src/mod/uptime/uptime.go
Normal file
@ -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
|
||||||
|
}
|
16
src/mod/utils/conv.go
Normal file
16
src/mod/utils/conv.go
Normal file
@ -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
|
||||||
|
}
|
19
src/mod/utils/template.go
Normal file
19
src/mod/utils/template.go
Normal file
@ -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))
|
||||||
|
}
|
175
src/mod/utils/utils.go
Normal file
175
src/mod/utils/utils.go
Normal file
@ -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))
|
||||||
|
}
|
68
src/mod/wakeonlan/wakeonlan.go
Normal file
68
src/mod/wakeonlan/wakeonlan.go
Normal file
@ -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
|
||||||
|
}
|
20
src/mod/websocketproxy/LICENSE.md
Normal file
20
src/mod/websocketproxy/LICENSE.md
Normal file
@ -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.
|
54
src/mod/websocketproxy/README.md
Normal file
54
src/mod/websocketproxy/README.md
Normal file
@ -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`
|
||||||
|
|
||||||
|
|
239
src/mod/websocketproxy/websocketproxy.go
Normal file
239
src/mod/websocketproxy/websocketproxy.go
Normal file
@ -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
|
||||||
|
}
|
130
src/mod/websocketproxy/websocketproxy_test.go
Normal file
130
src/mod/websocketproxy/websocketproxy_test.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
|
}
|
75
src/redirect.go
Normal file
75
src/redirect.go
Normal file
@ -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)
|
||||||
|
}
|
350
src/reverseproxy.go
Normal file
350
src/reverseproxy.go
Normal file
@ -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)
|
||||||
|
}
|
88
src/router.go
Normal file
88
src/router.go
Normal file
@ -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
|
||||||
|
}
|
182
src/start.go
Normal file
182
src/start.go
Normal file
@ -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)
|
||||||
|
}
|
107
src/webssh.go
Normal file
107
src/webssh.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
|
}
|
299
src/wrappers.go
Normal file
299
src/wrappers.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user