v2 init commit

This commit is contained in:
Toby Chui 2023-05-22 23:05:59 +08:00
parent 5ac0fdde1d
commit c07d5f85df
87 changed files with 273125 additions and 0 deletions

60
src/Makefile Normal file
View 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
View 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
View 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
View 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", &currentTlsSetting)
}
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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

478
src/mod/auth/auth.go Normal file
View 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
View 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
}

View 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()
}

View 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()
}

View 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
}

View 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)
}
}

View 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
}

View 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.

View 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
}
}

View 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
}

View 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
}

View 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)
}()
}
}

View 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
}
}

View 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
}

View 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)
}

View 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
View 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
}

View 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
}

View 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"
}

View 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
View 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
View 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)
}

View 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
}

View 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
View 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
View 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
View 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)
}

View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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.

View 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
}
}

View 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 = ""
// }

View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

222
src/mod/sshprox/sshprox.go Normal file
View 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
View 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
}

View 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))
}

View 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
}

View 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))
}
}

View 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{},
}
}

View 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
}

View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
}

View 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-----

View 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
View 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
View 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
View 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
View 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
View 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))
}

View 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
}

View 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.

View File

@ -0,0 +1,54 @@
# WebsocketProxy [![GoDoc](https://godoc.org/github.com/koding/websocketproxy?status.svg)](https://godoc.org/github.com/koding/websocketproxy) [![Build Status](https://travis-ci.org/koding/websocketproxy.svg)](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`

View 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
}

View 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
View 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
View 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", &currentRedirectToHttps)
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
View 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
View 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
View 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
View 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))
}
}