Merge pull request #192 from tobychui/v3.0.6

V3.0.6 Update

- Added fastly_client_ip to X-Real-IP auto rewrite
- Added atomic accumulator to TCP proxy
- Added white logo for future dark theme
- Added multi selection for white / blacklist #176 
- Moved custom header rewrite to dpcore 
- Restructure dpcore header rewrite sequence
- Added advance custom header settings (zoraxy to upstream and zoraxy to downstream mode)
- Added header remove feature
- Removed password requirement for SMTP #162 #80 
- Restructured TCP proxy into Stream Proxy (Support both TCP and UDP) #147 
- Added stream proxy auto start #169 
- Optimized UX for reminding user to click Apply after port change
- Added version number to footer #160
This commit is contained in:
Toby Chui 2024-06-10 16:32:39 +08:00 committed by GitHub
commit 83536a83f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 14896 additions and 15896 deletions

View File

@ -141,14 +141,13 @@ func initAPIs() {
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/start", tcpProxyManager.HandleStartProxy)
authRouter.HandleFunc("/api/tcpprox/config/stop", tcpProxyManager.HandleStopProxy)
authRouter.HandleFunc("/api/tcpprox/config/delete", tcpProxyManager.HandleRemoveProxy)
authRouter.HandleFunc("/api/tcpprox/config/status", tcpProxyManager.HandleGetProxyStatus)
authRouter.HandleFunc("/api/tcpprox/config/validate", tcpProxyManager.HandleConfigValidate)
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
authRouter.HandleFunc("/api/streamprox/config/start", streamProxyManager.HandleStartProxy)
authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
//mDNS APIs
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)

View File

@ -28,7 +28,7 @@ import (
"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/streamproxy"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
@ -52,7 +52,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
var (
name = "Zoraxy"
version = "3.0.5"
version = "3.0.6"
nodeUUID = "generic"
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
@ -79,7 +79,7 @@ var (
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
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs

View File

@ -14,11 +14,16 @@ import (
Main server for dynamic proxy core
Routing Handler Priority (High to Low)
- Blacklist
- Whitelist
- Special Routing Rule (e.g. acme)
- Redirectable
- Subdomain Routing
- Vitrual Directory Routing
- Access Router
- Blacklist
- Whitelist
- Basic Auth
- Vitrual Directory Proxy
- Subdomain Proxy
- Root router (default site router)
*/
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -34,9 +39,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
//Inject headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/*
Redirection Routing
*/

View File

@ -0,0 +1,46 @@
package dynamicproxy
/*
CustomHeader.go
This script handle parsing and injecting custom headers
into the dpcore routing logic
*/
//SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
//return upstream header and downstream header key-value pairs
//if the header is expected to be deleted, the value will be set to empty string
func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]string, [][]string) {
if len(ept.UserDefinedHeaders) == 0 {
//Early return if there are no defined headers
return [][]string{}, [][]string{}
}
//Use pre-allocation for faster performance
upstreamHeaders := make([][]string, len(ept.UserDefinedHeaders))
downstreamHeaders := make([][]string, len(ept.UserDefinedHeaders))
upstreamHeaderCounter := 0
downstreamHeaderCounter := 0
//Sort the headers into upstream or downstream
for _, customHeader := range ept.UserDefinedHeaders {
thisHeaderSet := make([]string, 2)
thisHeaderSet[0] = customHeader.Key
thisHeaderSet[1] = customHeader.Value
if customHeader.IsRemove {
//Prevent invalid config
thisHeaderSet[1] = ""
}
//Assign to slice
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
upstreamHeaderCounter++
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
downstreamHeaderCounter++
}
}
return upstreamHeaders, downstreamHeaders
}

View File

@ -57,11 +57,14 @@ type ReverseProxy struct {
}
type ResponseRewriteRuleSet struct {
ProxyDomain string
OriginalHost string
UseTLS bool
NoCache bool
PathPrefix string //Vdir prefix for root, / will be rewrite to this
ProxyDomain string
OriginalHost string
UseTLS bool
NoCache bool
PathPrefix string //Vdir prefix for root, / will be rewrite to this
UpstreamHeaders [][]string
DownstreamHeaders [][]string
Version string //Version number of Zoraxy, use for X-Proxy-By
}
type requestCanceler interface {
@ -248,78 +251,6 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
}
}
func removeHeaders(header http.Header, noCache bool) {
// 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)
}
}
//Restore the Upgrade header if any
if header.Get("Zr-Origin-Upgrade") != "" {
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("Zr-Origin-Upgrade")
}
//Disable cache if nocache is set
if noCache {
header.Del("Cache-Control")
header.Set("Cache-Control", "no-store")
}
//Hide Go-HTTP-Client UA if the client didnt sent us one
if _, ok := header["User-Agent"]; !ok {
// If the outbound request doesn't have a User-Agent header set,
// don't send the default Go HTTP client User-Agent.
header.Set("User-Agent", "")
}
}
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)
if req.TLS != nil {
req.Header.Set("X-Forwarded-Proto", "https")
} else {
req.Header.Set("X-Forwarded-Proto", "http")
}
if req.Header.Get("X-Real-Ip") == "" {
//Check if CF-Connecting-IP header exists
CF_Connecting_IP := req.Header.Get("CF-Connecting-IP")
if CF_Connecting_IP != "" {
//Use CF Connecting IP
req.Header.Set("X-Real-Ip", CF_Connecting_IP)
} else {
// Not exists. Fill it in with first entry in X-Forwarded-For
ips := strings.Split(clientIP, ",")
if len(ips) > 0 {
req.Header.Set("X-Real-Ip", strings.TrimSpace(ips[0]))
}
}
}
}
}
func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
transport := p.Transport
@ -358,12 +289,18 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
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.
// Remove hop-by-hop headers.
removeHeaders(outreq.Header, rrr.NoCache)
// Add X-Forwarded-For Header.
addXForwardedForHeader(outreq)
// Add user defined headers (to upstream)
injectUserDefinedHeaders(outreq.Header, rrr.UpstreamHeaders)
// Rewrite outbound UA, must be after user headers
rewriteUserAgent(outreq.Header, "Zoraxy/"+rrr.Version)
res, err := transport.RoundTrip(outreq)
if err != nil {
if p.Verbal {
@ -394,13 +331,17 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
}
}
//TODO: Figure out a way to proxy for proxmox
//if res.StatusCode == 501 || res.StatusCode == 500 {
// fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI)
// fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode)
// fmt.Println(outreq.Header, req.Host)
//}
//Custom header rewriter functions
//Add debug X-Proxy-By tracker
res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version)
//Custom Location header rewriter functions
if res.Header.Get("Location") != "" {
locationRewrite := res.Header.Get("Location")
originLocation := res.Header.Get("Location")
@ -426,6 +367,9 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
res.Header.Set("Location", locationRewrite)
}
// Add user defined headers (to downstream)
injectUserDefinedHeaders(res.Header, rrr.DownstreamHeaders)
// Copy header from response to client.
copyHeader(rw.Header(), res.Header)

View File

@ -0,0 +1,121 @@
package dpcore
import (
"net"
"net/http"
"strings"
)
/*
Header.go
This script handles headers rewrite and remove
in dpcore.
Added in Zoraxy v3.0.6 by tobychui
*/
// removeHeaders Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
func removeHeaders(header http.Header, noCache bool) {
// 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)
}
}
//Restore the Upgrade header if any
if header.Get("Zr-Origin-Upgrade") != "" {
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("Zr-Origin-Upgrade")
}
//Disable cache if nocache is set
if noCache {
header.Del("Cache-Control")
header.Set("Cache-Control", "no-store")
}
}
// rewriteUserAgent rewrite the user agent based on incoming request
func rewriteUserAgent(header http.Header, UA string) {
//Hide Go-HTTP-Client UA if the client didnt sent us one
if header.Get("User-Agent") == "" {
// If the outbound request doesn't have a User-Agent header set,
// don't send the default Go HTTP client User-Agent
header.Del("User-Agent")
header.Set("User-Agent", UA)
}
}
// Add X-Forwarded-For Header and rewrite X-Real-Ip according to sniffing logics
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)
if req.TLS != nil {
req.Header.Set("X-Forwarded-Proto", "https")
} else {
req.Header.Set("X-Forwarded-Proto", "http")
}
if req.Header.Get("X-Real-Ip") == "" {
//Check if CF-Connecting-IP header exists
CF_Connecting_IP := req.Header.Get("CF-Connecting-IP")
Fastly_Client_IP := req.Header.Get("Fastly-Client-IP")
if CF_Connecting_IP != "" {
//Use CF Connecting IP
req.Header.Set("X-Real-Ip", CF_Connecting_IP)
} else if Fastly_Client_IP != "" {
//Use Fastly Client IP
req.Header.Set("X-Real-Ip", Fastly_Client_IP)
} else {
// Not exists. Fill it in with first entry in X-Forwarded-For
ips := strings.Split(clientIP, ",")
if len(ips) > 0 {
req.Header.Set("X-Real-Ip", strings.TrimSpace(ips[0]))
}
}
}
}
}
// injectUserDefinedHeaders inject the user headers from slice
// if a value is empty string, the key will be removed from header.
// if a key is empty string, the function will return immediately
func injectUserDefinedHeaders(header http.Header, userHeaders [][]string) {
for _, userHeader := range userHeaders {
if len(userHeader) == 0 {
//End of header slice
return
}
headerKey := userHeader[0]
headerValue := userHeader[1]
if headerValue == "" {
//Remove header from head
header.Del(headerKey)
continue
}
//Default: Set header value
header.Del(headerKey) //Remove header if it already exists
header.Set(headerKey, headerValue)
}
}

View File

@ -142,6 +142,7 @@ func (router *Router) StartProxyService() error {
OriginalHost: originalHostHeader,
UseTLS: sep.RequireTLS,
PathPrefix: "",
Version: sep.parent.Option.HostVersion,
})
return
}

View File

@ -30,7 +30,6 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
return true
}
}
return false
}
@ -49,16 +48,13 @@ func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
}
// Add a user defined header to the list, duplicates will be automatically removed
func (ep *ProxyEndpoint) AddUserDefinedHeader(key string, value string) error {
if ep.UserDefinedHeaderExists(key) {
ep.RemoveUserDefinedHeader(key)
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *UserDefinedHeader) error {
if ep.UserDefinedHeaderExists(newHeaderRule.Key) {
ep.RemoveUserDefinedHeader(newHeaderRule.Key)
}
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, &UserDefinedHeader{
Key: cases.Title(language.Und, cases.NoLower).String(key), //e.g. x-proxy-by -> X-Proxy-By
Value: value,
})
newHeaderRule.Key = cases.Title(language.Und, cases.NoLower).String(newHeaderRule.Key)
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, newHeaderRule)
return nil
}

View File

@ -111,13 +111,6 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.UserDefinedHeaders) > 0 {
for _, customHeader := range target.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
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
@ -152,12 +145,18 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
r.URL, _ = url.Parse(originalHostHeader)
}
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
NoCache: h.Parent.Option.NoCache,
PathPrefix: "",
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
NoCache: h.Parent.Option.NoCache,
PathPrefix: "",
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
Version: target.parent.Option.HostVersion,
})
var dnsError *net.DNSError
@ -184,13 +183,6 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.parent.UserDefinedHeaders) > 0 {
for _, customHeader := range target.parent.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
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("Zr-Origin-Upgrade", "websocket")
@ -219,11 +211,17 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
r.URL, _ = url.Parse(originalHostHeader)
}
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders()
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
PathPrefix: target.MatchingPath,
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
PathPrefix: target.MatchingPath,
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
Version: target.parent.parent.Option.HostVersion,
})
var dnsError *net.DNSError

View File

@ -72,10 +72,20 @@ type BasicAuthExceptionRule struct {
PathPrefix string
}
// Header injection direction type
type HeaderDirection int
const (
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
)
// User defined headers to add into a proxy endpoint
type UserDefinedHeader struct {
Key string
Value string
Direction HeaderDirection
Key string
Value string
IsRemove bool //Instead of set, remove this key instead
}
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better

View File

@ -42,17 +42,22 @@ SendEmail(
)
*/
func (s *Sender) SendEmail(to string, subject string, content string) error {
//Parse the email content
// 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
//Username can be username (e.g. admin) or email (e.g. admin@example.com), depending on SMTP service provider
auth := smtp.PlainAuth("", s.Username, s.Password, s.Hostname)
// Initialize the auth variable
var auth smtp.Auth
if s.Password != "" {
// Login to the SMTP server
// Username can be username (e.g. admin) or email (e.g. admin@example.com), depending on SMTP service provider
auth = smtp.PlainAuth("", s.Username, s.Password, s.Hostname)
}
// Send the email
err := smtp.SendMail(s.Hostname+":"+strconv.Itoa(s.Port), auth, s.SenderAddr, []string{to}, msg)
if err != nil {
return err

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -9,13 +9,13 @@ import (
"time"
)
//Rewrite url based on proxy root
// Rewrite url based on proxy root (default site)
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
// 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
@ -34,7 +34,7 @@ func IsWebSSHSupported() bool {
return true
}
//Check if a given domain and port is a valid ssh server
// 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)
@ -60,7 +60,7 @@ func IsSSHConnectable(ipOrDomain string, port int) bool {
return string(buf[:7]) == "SSH-2.0"
}
//Check if the port is used by other process or application
// 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)

View File

@ -1,9 +1,10 @@
package tcpprox
package streamproxy
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
@ -22,13 +23,13 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
return
}
portA, err := utils.PostPara(r, "porta")
listenAddr, err := utils.PostPara(r, "listenAddr")
if err != nil {
utils.SendErrorResponse(w, "first address cannot be empty")
return
}
portB, err := utils.PostPara(r, "portb")
proxyAddr, err := utils.PostPara(r, "proxyAddr")
if err != nil {
utils.SendErrorResponse(w, "second address cannot be empty")
return
@ -44,27 +45,17 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
}
}
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")
}
useTCP, _ := utils.PostBool(r, "useTCP")
useUDP, _ := utils.PostBool(r, "useUDP")
//Create the target config
newConfigUUID := m.NewConfig(&ProxyRelayOptions{
Name: name,
PortA: portA,
PortB: portB,
Timeout: timeout,
Mode: modeValue,
Name: name,
ListeningAddr: strings.TrimSpace(listenAddr),
ProxyAddr: strings.TrimSpace(proxyAddr),
Timeout: timeout,
UseTCP: useTCP,
UseUDP: useUDP,
})
js, _ := json.Marshal(newConfigUUID)
@ -80,22 +71,10 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
}
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
}
}
listenAddr, _ := utils.PostPara(r, "listenAddr")
proxyAddr, _ := utils.PostPara(r, "proxyAddr")
useTCP, _ := utils.PostBool(r, "useTCP")
useUDP, _ := utils.PostBool(r, "useUDP")
newTimeoutStr, _ := utils.PostPara(r, "timeout")
newTimeout := -1
@ -108,7 +87,7 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
}
// Call the EditConfig method to modify the configuration
err = m.EditConfig(configUUID, newName, newPortA, newPortB, newMode, newTimeout)
err = m.EditConfig(configUUID, newName, listenAddr, proxyAddr, useTCP, useUDP, newTimeout)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
@ -158,6 +137,7 @@ func (m *Manager) HandleStopProxy(w http.ResponseWriter, r *http.Request) {
}
if !targetProxyConfig.IsRunning() {
targetProxyConfig.Running = false
utils.SendErrorResponse(w, "target proxy service is not running")
return
}
@ -180,6 +160,7 @@ func (m *Manager) HandleRemoveProxy(w http.ResponseWriter, r *http.Request) {
}
if targetProxyConfig.IsRunning() {
targetProxyConfig.Running = false
utils.SendErrorResponse(w, "Service is running")
return
}
@ -209,25 +190,3 @@ func (m *Manager) HandleGetProxyStatus(w http.ResponseWriter, r *http.Request) {
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)
}

View File

@ -0,0 +1,281 @@
package streamproxy
import (
"errors"
"log"
"net"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/database"
)
/*
TCP Proxy
Forward port from one port to another
Also accept active connection and passive
connection
*/
type ProxyRelayOptions struct {
Name string
ListeningAddr string
ProxyAddr string
Timeout int
UseTCP bool
UseUDP bool
}
type ProxyRelayConfig struct {
UUID string //A UUIDv4 representing this config
Name string //Name of the config
Running bool //Status, read only
AutoStart bool //If the service suppose to started automatically
ListeningAddress string //Listening Address, usually 127.0.0.1:port
ProxyTargetAddr string //Proxy target address
UseTCP bool //Enable TCP proxy
UseUDP bool //Enable UDP proxy
Timeout int //Timeout for connection in sec
tcpStopChan chan bool //Stop channel for TCP listener
udpStopChan chan bool //Stop channel for UDP listener
aTobAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from A to B
bToaAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from B to A
udpClientMap sync.Map //map storing the UDP client-server connections
parent *Manager `json:"-"`
}
type Options struct {
Database *database.Database
DefaultTimeout int
AccessControlHandler func(net.Conn) bool
}
type Manager struct {
//Config and stores
Options *Options
Configs []*ProxyRelayConfig
//Realtime Statistics
Connections int //currently connected connect counts
}
func NewStreamProxy(options *Options) *Manager {
options.Database.NewTable("tcprox")
//Load relay configs from db
previousRules := []*ProxyRelayConfig{}
if options.Database.KeyExists("tcprox", "rules") {
options.Database.Read("tcprox", "rules", &previousRules)
}
//Check if the AccessControlHandler is empty. If yes, set it to always allow access
if options.AccessControlHandler == nil {
options.AccessControlHandler = func(conn net.Conn) bool {
//Always allow access
return true
}
}
//Create a new proxy manager for TCP
thisManager := Manager{
Options: options,
Connections: 0,
}
//Inject manager into the rules
for _, rule := range previousRules {
rule.parent = &thisManager
if rule.Running {
//This was previously running. Start it again
log.Println("[Stream Proxy] Resuming stream proxy rule " + rule.Name)
rule.Start()
}
}
thisManager.Configs = previousRules
return &thisManager
}
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
//Generate two zero value for atomic int64
aAcc := atomic.Int64{}
bAcc := atomic.Int64{}
aAcc.Store(0)
bAcc.Store(0)
//Generate a new config from options
configUUID := uuid.New().String()
thisConfig := ProxyRelayConfig{
UUID: configUUID,
Name: config.Name,
ListeningAddress: config.ListeningAddr,
ProxyTargetAddr: config.ProxyAddr,
UseTCP: config.UseTCP,
UseUDP: config.UseUDP,
Timeout: config.Timeout,
tcpStopChan: nil,
udpStopChan: nil,
aTobAccumulatedByteTransfer: aAcc,
bToaAccumulatedByteTransfer: bAcc,
udpClientMap: sync.Map{},
parent: m,
}
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, newListeningAddr string, newProxyAddr string, useTCP bool, useUDP bool, 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 newListeningAddr != "" {
foundConfig.ListeningAddress = newListeningAddr
}
if newProxyAddr != "" {
foundConfig.ProxyTargetAddr = newProxyAddr
}
foundConfig.UseTCP = useTCP
foundConfig.UseUDP = useUDP
if newTimeout != -1 {
if newTimeout < 0 {
return errors.New("invalid timeout value given")
}
foundConfig.Timeout = newTimeout
}
m.SaveConfigToDatabase()
//Check if config is running. If yes, restart it
if foundConfig.IsRunning() {
foundConfig.Restart()
}
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)
}
/*
Config Functions
*/
// Start a proxy if stopped
func (c *ProxyRelayConfig) Start() error {
if c.IsRunning() {
c.Running = true
return errors.New("proxy already running")
}
// Create a stopChan to control the loop
tcpStopChan := make(chan bool)
udpStopChan := make(chan bool)
//Start the proxy service
if c.UseUDP {
c.udpStopChan = udpStopChan
go func() {
err := c.ForwardUDP(c.ListeningAddress, c.ProxyTargetAddr, udpStopChan)
if err != nil {
if !c.UseTCP {
c.Running = false
c.parent.SaveConfigToDatabase()
}
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
}
}()
}
if c.UseTCP {
c.tcpStopChan = tcpStopChan
go func() {
//Default to transport mode
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
if err != nil {
c.Running = false
c.parent.SaveConfigToDatabase()
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
}
}()
}
//Successfully spawned off the proxy routine
c.Running = true
c.parent.SaveConfigToDatabase()
return nil
}
// Return if a proxy config is running
func (c *ProxyRelayConfig) IsRunning() bool {
return c.tcpStopChan != nil || c.udpStopChan != nil
}
// Restart a proxy config
func (c *ProxyRelayConfig) Restart() {
if c.IsRunning() {
c.Stop()
}
time.Sleep(300 * time.Millisecond)
c.Start()
}
// Stop a running proxy if running
func (c *ProxyRelayConfig) Stop() {
log.Println("[STREAM PROXY] Stopping Stream Proxy " + c.Name)
if c.udpStopChan != nil {
log.Println("[STREAM PROXY] Stopping UDP for " + c.Name)
c.udpStopChan <- true
c.udpStopChan = nil
}
if c.tcpStopChan != nil {
log.Println("[STREAM PROXY] Stopping TCP for " + c.Name)
c.tcpStopChan <- true
c.tcpStopChan = nil
}
log.Println("[STREAM PROXY] Stopped Stream Proxy " + c.Name)
c.Running = false
//Update the running status
c.parent.SaveConfigToDatabase()
}

View File

@ -1,10 +1,10 @@
package tcpprox_test
package streamproxy_test
import (
"testing"
"time"
"imuslab.com/zoraxy/mod/tcpprox"
"imuslab.com/zoraxy/mod/streamproxy"
)
func TestPort2Port(t *testing.T) {
@ -12,7 +12,7 @@ func TestPort2Port(t *testing.T) {
stopChan := make(chan bool)
// Create a ProxyRelayConfig with dummy values
config := &tcpprox.ProxyRelayConfig{
config := &streamproxy.ProxyRelayConfig{
Timeout: 1,
}
@ -36,7 +36,7 @@ func TestPort2Port(t *testing.T) {
time.Sleep(1 * time.Second)
// If the goroutine is still running, it means it did not stop as expected
if config.Running {
if config.IsRunning() {
t.Errorf("port2port did not stop as expected")
}

View File

@ -0,0 +1,146 @@
package streamproxy
import (
"errors"
"io"
"log"
"net"
"strconv"
"strings"
"sync"
"sync/atomic"
"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 connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *atomic.Int64) {
n, err := io.Copy(conn1, conn2)
if err != nil {
return
}
accumulator.Add(n) //Add to accumulator
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 *atomic.Int64, bToa *atomic.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 (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
conn, err := listener.Accept()
if err != nil {
return nil, err
}
//Check if connection in blacklist or whitelist
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
if !c.parent.Options.AccessControlHandler(conn) {
time.Sleep(300 * time.Millisecond)
conn.Close()
log.Println("[x]", "Connection from "+addr.IP.String()+" rejected by access control policy")
return nil, errors.New("Connection from " + addr.IP.String() + " rejected by access control policy")
}
}
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
}
/*
Forwarder Functions
*/
/*
portA -> server
server -> portB
*/
func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, stopChan chan bool) error {
listenerStartingAddr := allowPort
if isValidPort(allowPort) {
//number only, e.g. 8080
listenerStartingAddr = "0.0.0.0:" + allowPort
} else if strings.HasPrefix(allowPort, ":") && isValidPort(allowPort[1:]) {
//port number starting with :, e.g. :8080
listenerStartingAddr = "0.0.0.0" + allowPort
}
server, err := startListener(listenerStartingAddr)
if err != nil {
return err
}
targetAddress = strings.TrimSpace(targetAddress)
//Start stop handler
go func() {
<-stopChan
log.Println("[x]", "Received stop signal. Exiting Port to Host forwarder")
server.Close()
}()
//Start blocking loop for accepting connections
for {
conn, err := c.accept(server)
if err != nil {
if errors.Is(err, net.ErrClosed) {
//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)
}
}

View File

@ -0,0 +1,157 @@
package streamproxy
import (
"errors"
"log"
"net"
"strings"
"time"
)
/*
UDP Proxy Module
*/
// Information maintained for each client/server connection
type udpClientServerConn struct {
ClientAddr *net.UDPAddr // Address of the client
ServerConn *net.UDPConn // UDP connection to server
}
// Generate a new connection by opening a UDP connection to the server
func createNewUDPConn(srvAddr, cliAddr *net.UDPAddr) *udpClientServerConn {
conn := new(udpClientServerConn)
conn.ClientAddr = cliAddr
srvudp, err := net.DialUDP("udp", nil, srvAddr)
if err != nil {
return nil
}
conn.ServerConn = srvudp
return conn
}
// Start listener, return inbound lisener and proxy target UDP address
func initUDPConnections(listenAddr string, targetAddress string) (*net.UDPConn, *net.UDPAddr, error) {
// Set up Proxy
saddr, err := net.ResolveUDPAddr("udp", listenAddr)
if err != nil {
return nil, nil, err
}
inboundConn, err := net.ListenUDP("udp", saddr)
if err != nil {
return nil, nil, err
}
log.Println("[UDP] Proxy listening on " + listenAddr)
outboundConn, err := net.ResolveUDPAddr("udp", targetAddress)
if err != nil {
return nil, nil, err
}
return inboundConn, outboundConn, nil
}
// Go routine which manages connection from server to single client
func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lisenter *net.UDPConn) {
var buffer [1500]byte
for {
// Read from server
n, err := conn.ServerConn.Read(buffer[0:])
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
continue
}
// Relay it to client
_, err = lisenter.WriteToUDP(buffer[0:n], conn.ClientAddr)
if err != nil {
continue
}
}
}
// Close all connections that waiting for read from server
func (c *ProxyRelayConfig) CloseAllUDPConnections() {
c.udpClientMap.Range(func(clientAddr, clientServerConn interface{}) bool {
conn := clientServerConn.(*udpClientServerConn)
conn.ServerConn.Close()
return true
})
}
func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan bool) error {
//By default the incoming listen Address is int
//We need to add the loopback address into it
if isValidPort(address1) {
//Port number only. Missing the : in front
address1 = ":" + address1
}
if strings.HasPrefix(address1, ":") {
//Prepend 127.0.0.1 to the address
address1 = "127.0.0.1" + address1
}
lisener, targetAddr, err := initUDPConnections(address1, address2)
if err != nil {
return err
}
go func() {
//Stop channel receiver
for {
select {
case <-stopChan:
//Stop signal received
//Stop server -> client forwarder
c.CloseAllUDPConnections()
//Stop client -> server forwarder
//Force close, will terminate ReadFromUDP for inbound listener
lisener.Close()
return
default:
time.Sleep(100 * time.Millisecond)
}
}
}()
var buffer [1500]byte
for {
n, cliaddr, err := lisener.ReadFromUDP(buffer[0:])
if err != nil {
if errors.Is(err, net.ErrClosed) {
//Proxy stopped
return nil
}
continue
}
c.aTobAccumulatedByteTransfer.Add(int64(n))
saddr := cliaddr.String()
rawConn, found := c.udpClientMap.Load(saddr)
var conn *udpClientServerConn
if !found {
conn = createNewUDPConn(targetAddr, cliaddr)
if conn == nil {
continue
}
c.udpClientMap.Store(saddr, conn)
log.Println("[UDP] Created new connection for client " + saddr)
// Fire up routine to manage new connection
go c.RunUDPConnectionRelay(conn, lisener)
} else {
log.Println("[UDP] Found connection for client " + saddr)
conn = rawConn.(*udpClientServerConn)
}
// Relay to server
_, err = conn.ServerConn.Write(buffer[0:n])
if err != nil {
continue
}
}
}

View File

@ -1,341 +0,0 @@
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 (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
conn, err := listener.Accept()
if err != nil {
return nil, err
}
//Check if connection in blacklist or whitelist
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
if !c.parent.Options.AccessControlHandler(conn) {
time.Sleep(300 * time.Millisecond)
conn.Close()
log.Println("[x]", "Connection from "+addr.IP.String()+" rejected by access control policy")
return nil, errors.New("Connection from " + addr.IP.String() + " rejected by access control policy")
}
}
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) IsRunning() bool {
return c.Running || c.stopChan != 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 := c.accept(listen1)
if err != nil {
if !c.Running {
return nil
}
continue
}
conn2, err := c.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
}
go 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 := c.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
}
}
go forward(host1, host2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
}
return nil
}

View File

@ -1,289 +0,0 @@
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
}

View File

@ -1,185 +0,0 @@
package tcpprox
import (
"errors"
"net"
"github.com/google/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
parent *Manager `json:"-"`
}
type Options struct {
Database *database.Database
DefaultTimeout int
AccessControlHandler func(net.Conn) bool
}
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")
//Load relay configs from db
previousRules := []*ProxyRelayConfig{}
if options.Database.KeyExists("tcprox", "rules") {
options.Database.Read("tcprox", "rules", &previousRules)
}
//Check if the AccessControlHandler is empty. If yes, set it to always allow access
if options.AccessControlHandler == nil {
options.AccessControlHandler = func(conn net.Conn) bool {
//Always allow access
return true
}
}
//Create a new proxy manager for TCP
thisManager := Manager{
Options: options,
Connections: 0,
}
//Inject manager into the rules
for _, rule := range previousRules {
rule.parent = &thisManager
}
thisManager.Configs = previousRules
return &thisManager
}
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
//Generate a new config from options
configUUID := uuid.New().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,
parent: m,
}
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

@ -68,9 +68,9 @@ func PostBool(r *http.Request, key string) (bool, error) {
x = strings.TrimSpace(x)
if x == "1" || strings.ToLower(x) == "true" {
if x == "1" || strings.ToLower(x) == "true" || strings.ToLower(x) == "on" {
return true, nil
} else if x == "0" || strings.ToLower(x) == "false" {
} else if x == "0" || strings.ToLower(x) == "false" || strings.ToLower(x) == "off" {
return false, nil
}

View File

@ -1076,9 +1076,9 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
// Add a new header to the target endpoint
func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
epType, err := utils.PostPara(r, "type")
rewriteType, err := utils.PostPara(r, "type")
if err != nil {
utils.SendErrorResponse(w, "endpoint type not defined")
utils.SendErrorResponse(w, "rewriteType not defined")
return
}
@ -1088,6 +1088,12 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
return
}
direction, err := utils.PostPara(r, "direction")
if err != nil {
utils.SendErrorResponse(w, "HTTP modifiy direction not set")
return
}
name, err := utils.PostPara(r, "name")
if err != nil {
utils.SendErrorResponse(w, "HTTP header name not set")
@ -1095,26 +1101,46 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
}
value, err := utils.PostPara(r, "value")
if err != nil {
if err != nil && rewriteType == "add" {
utils.SendErrorResponse(w, "HTTP header value not set")
return
}
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
if epType == "root" {
targetProxyEndpoint = dynamicProxyRouter.Root
} else {
ep, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
targetProxyEndpoint = ep
//Create a Custom Header Defination type
var rewriteDirection dynamicproxy.HeaderDirection
if direction == "toOrigin" {
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToUpstream
} else if direction == "toClient" {
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToDownstream
} else {
//Unknown direction
utils.SendErrorResponse(w, "header rewrite direction not supported")
return
}
isRemove := false
if rewriteType == "remove" {
isRemove = true
}
headerRewriteDefination := dynamicproxy.UserDefinedHeader{
Key: name,
Value: value,
Direction: rewriteDirection,
IsRemove: isRemove,
}
//Create a new custom header object
targetProxyEndpoint.AddUserDefinedHeader(name, value)
err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefination)
if err != nil {
utils.SendErrorResponse(w, "unable to add header rewrite rule: "+err.Error())
return
}
//Save it (no need reload as header are not handled by dpcore)
err = SaveReverseProxyConfig(targetProxyEndpoint)
@ -1128,12 +1154,6 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
// Remove a header from the target endpoint
func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
epType, err := utils.PostPara(r, "type")
if err != nil {
utils.SendErrorResponse(w, "endpoint type not defined")
return
}
domain, err := utils.PostPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain or matching rule not defined")
@ -1146,20 +1166,17 @@ func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
return
}
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
if epType == "root" {
targetProxyEndpoint = dynamicProxyRouter.Root
} else {
ep, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
targetProxyEndpoint = ep
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
targetProxyEndpoint.RemoveUserDefinedHeader(name)
err = targetProxyEndpoint.RemoveUserDefinedHeader(name)
if err != nil {
utils.SendErrorResponse(w, "unable to remove header rewrite rule: "+err.Error())
return
}
err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil {

View File

@ -23,7 +23,7 @@ import (
"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/streamproxy"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/webserv"
)
@ -229,7 +229,7 @@ func startupSequence() {
webSshManager = sshprox.NewSSHProxyManager()
//Create TCP Proxy Manager
tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
streamProxyManager = streamproxy.NewStreamProxy(&streamproxy.Options{
Database: sysdb,
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
})

View File

@ -65,7 +65,7 @@
<div class="ui form">
<div class="field">
<label>Select Country</label>
<div id="countrySelector" class="ui fluid search selection dropdown">
<div id="countrySelector" class="ui fluid search multiple selection dropdown">
<input type="hidden" name="country">
<i class="dropdown icon"></i>
<div class="default text">Select Country</div>
@ -382,7 +382,7 @@
<div class="ui form">
<div class="field">
<label>Select Country</label>
<div id="countrySelectorWhitelist" class="ui fluid search selection dropdown">
<div id="countrySelectorWhitelist" class="ui fluid search multiple selection dropdown">
<input type="hidden" name="country">
<i class="dropdown icon"></i>
<div class="default text">Select Country</div>
@ -1018,42 +1018,71 @@
function addCountryToBlacklist() {
var countryCode = $("#countrySelector").dropdown("get value").toLowerCase();
$('#countrySelector').dropdown('clear');
$.ajax({
type: "POST",
url: "/api/blacklist/country/add",
data: { cc: countryCode, id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false);
}
initBannedCountryList();
},
error: function(xhr, status, error) {
// handle error response
}
});
}
let ccs = [countryCode];
if (countryCode.includes(",")){
//Multiple country codes selected
//Usually just a few countries a for loop will get the job done
ccs = countryCode.split(",");
}
function removeFromBannedList(countryCode){
if (confirm("Confirm removing " + getCountryName(countryCode) + " from blacklist?")){
countryCode = countryCode.toLowerCase();
let counter = 0;
for(var i = 0; i < ccs.length; i++){
let thisCountryCode = ccs[i];
$.ajax({
url: "/api/blacklist/country/remove",
method: "POST",
data: { cc: countryCode, id: currentEditingAccessRule},
type: "POST",
url: "/api/blacklist/country/add",
data: { cc: thisCountryCode, id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false);
}
initBannedCountryList();
if (counter == (ccs.length - 1)){
//Last item
setTimeout(function(){
initBannedCountryList();
if (ccs.length == 1){
//Single country
msgbox(`Added ${getCountryName(ccs[0])} to blacklist`);
}else{
msgbox(ccs.length + " countries added to blacklist");
}
}, (ccs.length==1)?0:100);
}
counter++;
},
error: function(xhr, status, error) {
console.error("Error removing country from blacklist: " + error);
// Handle error response
// handle error response
}
});
}
$('#countrySelector').dropdown('clear');
}
function removeFromBannedList(countryCode){
countryCode = countryCode.toLowerCase();
let countryName = getCountryName(countryCode);
$.ajax({
url: "/api/blacklist/country/remove",
method: "POST",
data: { cc: countryCode, id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false);
}else{
msgbox(countryName + " removed from blacklist");
}
initBannedCountryList();
},
error: function(xhr, status, error) {
console.error("Error removing country from blacklist: " + error);
// Handle error response
}
});
}
function addIpBlacklist(){
@ -1126,21 +1155,45 @@
function addCountryToWhitelist() {
var countryCode = $("#countrySelectorWhitelist").dropdown("get value").toLowerCase();
$('#countrySelectorWhitelist').dropdown('clear');
$.ajax({
type: "POST",
url: "/api/whitelist/country/add",
data: { cc: countryCode , id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false);
let ccs = [countryCode];
if (countryCode.includes(",")){
//Multiple country codes selected
//Usually just a few countries a for loop will get the job done
ccs = countryCode.split(",");
}
let counter = 0;
for(var i = 0; i < ccs.length; i++){
let thisCountryCode = ccs[i];
$.ajax({
type: "POST",
url: "/api/whitelist/country/add",
data: { cc: thisCountryCode , id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false);
}
if (counter == (ccs.length - 1)){
setTimeout(function(){
initWhitelistCountryList();
if (ccs.length == 1){
//Single country
msgbox(`Added ${getCountryName(ccs[0])} to whitelist`);
}else{
msgbox(ccs.length + " countries added to whitelist");
}
}, (ccs.length==1)?0:100);
}
counter++;
},
error: function(xhr, status, error) {
// handle error response
}
initWhitelistCountryList();
},
error: function(xhr, status, error) {
// handle error response
}
});
});
}
$('#countrySelectorWhitelist').dropdown('clear');
}
function removeFromWhiteList(countryCode){

View File

@ -258,7 +258,7 @@
setTimeout(function(){
//Update the checkbox
msgbox("Proxy Root Updated");
msgbox("Default Site Updated");
}, 100);
})

View File

@ -68,12 +68,13 @@
<div class="standardContainer">
<div class="ui divider"></div>
<h4>Global Settings</h4>
<p>Inbound Port (Port to be proxied)</p>
<p>Inbound Port (Reverse Proxy Listening Port)</p>
<div class="ui action fluid notloopbackOnly input">
<small id="applyButtonReminder">Click "Apply" button to confirm listening port changes</small>
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
<button class="ui basic notloopbackOnly button" onclick="handlePortChange();"><i class="ui green checkmark icon"></i> Apply</button>
<button class="ui green notloopbackOnly button" style="background: linear-gradient(60deg, #27e7ff, #00ca52);" onclick="handlePortChange();"><i class="ui checkmark icon"></i> Apply</button>
</div>
<br>
<br><br>
<div id="tls" class="ui toggle notloopbackOnly checkbox">
<input type="checkbox">
<label>Use TLS to serve proxy request</label>
@ -160,6 +161,7 @@
</div>
<script>
let loopbackProxiedInterface = false;
let currentListeningPort = 80;
$(".advanceSettings").accordion();
//Initial the start stop button if this is reverse proxied
@ -176,6 +178,8 @@
//Get the latest server status from proxy server
function initRPStaste(){
$.get("/api/proxy/status", function(data){
$("#incomingPort").off("change");
if (data.Running == true){
$("#startbtn").addClass("disabled");
if (!loopbackProxiedInterface){
@ -194,6 +198,15 @@
$("#serverstatus").removeClass("green");
}
$("#incomingPort").val(data.Option.Port);
currentListeningPort = data.Option.Port;
$("#incomingPort").on("change", function(){
let newPortValue = $("#incomingPort").val().trim();
if (currentListeningPort != newPortValue){
$("#applyButtonReminder").show();
}else{
$("#applyButtonReminder").hide();
}
});
});
@ -353,8 +366,11 @@
msgbox(data.error, false, 5000);
return;
}
msgbox("Setting Updated");
msgbox("Listening Port Updated");
initRPStaste();
//Hide the reminder text
$("#applyButtonReminder").hide();
});
}

View File

@ -0,0 +1,382 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Stream Proxy</h2>
<p>Proxy traffic flow on layer 3 via TCP or UDP</p>
</div>
<div class="ui divider"></div>
<div class="ui basic segment" style="margin-top: 0;">
<h3>TCP / UDP Proxy Rules</h3>
<p>A list of TCP / UDP proxy rules created on this host.</p>
<div style="overflow-x: auto; ">
<table id="proxyTable" class="ui celled basic unstackable table">
<thead>
<tr>
<th>Name</th>
<th>Listening Address</th>
<th>Target Address</th>
<th>Mode</th>
<th>Timeout (s)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<br>
<button class="ui basic right floated button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i>Refresh</button>
<br><br>
</div>
<div class="ui divider"></div>
<div class="ui basic segment" id="addproxyConfig">
<h3>Add or Edit Stream Proxy</h3>
<p>Create or edit a new stream proxy instance</p>
<form id="streamProxyForm" class="ui form">
<div class="field" style="display:none;">
<label>UUID</label>
<input type="text" name="uuid">
</div>
<div class="field">
<label>Name</label>
<input type="text" name="name" placeholder="Config Name">
</div>
<div class="field">
<label>Listening Address with Port</label>
<input type="text" name="listenAddr" placeholder="">
<small>Address to listen on this host. e.g. :25565 or 127.0.0.1:25565. <br>
If you are using Docker, you will also need to expose this port to host network.</small>
</div>
<div class="field">
<label>Proxy Target Address with Port</label>
<input type="text" name="proxyAddr" placeholder="">
<small>Server address to forward TCP / UDP package. e.g. 192.168.1.100:25565</small>
</div>
<div class="field">
<label>Timeout (s)</label>
<input type="text" name="timeout" placeholder="" value="10">
<small>Connection timeout in seconds</small>
</div>
<Br>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" tabindex="0" name="useTCP" class="hidden">
<label>Enable TCP<br>
<small>Forward TCP request on this listening socket</small>
</label>
</div>
</div>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" tabindex="0" name="useUDP" class="hidden">
<label>Enable UDP<br>
<small>Forward UDP request on this listening socket</small></label>
</div>
</div>
<button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
<button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);" style="display:none;"><i class="ui green check icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
</form>
</div>
</div>
<script>
let editingStreamProxyConfigUUID = ""; //The current editing TCP Proxy config UUID
$("#streamProxyForm .dropdown").dropdown();
$('#streamProxyForm').on('submit', function(event) {
event.preventDefault();
//Check if update mode
if ($("#editStreamProxyButton").is(":visible")){
confirmEditTCPProxyConfig(event);
return;
}
var form = $(this);
var formValid = validateTCPProxyConfig(form);
if (!formValid){
return;
}
// Send the AJAX POST request
$.ajax({
type: 'POST',
url: '/api/streamprox/config/add',
data: form.serialize(),
success: function(response) {
if (response.error) {
msgbox(response.error, false, 6000);
}else{
msgbox("Config Added");
}
clearStreamProxyAddEditForm();
initProxyConfigList();
},
error: function() {
msgbox('An error occurred while processing the request', false);
}
});
});
function clearStreamProxyAddEditForm(){
$('#streamProxyForm input, #streamProxyForm select').val('');
$('#streamProxyForm select').dropdown('clear');
$("#streamProxyForm input[name=timeout]").val(10);
}
function cancelStreamProxyEdit(event=undefined) {
clearStreamProxyAddEditForm();
$("#addStreamProxyButton").show();
$("#editStreamProxyButton").hide();
}
function validateTCPProxyConfig(form){
//Check if name is filled. If not, generate a random name for it
var name = form.find('input[name="name"]').val()
if (name == ""){
let randomName = "Proxy Rule (#" + Math.round(Date.now()/1000) + ")";
form.find('input[name="name"]').val(randomName);
}
// Validate timeout is an integer
var timeout = parseInt(form.find('input[name="timeout"]').val());
if (form.find('input[name="timeout"]').val() == ""){
//Not set. Assign a random one to it
form.find('input[name="timeout"]').val("10");
timeout = 10;
}
if (isNaN(timeout)) {
form.find('input[name="timeout"]').parent().addClass("error");
msgbox('Timeout must be a valid integer', false, 5000);
return false;
}else{
form.find('input[name="timeout"]').parent().removeClass("error");
}
// Validate mode is selected
var mode = form.find('select[name="mode"]').val();
if (mode === '') {
form.find('select[name="mode"]').parent().addClass("error");
msgbox('Please select a mode', false, 5000);
return false;
}else{
form.find('select[name="mode"]').parent().removeClass("error");
}
return true;
}
function renderProxyConfigs(proxyConfigs) {
var tableBody = $('#proxyTable tbody');
tableBody.empty();
if (proxyConfigs.length === 0) {
var noResultsRow = $('<tr><td colspan="7"><i class="green check circle icon"></i>No Proxy Configs</td></tr>');
tableBody.append(noResultsRow);
} else {
proxyConfigs.forEach(function(config) {
var runningLogo = 'Stopped';
var runningClass = "stopped";
var startButton = `<button onclick="startStreamProx('${config.UUID}');" class="ui basic mini circular icon button" title="Start Proxy"><i class="green play icon"></i></button>`;
if (config.Running){
runningLogo = 'Running';
startButton = `<button onclick="stopStreamProx('${config.UUID}');" class="ui basic mini circular icon button" title="Stop Proxy"><i class="red stop icon"></i></button>`;
runningClass = "running"
}
var modeText = [];
if (config.UseTCP){
modeText.push("TCP")
}
if (config.UseUDP){
modeText.push("UDP")
}
modeText = modeText.join(" & ")
var thisConfig = encodeURIComponent(JSON.stringify(config));
var row = $(`<tr class="streamproxConfig ${runningClass}" uuid="${config.UUID}" config="${thisConfig}">`);
row.append($('<td>').html(`
${config.Name}
<div class="statusText">${runningLogo}</div>`));
row.append($('<td>').text(config.ListeningAddress));
row.append($('<td>').text(config.ProxyTargetAddr));
row.append($('<td>').text(modeText));
row.append($('<td>').text(config.Timeout));
row.append($('<td>').html(`
${startButton}
<button onclick="editTCPProxyConfig('${config.UUID}');" class="ui circular basic mini icon button" title="Edit Config"><i class="edit icon"></i></button>
<button onclick="deleteTCPProxyConfig('${config.UUID}');" class="ui circular red basic mini icon button" title="Delete Config"><i class="trash icon"></i></button>
`));
tableBody.append(row);
});
}
}
function getConfigDetailsFromDOM(configUUID){
let thisConfig = null;
$(".streamproxConfig").each(function(){
let uuid = $(this).attr("uuid");
if (configUUID == uuid){
//This is the one we are looking for
thisConfig = JSON.parse(decodeURIComponent($(this).attr("config")));
}
});
return thisConfig;
}
function editTCPProxyConfig(configUUID){
let targetConfig = getConfigDetailsFromDOM(configUUID);
if (targetConfig != null){
$("#addStreamProxyButton").hide();
$("#editStreamProxyButton").show();
$.each(targetConfig, function(key, value) {
var field;
if (key == "UseTCP"){
let checkboxEle = $("#streamProxyForm input[name=useTCP]").parent();
if (value === true){
$(checkboxEle).checkbox("set checked");
}else{
$(checkboxEle).checkbox("set unchecked");
}
return;
}else if (key == "UseUDP"){
let checkboxEle = $("#streamProxyForm input[name=useUDP]").parent();
if (value === true){
$(checkboxEle).checkbox("set checked");
}else{
$(checkboxEle).checkbox("set unchecked");
}
return;
}else if (key == "ListeningAddress"){
field = $("#streamProxyForm input[name=listenAddr]");
}else if (key == "ProxyTargetAddr"){
field = $("#streamProxyForm input[name=proxyAddr]");
}else if (key == "UUID"){
field = $("#streamProxyForm input[name=uuid]");
}else if (key == "Name"){
field = $("#streamProxyForm input[name=name]");
}else if (key == "Timeout"){
field = $("#streamProxyForm input[name=timeout]");
}
if (field != undefined && field.length > 0) {
field.val(value);
}
});
editingStreamProxyConfigUUID = configUUID;
}else{
msgbox("Unable to load target config", false);
}
}
function confirmEditTCPProxyConfig(event){
event.preventDefault();
event.stopImmediatePropagation();
var form = $("#streamProxyForm");
var formValid = validateTCPProxyConfig(form);
if (!formValid){
return;
}
// Send the AJAX POST request
$.ajax({
type: 'POST',
url: '/api/streamprox/config/edit',
method: "POST",
data: {
uuid: $("#streamProxyForm input[name=uuid]").val().trim(),
name: $("#streamProxyForm input[name=name]").val().trim(),
listenAddr: $("#streamProxyForm input[name=listenAddr]").val().trim(),
proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(),
useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked ,
useUDP: $("#streamProxyForm input[name=useUDP]")[0].checked ,
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
},
success: function(response) {
if (response.error) {
msgbox(response.error, false, 6000);
}else{
msgbox("Config Updated");
}
initProxyConfigList();
cancelStreamProxyEdit();
},
error: function() {
msgbox('An error occurred while processing the request', false);
}
});
}
function deleteTCPProxyConfig(configUUID){
$.ajax({
url: "/api/streamprox/config/delete",
method: "POST",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Proxy Config Removed");
initProxyConfigList();
}
}
})
}
//Start a TCP proxy by their config UUID
function startStreamProx(configUUID){
$.ajax({
url: "/api/streamprox/config/start",
method: "POST",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Service Started");
initProxyConfigList();
}
}
});
}
//Stop a TCP proxy by their config UUID
function stopStreamProx(configUUID){
$.ajax({
url: "/api/streamprox/config/stop",
method: "POST",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Service Stopped");
initProxyConfigList();
}
}
});
}
function initProxyConfigList(){
$.ajax({
type: 'GET',
url: '/api/streamprox/config/list',
success: function(response) {
renderProxyConfigs(response);
},
error: function() {
msgbox('Unable to load proxy configs', false);
}
});
}
initProxyConfigList();
</script>
</div>

View File

@ -1,431 +0,0 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>TCP Proxy</h2>
<p>Proxy traffic flow on layer 3 via TCP/IP</p>
</div>
<div class="ui divider"></div>
<div class="ui basic segment" style="margin-top: 0;">
<h4>TCP Proxy Rules</h4>
<p>A list of TCP proxy rules created on this host. To enable them, use the toggle button on the right.</p>
<div style="overflow-x: auto; min-height: 400px;">
<table id="proxyTable" class="ui celled unstackable table">
<thead>
<tr>
<th>Name</th>
<th>Port/Addr A</th>
<th>Port/Addr B</th>
<th>Mode</th>
<th>Timeout (s)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<button class="ui basic right floated button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i>Refresh</button>
<br><br>
</div>
<div class="ui divider"></div>
<div class="ui basic segment" id="addproxyConfig">
<h4>Add or Edit TCP Proxy</h4>
<p>Create or edit a new proxy instance</p>
<form id="tcpProxyForm" class="ui form">
<div class="field" style="display:none;">
<label>UUID</label>
<input type="text" name="uuid">
</div>
<div class="field">
<label>Name</label>
<input type="text" name="name" placeholder="Config Name">
</div>
<div class="field">
<label>Port A</label>
<input type="text" name="porta" placeholder="First address or port">
</div>
<div class="field">
<label>Port B</label>
<input type="text" name="portb" placeholder="Second address or port">
</div>
<div class="field">
<label>Timeout (s)</label>
<input type="text" name="timeout" placeholder="Timeout (s)">
</div>
<div class="field">
<label>Mode</label>
<select name="mode" class="ui dropdown">
<option value="">Select Mode</option>
<option value="listen">Listen</option>
<option value="transport">Transport</option>
<option value="starter">Starter</option>
</select>
</div>
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);" style="display:none;"><i class="ui green check icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
<div class="ui basic inverted segment" style="background: var(--theme_background_inverted); border-radius: 0.6em;">
<p>TCP Proxy support the following TCP sockets proxy modes</p>
<table class="ui celled padded inverted basic table">
<thead>
<tr><th class="single line">Mode</th>
<th>Public-IP</th>
<th>Concurrent Access</th>
<th>Flow Diagram</th>
</tr></thead>
<tbody>
<tr>
<td>
<h4 class="ui center aligned inverted header">Transport</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui green check icon"></i> (or same LAN)<br>
</td>
<td>
<i class="ui green check icon"></i>
</td>
<td>Port A (e.g. 25565) <i class="arrow right icon"></i> Server<br>
Server <i class="arrow right icon"></i> Port B (e.g. 192.168.0.2:25565)<br>
<small>Traffic from Port A will be forward to Port B's (IP if provided and) Port</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Listen</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui remove icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Port A (e.g. 8080) <i class="arrow right icon"></i> Server<br>
Port B (e.g. 8081) <i class="arrow right icon"></i> Server<br>
<small>Server will act as a bridge to proxy traffic between Port A and B</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Starter</h4>
</td>
<td class="single line">
Server: <i class="ui times icon"></i><br>
A: <i class="ui green check icon"></i><br>
B: <i class="ui green check icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Server <i class="arrow right icon"></i> Port A (e.g. remote.local.:8080) <br>
Server <i class="arrow right icon"></i> Port B (e.g. recv.local.:8081) <br>
<small>Port A and B will be actively bridged</small>
</td>
</tr>
</tbody>
</table>
</div>
</form>
</div>
</div>
<script>
let editingTCPProxyConfigUUID = ""; //The current editing TCP Proxy config UUID
$("#tcpProxyForm .dropdown").dropdown();
$('#tcpProxyForm').on('submit', function(event) {
event.preventDefault();
//Check if update mode
if ($("#editTcpProxyButton").is(":visible")){
confirmEditTCPProxyConfig(event);
return;
}
var form = $(this);
var formValid = validateTCPProxyConfig(form);
if (!formValid){
return;
}
// Send the AJAX POST request
$.ajax({
type: 'POST',
url: '/api/tcpprox/config/add',
data: form.serialize(),
success: function(response) {
if (response.error) {
msgbox(response.error, false, 6000);
}else{
msgbox("Config Added");
}
clearTCPProxyAddEditForm();
initProxyConfigList();
$("#addproxyConfig").slideUp("fast");
},
error: function() {
msgbox('An error occurred while processing the request', false);
}
});
});
function clearTCPProxyAddEditForm(){
$('#tcpProxyForm input, #tcpProxyForm select').val('');
$('#tcpProxyForm select').dropdown('clear');
}
function cancelTCPProxyEdit(event=undefined) {
clearTCPProxyAddEditForm();
$("#addTcpProxyButton").show();
$("#editTcpProxyButton").hide();
}
function validateTCPProxyConfig(form){
//Check if name is filled. If not, generate a random name for it
var name = form.find('input[name="name"]').val()
if (name == ""){
let randomName = "Proxy Rule (#" + Math.round(Date.now()/1000) + ")";
form.find('input[name="name"]').val(randomName);
}
// Validate timeout is an integer
var timeout = parseInt(form.find('input[name="timeout"]').val());
if (form.find('input[name="timeout"]').val() == ""){
//Not set. Assign a random one to it
form.find('input[name="timeout"]').val("10");
timeout = 10;
}
if (isNaN(timeout)) {
form.find('input[name="timeout"]').parent().addClass("error");
msgbox('Timeout must be a valid integer', false, 5000);
return false;
}else{
form.find('input[name="timeout"]').parent().removeClass("error");
}
// Validate mode is selected
var mode = form.find('select[name="mode"]').val();
if (mode === '') {
form.find('select[name="mode"]').parent().addClass("error");
msgbox('Please select a mode', false, 5000);
return false;
}else{
form.find('select[name="mode"]').parent().removeClass("error");
}
return true;
}
function renderProxyConfigs(proxyConfigs) {
var tableBody = $('#proxyTable tbody');
tableBody.empty();
if (proxyConfigs.length === 0) {
var noResultsRow = $('<tr><td colspan="7"><i class="green check circle icon"></i>No Proxy Configs</td></tr>');
tableBody.append(noResultsRow);
} else {
proxyConfigs.forEach(function(config) {
var runningLogo = 'Stopped';
var runningClass = "stopped";
var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="green play icon"></i> Start Proxy</button>`;
if (config.Running){
runningLogo = 'Running';
startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`;
runningClass = "running"
}
var modeText = "Unknown";
if (config.Mode == 0){
modeText = "Listen";
}else if (config.Mode == 1){
modeText = "Transport";
}else if (config.Mode == 2){
modeText = "Starter";
}
var thisConfig = encodeURIComponent(JSON.stringify(config));
var row = $(`<tr class="tcproxConfig ${runningClass}" uuid="${config.UUID}" config="${thisConfig}">`);
row.append($('<td>').html(`
${config.Name}
<div class="statusText">${runningLogo}</div>`));
row.append($('<td>').text(config.PortA));
row.append($('<td>').text(config.PortB));
row.append($('<td>').text(modeText));
row.append($('<td>').text(config.Timeout));
row.append($('<td>').html(`
<div class="ui basic vertical fluid tiny buttons">
<button class="ui button" onclick="validateProxyConfig('${config.UUID}', this);" title="Validate Config"><i class="teal question circle outline icon"></i> CXN Test</button>
${startButton}
<button onclick="editTCPProxyConfig('${config.UUID}');" class="ui button" title="Edit Config"><i class="edit icon"></i> Edit </button>
<button onclick="deleteTCPProxyConfig('${config.UUID}');" class="ui red basic button" title="Delete Config"><i class="trash icon"></i> Remove</button>
</div>
`));
tableBody.append(row);
});
}
}
function getConfigDetailsFromDOM(configUUID){
let thisConfig = null;
$(".tcproxConfig").each(function(){
let uuid = $(this).attr("uuid");
if (configUUID == uuid){
//This is the one we are looking for
thisConfig = JSON.parse(decodeURIComponent($(this).attr("config")));
}
});
return thisConfig;
}
function validateProxyConfig(configUUID, btn){
$(btn).html(`<i class="ui loading spinner icon"></i>`);
$.ajax({
url: "/api/tcpprox/config/validate",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
let errormsg = data.error.charAt(0).toUpperCase() + data.error.slice(1);
$(btn).html(`<i class="red times icon"></i> ${errormsg}`);
msgbox(data.error, false, 6000);
}else{
$(btn).html(`<i class="green check icon"></i> Config Valid`);
msgbox("Config Check Passed");
}
}
})
}
function editTCPProxyConfig(configUUID){
let targetConfig = getConfigDetailsFromDOM(configUUID);
if (targetConfig != null){
$("#addTcpProxyButton").hide();
$("#editTcpProxyButton").show();
$.each(targetConfig, function(key, value) {
var field = $("#tcpProxyForm").find('[name="' + key.toLowerCase() + '"]');
if (field.length > 0) {
if (field.is('input')) {
field.val(value);
}else if (field.is('select')){
if (key.toLowerCase() == "mode"){
if (value == 0){
value = "listen";
}else if (value == 1){
value = "transport";
}else if (value == 2){
value = "starter";
}
}
$(field).dropdown("set selected", value);
}
}
});
editingTCPProxyConfigUUID = configUUID;
$("#addproxyConfig").slideDown("fast");
}else{
msgbox("Unable to load target config", false);
}
}
function confirmEditTCPProxyConfig(event){
event.preventDefault();
event.stopImmediatePropagation();
var form = $("#tcpProxyForm");
var formValid = validateTCPProxyConfig(form);
if (!formValid){
return;
}
// Send the AJAX POST request
$.ajax({
type: 'POST',
url: '/api/tcpprox/config/edit',
data: form.serialize(),
success: function(response) {
if (response.error) {
msgbox(response.error, false, 6000);
}else{
msgbox("Config Updated");
}
initProxyConfigList();
cancelTCPProxyEdit();
},
error: function() {
msgbox('An error occurred while processing the request', false);
}
});
}
function deleteTCPProxyConfig(configUUID){
$.ajax({
url: "/api/tcpprox/config/delete",
method: "POST",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Proxy Config Removed");
initProxyConfigList();
}
}
})
}
//Start a TCP proxy by their config UUID
function startTcpProx(configUUID){
$.ajax({
url: "/api/tcpprox/config/start",
method: "POST",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Service Started");
initProxyConfigList();
}
}
});
}
//Stop a TCP proxy by their config UUID
function stopTcpProx(configUUID){
$.ajax({
url: "/api/tcpprox/config/stop",
method: "POST",
data: {uuid: configUUID},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Service Stopped");
initProxyConfigList();
}
}
});
}
function initProxyConfigList(){
$.ajax({
type: 'GET',
url: '/api/tcpprox/config/list',
success: function(response) {
renderProxyConfigs(response);
},
error: function() {
msgbox('Unable to load proxy configs', false);
}
});
}
initProxyConfigList();
</script>
</div>

View File

@ -217,6 +217,7 @@
$("#zoraxyinfo .uuid").text(data.NodeUUID);
$("#zoraxyinfo .development").text(data.Development?"Development":"Release");
$("#zoraxyinfo .version").text(data.Version);
$(".zrversion").text("v." + data.Version); //index footer
$("#zoraxyinfo .boottime").text(timeConverter(data.BootTime) + ` ( ${secondsToDhms(parseInt(Date.now()/1000) - data.BootTime)} ago)`);
$("#zoraxyinfo .zt").html(data.ZerotierConnected?`<i class="ui green check icon"></i> Connected`:`<i class="ui red times icon"></i> Link Error`);
$("#zoraxyinfo .sshlb").html(data.EnableSshLoopback?`<i class="ui yellow exclamation triangle icon"></i> Enabled`:`Disabled`);
@ -341,15 +342,6 @@
form.find('input[name="username"]').parent().removeClass('error');
}
// validate password
const password = form.find('input[name="password"]').val().trim();
if (password === '') {
form.find('input[name="password"]').parent().addClass('error');
isValid = false;
} else {
form.find('input[name="password"]').parent().removeClass('error');
}
// validate sender address
const senderAddr = form.find('input[name="senderAddr"]').val().trim();
if (!emailRegex.test(senderAddr)) {

1512
src/web/img/logo_white.ai Normal file

File diff suppressed because it is too large Load Diff

BIN
src/web/img/logo_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="600px" height="200px" viewBox="0 0 600 200" enable-background="new 0 0 600 200" xml:space="preserve">
<g>
<path fill="#EFEFEF" d="M138.761,47.403l-16.064,17.87c9.504,8.549,15.48,20.94,15.48,34.728c0,13.785-5.976,26.179-15.48,34.726
l16.063,17.871c14.393-12.945,23.445-31.717,23.445-52.597C162.206,79.115,153.155,60.351,138.761,47.403z"/>
<path fill="#EFEFEF" d="M44.198,152.596l16.064-17.869c-9.503-8.547-15.48-20.941-15.48-34.726c0-13.79,5.978-26.179,15.48-34.728
l-16.063-17.87C29.807,60.351,20.753,79.115,20.753,100C20.753,120.881,29.807,139.652,44.198,152.596z"/>
</g>
<polygon fill="#A9D1F3" points="106.581,38.326 91.48,56.48 76.38,38.326 "/>
<polygon fill="#A9D1F3" points="106.581,143.52 91.48,161.674 76.379,143.52 "/>
<circle fill="#A9D1F3" cx="91.48" cy="100" r="22.422"/>
<g>
<path fill="#F7F7F7" d="M194.194,132.898l43.232-66.846h-39.238V54.539h56.155v8.224l-43.233,66.729h43.703v11.629h-60.619V132.898
z"/>
<path fill="#F7F7F7" d="M263.038,108.814c0-21.499,14.45-33.951,30.544-33.951c15.977,0,30.31,12.452,30.31,33.951
c0,21.498-14.333,33.951-30.31,33.951C277.488,142.766,263.038,130.313,263.038,108.814z M310.029,108.814
c0-13.627-6.344-22.791-16.447-22.791c-10.221,0-16.564,9.164-16.564,22.791c0,13.744,6.344,22.674,16.564,22.674
C303.686,131.488,310.029,122.559,310.029,108.814z"/>
<path fill="#F7F7F7" d="M339.869,76.391h11.042l1.176,11.629h0.234c4.582-8.223,11.396-13.156,18.444-13.156
c3.173,0,5.169,0.471,7.166,1.293l-2.35,11.863c-2.349-0.704-3.877-1.057-6.578-1.057c-5.287,0-11.632,3.643-15.626,13.981v40.177
h-13.509V76.391z"/>
<path fill="#F7F7F7" d="M380.868,123.969c0-13.98,11.748-21.146,38.649-24.082c-0.115-7.402-2.819-13.98-12.334-13.98
c-6.813,0-13.158,3.056-18.68,6.578l-5.052-9.162c6.696-4.23,15.742-8.459,26.08-8.459c16.096,0,23.497,10.104,23.497,27.374
v38.884h-11.044l-1.058-7.4h-0.469c-5.875,5.051-12.806,9.045-20.56,9.045C388.739,142.766,380.868,135.365,380.868,123.969z
M419.518,124.322V108.58c-19.147,2.23-25.61,7.166-25.61,14.332c0,6.461,4.348,9.047,10.104,9.047
C409.649,131.959,414.231,129.256,419.518,124.322z"/>
<path fill="#F7F7F7" d="M464.63,107.405l-19.383-31.015h14.686l7.636,13.039c1.996,3.643,3.995,7.285,6.109,10.927h0.587
c1.645-3.642,3.406-7.284,5.287-10.927l6.813-13.039h14.099l-19.386,32.424l20.795,32.307h-14.685l-8.459-13.744
c-2.115-3.76-4.346-7.754-6.697-11.396h-0.586c-1.997,3.643-3.995,7.52-5.992,11.396l-7.518,13.744h-14.098L464.63,107.405z"/>
<path fill="#F7F7F7" d="M508.096,166.85l2.586-10.574c1.176,0.354,3.054,0.939,4.815,0.939c6.932,0,11.045-5.168,13.394-12.1
l1.41-4.463l-25.611-64.262h13.746l11.865,33.363c1.996,5.758,3.993,12.1,5.991,18.209h0.587
c1.645-5.992,3.406-12.334,5.053-18.209l10.456-33.363h13.038l-23.73,68.607c-5.051,13.863-11.865,23.143-25.375,23.143
C512.914,168.141,510.329,167.672,508.096,166.85z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -52,8 +52,8 @@
<a class="item" tag="rules">
<i class="simplistic plus square icon"></i> Create Proxy Rules
</a>
<a class="item" tag="tcpprox">
<i class="simplistic exchange icon"></i> TCP Proxy
<a class="item" tag="streamproxy">
<i class="simplistic exchange icon"></i> Stream Proxy
</a>
<div class="ui divider menudivider">Access & Connections</div>
<a class="item" tag="cert">
@ -125,7 +125,7 @@
<div id="zgrok" class="functiontab" target="zgrok.html"></div>
<!-- TCP Proxy -->
<div id="tcpprox" class="functiontab" target="tcpprox.html"></div>
<div id="streamproxy" class="functiontab" target="streamprox.html"></div>
<!-- Web Server -->
<div id="webserv" class="functiontab" target="webserv.html"></div>
@ -154,7 +154,7 @@
<br><br>
<div class="ui divider"></div>
<div class="ui container" style="color: grey; font-size: 90%">
<p>CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
<p><a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a> <span class="zrversion"></span> © 2021 - <span class="year"></span> tobychui. Licensed under AGPL</p>
</div>
<div id="messageBox" class="ui green floating big compact message">

View File

@ -509,6 +509,16 @@ body{
}
}
/* Remind message for user forgetting to click Apply button*/
#applyButtonReminder{
position: absolute;
bottom:-1.6em;
left: 0;
font-weight: bolder;
color: #faac26;
display:none;
}
/*
HTTP Proxy & Virtual Directory
*/
@ -551,23 +561,23 @@ body{
TCP Proxy
*/
.tcproxConfig td:first-child{
.streamproxConfig td:first-child{
position: relative;
}
.tcproxConfig.running td:first-child{
border-left: 0.6em solid #02cb59 !important;
.streamproxConfig.running td:first-child{
border-left: 0.6em solid #01cb55 !important;
}
.tcproxConfig.stopped td:first-child{
border-left: 0.6em solid #02032a !important;
.streamproxConfig.stopped td:first-child{
border-left: 0.6em solid #1b1b1b !important;
}
.tcproxConfig td:first-child .statusText{
.streamproxConfig td:first-child .statusText{
position: absolute;
bottom: 0.3em;
left: 0.2em;
font-size: 1.4em;
bottom: 0.1em;
right: 0.2em;
font-size: 1em;
color:rgb(224, 224, 224);
opacity: 0.7;
pointer-events: none;

View File

@ -23,7 +23,7 @@
<table class="ui very basic compacted unstackable celled table">
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>Value</th>
<th>Remove</th>
</tr></thead>
@ -34,18 +34,29 @@
</tbody>
</table>
<div class="ui divider"></div>
<h4>Add Custom Header</h4>
<p>Add custom header(s) into this proxy target</p>
<h4>Edit Custom Header</h4>
<p>Add or remove custom header(s) over this proxy target</p>
<div class="scrolling content ui form">
<div class="three small fields credentialEntry">
<div class="field">
<input id="headerName" type="text" placeholder="X-Custom-Header" autocomplete="off">
<div class="five small fields credentialEntry">
<div class="field" align="center">
<button id="toOriginButton" title="Downstream to Upstream" class="ui circular basic active button">Zoraxy <i class="angle double right blue icon" style="margin-right: 0.4em;"></i> Origin</button>
<button id="toClientButton" title="Upstream to Downstream" class="ui circular basic button">Client <i class="angle double left orange icon" style="margin-left: 0.4em;"></i> Zoraxy</button>
</div>
<div class="field" align="center">
<button id="headerModeAdd" class="ui circular basic active button"><i class="ui green circle add icon"></i> Add Header</button>
<button id="headerModeRemove" class="ui circular basic button"><i class="ui red circle times icon"></i> Remove Header</button>
</div>
<div class="field">
<label>Header Key</label>
<input id="headerName" type="text" placeholder="X-Custom-Header" autocomplete="off">
<small>The header key is <b>NOT</b> case sensitive</small>
</div>
<div class="field">
<label>Header Value</label>
<input id="headerValue" type="text" placeholder="value1,value2,value3" autocomplete="off">
</div>
<div class="field" >
<button class="ui basic button" onclick="addCustomHeader();"><i class="green add icon"></i> Add Header</button>
<button class="ui basic button" onclick="addCustomHeader();"><i class="green add icon"></i> Add Header Rewrite Rule</button>
</div>
<div class="ui divider"></div>
</div>
@ -75,6 +86,48 @@
parent.hideSideWrapper(true);
}
//Bind events to header mod mode
$("#headerModeAdd").on("click", function(){
$("#headerModeAdd").addClass("active");
$("#headerModeRemove").removeClass("active");
$("#headerValue").parent().show();
});
$("#headerModeRemove").on("click", function(){
$("#headerModeAdd").removeClass("active");
$("#headerModeRemove").addClass("active");
$("#headerValue").parent().hide();
$("#headerValue").val("");
});
//Bind events to header directions option
$("#toOriginButton").on("click", function(){
$("#toOriginButton").addClass("active");
$("#toClientButton").removeClass("active");
});
$("#toClientButton").on("click", function(){
$("#toOriginButton").removeClass("active");
$("#toClientButton").addClass("active");
});
//Return "add" or "remove" depending on mode user selected
function getHeaderEditMode(){
if ($("#headerModeAdd").hasClass("active")){
return "add";
}
return "remove";
}
//Return "toOrigin" or "toClient"
function getHeaderDirection(){
if ($("#toOriginButton").hasClass("active")){
return "toOrigin";
}
return "toClient";
}
//$("#debug").text(JSON.stringify(editingEndpoint));
function addCustomHeader(){
@ -88,18 +141,21 @@
$("#headerName").parent().removeClass("error");
}
if (value == ""){
$("#headerValue").parent().addClass("error");
return
}else{
$("#headerValue").parent().removeClass("error");
if (getHeaderEditMode() == "add"){
if (value == ""){
$("#headerValue").parent().addClass("error");
return
}else{
$("#headerValue").parent().removeClass("error");
}
}
$.ajax({
url: "/api/proxy/header/add",
data: {
"type": editingEndpoint.ept,
"type": getHeaderEditMode(),
"domain": editingEndpoint.ep,
"direction":getHeaderDirection(),
"name": name,
"value": value
},
@ -129,7 +185,7 @@
$.ajax({
url: "/api/proxy/header/remove",
data: {
"type": editingEndpoint.ept,
//"type": editingEndpoint.ept,
"domain": editingEndpoint.ep,
"name": name,
},
@ -157,10 +213,16 @@
$("#headerTable").html("");
data.forEach(header => {
let editModeIcon = header.IsRemove?`<i class="ui red times circle icon"></i>`:`<i class="ui green add circle icon"></i>`;
let direction = (header.Direction==0)?`<i class="angle double right blue icon"></i>`:`<i class="angle double left orange icon"></i>`;
let valueField = header.Value;
if (header.IsRemove){
valueField = "<small style='color: grey;'>(Field Removed)</small>";
}
$("#headerTable").append(`
<tr>
<td>${header.Key}</td>
<td>${header.Value}</td>
<td>${direction} ${header.Key}</td>
<td>${editModeIcon} ${valueField}</td>
<td><button class="ui basic circular mini red icon button" onclick="deleteCustomHeader('${header.Key}');"><i class="ui trash icon"></i></button></td>
</tr>
`);

10
tools/update_acmedns.sh Normal file
View File

@ -0,0 +1,10 @@
# /bin/sh
# Build the acmedns
echo "Building ACMEDNS"
cd ../tools/dns_challenge_update/code-gen
./update.sh
cd ../../../
cp ./tools/dns_challenge_update/code-gen/acmedns/acmedns.go ./src/mod/acme/acmedns/acmedns.go
cp ./tools/dns_challenge_update/code-gen/acmedns/providers.json ./src/mod/acme/acmedns/providers.json

34
tools/update_geodb.sh Normal file
View File

@ -0,0 +1,34 @@
#/bin/bash
cd ../src/mod/geodb
# Delete the old csv files
rm geoipv4.csv
rm geoipv6.csv
echo "Updating geodb csv files"
echo "Downloading IPv4 database"
curl -f https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv4.csv -o geoipv4.csv
if [ $? -ne 0 ]; then
echo "Failed to download IPv4 database"
failed=true
else
echo "Successfully downloaded IPv4 database"
fi
echo "Downloading IPv6 database"
curl -f https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv6.csv -o geoipv6.csv
if [ $? -ne 0 ]; then
echo "Failed to download IPv6 database"
failed=true
else
echo "Successfully downloaded IPv6 database"
fi
if [ "$failed" = true ]; then
echo "One or more downloads failed. Blocking exit..."
while :; do
read -p "Press [Ctrl+C] to exit..." input
done
fi