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