29 Commits
3.0.4 ... 3.0.6

Author SHA1 Message Date
83536a83f7 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
2024-06-10 16:32:39 +08:00
1183b0ed55 Finalized v3.0.6 changes
- Updated geodb database
- Updated custom header UI
- Added tools for update acmedns and geodb
2024-06-10 15:36:20 +08:00
b00e302f6d Added new custom header structure
+ 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 #80
+ Completed stream proxy module (TCP and UDP)
+ Optimized UX for reminding user to click Apply after port change
+ Added version number to footer #160
2024-06-09 22:49:35 +08:00
deddb17803 Updated Stream Proxy module
- Fixed stream proxy stopping racing condition bug
- Merged PR #187
- Updated stream proxy UI
2024-06-08 00:33:29 +08:00
aa96d831e1 Merge pull request #187 from Kirari04/main
fix missing / unnecessary error check
2024-06-07 01:14:14 +08:00
c6f7f37aaf Added stream proxy UDP support
+ Added UDP support #147 (wip)
+ Updated structure for proxy storage
+ Renamed TCPprox module to streamproxy
+ Added multi selection for white / blacklist #176
2024-06-07 01:12:42 +08:00
63f12dedcf fix missing / unnecessary error check 2024-06-06 18:13:02 +02:00
136d1ecafb init commit
+ Added fastly_client_ip to X-Real-IP auto rewrite
+ Updated header rewrite data structure (wip)
+ Added atomic accumulator to TCP proxy
+ Added wip UDP proxy
+ Added white logo for future dark theme
2024-06-06 11:30:16 +08:00
7193defad7 Merge branch 'main' of https://github.com/tobychui/zoraxy 2024-06-05 23:24:25 +08:00
cf4c57298e Update index.html
Updated index slogan
2024-06-05 23:24:11 +08:00
d82a531a41 Update README.md
Added itsvmn's getting start blog post
2024-05-31 12:43:07 +08:00
7694e317f7 Merge pull request #175 from Morethanevil/main
Update CHANGELOG.md
2024-05-26 13:57:12 +08:00
ed4945ab7e Update CHANGELOG.md 2024-05-26 07:51:34 +02:00
ce8741bfc8 Merge pull request #174 from tobychui/v3.0.5
- Optimized uptime monitor error message
- Optimized detection logic for internal proxy target and header rewrite condition for HTTP_HOST
- Fixed ovh DNS challenge provider form generator bug
- Configuration for OVH DNS Challenge
- Added permission policy module (not enabled)
- Added single-use cookiejar to uptime monitor request client to handle cookie issues on some poorly written back-end server
2024-05-26 13:26:03 +08:00
7a3db09811 Updated acmedns generator results 2024-05-26 13:13:43 +08:00
e73f9b47d3 Update issue templates 2024-05-26 12:32:29 +08:00
c248dacccf Update uptime.go
+ Added cookiejar to request client #149
2024-05-25 14:44:48 +08:00
d596d6b843 v3.0.5 init commit
+ Added external domain name detection for PR #168
+ Updated uptime error message in 5xx range
+ Modernized reverse proxy error page template
+ Added wip permission policy module
2024-05-24 22:24:14 +08:00
6feb2d105d Merge pull request #168 from nettybun/issue-164-http-host-header
Use correct Host HTTP header
2024-05-24 20:13:33 +08:00
3a26a5b4d3 Use correct Host HTTP header 2024-05-23 12:03:00 -07:00
2cdd5654ed Update README.md
Fixed wordings
2024-05-21 15:23:54 +08:00
a0d362df4e Update README.md
Added getting started guide
2024-05-21 15:22:55 +08:00
334c1ab131 Updated provider dns credential fields 2024-05-20 21:56:40 +08:00
08d52024ab Fixed bug in generator
Fixed bug for acmedns module auto generation for ovh provider #161
2024-05-20 21:53:53 +08:00
a3e16594e8 Merge pull request #155 from Morethanevil/main
Update CHANGELOG.md
2024-05-18 21:30:42 +08:00
cced07ba2d Update CHANGELOG.md 2024-05-18 14:11:57 +02:00
2003992d75 Update README.md
Updated project desc
2024-05-18 15:30:21 +08:00
71423d98b1 Updated README banner
Updated readme banner
2024-05-18 15:27:53 +08:00
8ca716c59f Update README.md
Added DNS challenge in feature list
2024-05-18 15:12:25 +08:00
59 changed files with 15541 additions and 16016 deletions

View File

@ -33,6 +33,7 @@ If applicable, add screenshots to help explain your problem.
- Device: [e.g. Bananapi R2 PRO] - Device: [e.g. Bananapi R2 PRO]
- OS: [e.g. Armbian] - OS: [e.g. Armbian]
- Version [e.g. 23.02 Bullseye ] - Version [e.g. 23.02 Bullseye ]
- Docker Version (if you are running Zoraxy in docker): [e.g. 3.0.4]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

25
.github/ISSUE_TEMPLATE/help-needed.md vendored Normal file
View File

@ -0,0 +1,25 @@
---
name: Help Needed
about: Something went wrong but I don't know why
title: "[HELP]"
labels: help wanted
assignees: ''
---
**What happened?**
A clear and concise description of what the problem is. Ex. I tried to create a proxy rule but it doesn't work. When I connects to my domain, I see [...]
**Describe what have you tried**
A clear and concise description of what you expect to see and what you have tried to debug it.
**Describe the networking setup you are using**
Here are some example, commonly asked questions from our maintainers:
- Are you using the docker build of Zoraxy? [yes (with docker setup & networking config attach) /no]
- Your Zoraxy version? [e.g. 3.0.4]
- Are you using Cloudflare? [yes/no]
- Are your system hosted under a NAT router? [e.g. yes, with subnet is e.g. 192.168.0.0/24 and include port forwarding config if any]
- DNS record (if any)
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,3 +1,28 @@
# v3.0.5 May 26 2024
+ Optimized uptime monitor error message [#121](https://github.com/tobychui/zoraxy/issues/121)
+ Optimized detection logic for internal proxy target and header rewrite condition for HTTP_HOST [#164](https://github.com/tobychui/zoraxy/issues/164)
+ Fixed ovh DNS challenge provider form generator bug [#161](https://github.com/tobychui/zoraxy/issues/161)
+ Added permission policy module (not enabled)
+ Added single-use cookiejar to uptime monitor request client to handle cookie issues on some poorly written back-end server [#149](https://github.com/tobychui/zoraxy/issues/149)
# v3.0.4 May 18 2024
## This release tidied up the contribution by [Teifun2](https://github.com/Teifun2) and added a new way to generate DNS challenge based certificate (e.g. wildcards) from Let's Encrypt without changing any environment variables. This also fixes a few previous ACME module EAB settings bug related to concurrent save.
You can find the DNS challenge settings under TLS / SSL > ACME snippet > Generate New Certificate > (Check the "Use a DNS Challenge" checkbox)
+ Optimized DNS challenge implementation [thanks to Teifun2](https://github.com/Teifun2) / Issues [#49](https://github.com/tobychui/zoraxy/issues/49) [#79](https://github.com/tobychui/zoraxy/issues/79)
+ Removed dependencies on environment variable write and keep all data contained
+ Fixed panic on loading certificate generated by Zoraxy v2
+ Added automatic form generator for DNS challenge / providers
+ Added CA name default value
+ Added code generator for acmedns module (storing the DNS challenge provider contents extracted from lego)
+ Fixed ACME snippet "Obtain Certificate" concurrent issues in save EAB and DNS credentials
# v3.0.3 Apr 30 2024 # v3.0.3 Apr 30 2024
## Breaking Change ## Breaking Change

View File

@ -2,9 +2,8 @@
# Zoraxy # Zoraxy
General purpose request (reverse) proxy and forwarding tool for networking noobs. Now written in Go! A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
*Zoraxy v3 HTTP proxy config is not compatible with the older v2. If you are looking for the legacy version of Zoraxy, take a look at the [v2 branch](https://github.com/tobychui/zoraxy/tree/v2)*
### Features ### Features
@ -19,6 +18,7 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs
- TLS / SSL setup and deploy - TLS / SSL setup and deploy
- ACME features like auto-renew to serve your sites in http**s** - ACME features like auto-renew to serve your sites in http**s**
- SNI support (and SAN certs) - SNI support (and SAN certs)
- DNS Challenge for Let's Encrypt and [these DNS providers](https://go-acme.github.io/lego/dns/)
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners) - Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
- Global Area Network Controller Web UI (ZeroTier not included) - Global Area Network Controller Web UI (ZeroTier not included)
- TCP Tunneling / Proxy - TCP Tunneling / Proxy
@ -41,6 +41,12 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs
For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/) For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/)
## Getting Started
[Installing Zoraxy Reverse Proxy: Your Gateway to Efficient Web Routing](https://geekscircuit.com/installing-zoraxy-reverse-proxy-your-gateway-to-efficient-web-routing/)
Thank you for the well written and easy to follow tutorial by Reddit users [itsvmn](https://www.reddit.com/user/itsvmn/)!
If you have no background in setting up reverse proxy or web routing, you should check this out before you start setting up your Zoraxy.
## Build from Source ## Build from Source
Requires Go 1.22 or higher Requires Go 1.22 or higher
@ -60,7 +66,7 @@ Zoraxy provides basic authentication system for standalone mode. To use it in st
### Standalone Mode ### Standalone Mode
Standalone mode is the default mode for Zoraxy. This allows a single account to manage your reverse proxy server, just like a home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers. Standalone mode is the default mode for Zoraxy. This allows a single account to manage your reverse proxy server just like a basic home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers. A full "Getting Started" guide can be found [here](https://github.com/tobychui/zoraxy/wiki/Getting-Started).
#### Linux #### Linux

View File

@ -80,7 +80,7 @@
<div class="bannerHeaderWrapper"> <div class="bannerHeaderWrapper">
<h1 class="bannerHeader">Zoraxy</h1> <h1 class="bannerHeader">Zoraxy</h1>
<div class="ui divider"></div><br> <div class="ui divider"></div><br>
<p class="bannerSubheader">All in one homelab network routing solution</p> <p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
</div> </div>
<br><br> <br><br>
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a> <a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

View File

@ -141,14 +141,13 @@ func initAPIs() {
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete) authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
//TCP Proxy //TCP Proxy
authRouter.HandleFunc("/api/tcpprox/config/add", tcpProxyManager.HandleAddProxyConfig) authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
authRouter.HandleFunc("/api/tcpprox/config/edit", tcpProxyManager.HandleEditProxyConfigs) authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
authRouter.HandleFunc("/api/tcpprox/config/list", tcpProxyManager.HandleListConfigs) authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
authRouter.HandleFunc("/api/tcpprox/config/start", tcpProxyManager.HandleStartProxy) authRouter.HandleFunc("/api/streamprox/config/start", streamProxyManager.HandleStartProxy)
authRouter.HandleFunc("/api/tcpprox/config/stop", tcpProxyManager.HandleStopProxy) authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
authRouter.HandleFunc("/api/tcpprox/config/delete", tcpProxyManager.HandleRemoveProxy) authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
authRouter.HandleFunc("/api/tcpprox/config/status", tcpProxyManager.HandleGetProxyStatus) authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
authRouter.HandleFunc("/api/tcpprox/config/validate", tcpProxyManager.HandleConfigValidate)
//mDNS APIs //mDNS APIs
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing) authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)

View File

@ -272,17 +272,14 @@ func HandleNewPasswordSetup(w http.ResponseWriter, r *http.Request) {
return return
} }
//Delete the user account // Un register the user account
authAgent.UnregisterUser(username) if err := authAgent.UnregisterUser(username); err != nil {
//Ok. Set the new password
err = authAgent.CreateUserAccount(username, newPassword, "")
if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
} }
if err != nil { //Ok. Set the new password
if err := authAgent.CreateUserAccount(username, newPassword, ""); err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
} }

View File

@ -28,7 +28,7 @@ import (
"imuslab.com/zoraxy/mod/sshprox" "imuslab.com/zoraxy/mod/sshprox"
"imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/statistic/analytic" "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/tlscert"
"imuslab.com/zoraxy/mod/uptime" "imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
@ -52,7 +52,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
var ( var (
name = "Zoraxy" name = "Zoraxy"
version = "3.0.4" version = "3.0.6"
nodeUUID = "generic" nodeUUID = "generic"
development = false //Set this to false to use embedded web fs development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix() bootTime = time.Now().Unix()
@ -79,7 +79,7 @@ var (
mdnsScanner *mdns.MDNSHost //mDNS discovery services mdnsScanner *mdns.MDNSHost //mDNS discovery services
ganManager *ganserv.NetworkManager //Global Area Network Manager ganManager *ganserv.NetworkManager //Global Area Network Manager
webSshManager *sshprox.Manager //Web SSH connection service 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 acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs

View File

@ -1242,15 +1242,32 @@
"Name": "gandiv5", "Name": "gandiv5",
"ConfigableFields": [ "ConfigableFields": [
{ {
"Title": "fieldName", "Title": "BaseURL",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "authZone", "Title": "APIKey",
"Datatype": "string"
},
{
"Title": "PersonalAccessToken",
"Datatype": "string" "Datatype": "string"
} }
], ],
"HiddenFields": [] "HiddenFields": [
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
},
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
}, },
"gcore": { "gcore": {
"Name": "gcore", "Name": "gcore",
@ -2063,35 +2080,40 @@
"Name": "namecheap", "Name": "namecheap",
"ConfigableFields": [ "ConfigableFields": [
{ {
"Title": "domain", "Title": "Debug",
"Datatype": "bool"
},
{
"Title": "BaseURL",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "key", "Title": "APIUser",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "keyFqdn", "Title": "APIKey",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "keyValue", "Title": "ClientIP",
"Datatype": "string"
},
{
"Title": "tld",
"Datatype": "string"
},
{
"Title": "sld",
"Datatype": "string"
},
{
"Title": "host",
"Datatype": "string" "Datatype": "string"
} }
], ],
"HiddenFields": [] "HiddenFields": [
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
},
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
}, },
"namedotcom": { "namedotcom": {
"Name": "namedotcom", "Name": "namedotcom",
@ -2418,26 +2440,38 @@
"Name": "ovh", "Name": "ovh",
"ConfigableFields": [ "ConfigableFields": [
{ {
"Title": "FieldType", "Title": "APIEndpoint",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "SubDomain", "Title": "ApplicationKey",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "Target", "Title": "ApplicationSecret",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "Zone", "Title": "ConsumerKey",
"Datatype": "string" "Datatype": "string"
} }
], ],
"HiddenFields": [ "HiddenFields": [
{ {
"Title": "ID", "Title": "OAuth2Config",
"Datatype": "int64" "Datatype": "*OAuth2Config"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
},
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
} }
] ]
}, },
@ -2875,15 +2909,28 @@
"Name": "shellrent", "Name": "shellrent",
"ConfigableFields": [ "ConfigableFields": [
{ {
"Title": "domainID", "Title": "Username",
"Datatype": "int" "Datatype": "string"
}, },
{ {
"Title": "recordID", "Title": "Token",
"Datatype": "int" "Datatype": "string"
} }
], ],
"HiddenFields": [] "HiddenFields": [
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
},
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
}, },
"simply": { "simply": {
"Name": "simply", "Name": "simply",
@ -3034,15 +3081,28 @@
}, },
"ultradns": { "ultradns": {
"Name": "ultradns", "Name": "ultradns",
"ConfigableFields": [], "ConfigableFields": [
"HiddenFields": [
{ {
"Title": "config", "Title": "Username",
"Datatype": "*Config" "Datatype": "string"
}, },
{ {
"Title": "client", "Title": "Password",
"Datatype": "*client.Client" "Datatype": "string"
},
{
"Title": "Endpoint",
"Datatype": "string"
}
],
"HiddenFields": [
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
} }
] ]
}, },

View File

@ -14,11 +14,16 @@ import (
Main server for dynamic proxy core Main server for dynamic proxy core
Routing Handler Priority (High to Low) Routing Handler Priority (High to Low)
- Blacklist - Special Routing Rule (e.g. acme)
- Whitelist
- Redirectable - Redirectable
- Subdomain Routing - 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) { 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 return
} }
//Inject headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/* /*
Redirection Routing 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

@ -10,6 +10,8 @@ import (
"net/url" "net/url"
"strings" "strings"
"time" "time"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
) )
// ReverseProxy is an HTTP Handler that takes an incoming request and // ReverseProxy is an HTTP Handler that takes an incoming request and
@ -55,11 +57,14 @@ type ReverseProxy struct {
} }
type ResponseRewriteRuleSet struct { type ResponseRewriteRuleSet struct {
ProxyDomain string ProxyDomain string
OriginalHost string OriginalHost string
UseTLS bool UseTLS bool
NoCache bool NoCache bool
PathPrefix string //Vdir prefix for root, / will be rewrite to this 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 { type requestCanceler interface {
@ -246,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 { func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
transport := p.Transport transport := p.Transport
@ -346,9 +279,9 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
p.Director(outreq) p.Director(outreq)
outreq.Close = false outreq.Close = false
if !rrr.UseTLS { //Only skip origin rewrite iff proxy target require TLS and it is external domain name like github.com
//This seems to be routing to external sites if !(rrr.UseTLS && isExternalDomainName(rrr.ProxyDomain)) {
//Do not keep the original host // Always use the original host, see issue #164
outreq.Host = rrr.OriginalHost outreq.Host = rrr.OriginalHost
} }
@ -356,12 +289,18 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
outreq.Header = make(http.Header) outreq.Header = make(http.Header)
copyHeader(outreq.Header, req.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) removeHeaders(outreq.Header, rrr.NoCache)
// Add X-Forwarded-For Header. // Add X-Forwarded-For Header.
addXForwardedForHeader(outreq) 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) res, err := transport.RoundTrip(outreq)
if err != nil { if err != nil {
if p.Verbal { if p.Verbal {
@ -392,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 { //if res.StatusCode == 501 || res.StatusCode == 500 {
// fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI) // fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI)
// fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode) // fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode)
// fmt.Println(outreq.Header, req.Host) // 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") != "" { if res.Header.Get("Location") != "" {
locationRewrite := res.Header.Get("Location") locationRewrite := res.Header.Get("Location")
originLocation := res.Header.Get("Location") originLocation := res.Header.Get("Location")
@ -424,9 +367,16 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
res.Header.Set("Location", locationRewrite) res.Header.Set("Location", locationRewrite)
} }
// Add user defined headers (to downstream)
injectUserDefinedHeaders(res.Header, rrr.DownstreamHeaders)
// Copy header from response to client. // Copy header from response to client.
copyHeader(rw.Header(), res.Header) copyHeader(rw.Header(), res.Header)
// inject permission policy headers
//TODO: Load permission policy from rrr
permissionpolicy.InjectPermissionPolicyHeader(rw, nil)
// The "Trailer" header isn't included in the Transport's response, Build it up from Trailer. // The "Trailer" header isn't included in the Transport's response, Build it up from Trailer.
if len(res.Trailer) > 0 { if len(res.Trailer) > 0 {
trailerKeys := make([]string, 0, len(res.Trailer)) trailerKeys := make([]string, 0, len(res.Trailer))

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

@ -1,6 +1,7 @@
package dpcore package dpcore
import ( import (
"net"
"net/url" "net/url"
"strings" "strings"
) )
@ -60,3 +61,34 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) { func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
return replaceLocationHost(urlString, rrr, useTLS) return replaceLocationHost(urlString, rrr, useTLS)
} }
// isExternalDomainName check and return if the hostname is external domain name (e.g. github.com)
// instead of internal (like 192.168.1.202:8443 (ip address) or domains end with .local or .internal)
func isExternalDomainName(hostname string) bool {
host, _, err := net.SplitHostPort(hostname)
if err != nil {
//hostname doesnt contain port
ip := net.ParseIP(hostname)
if ip != nil {
//IP address, not a domain name
return false
}
} else {
//Hostname contain port, use hostname without port to check if it is ip
ip := net.ParseIP(host)
if ip != nil {
//IP address, not a domain name
return false
}
}
//Check if it is internal DNS assigned domains
internalDNSTLD := []string{".local", ".internal", ".localhost", ".home.arpa"}
for _, tld := range internalDNSTLD {
if strings.HasSuffix(strings.ToLower(hostname), tld) {
return false
}
}
return true
}

View File

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

View File

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

View File

@ -0,0 +1,193 @@
package permissionpolicy
import (
"fmt"
"net/http"
"strings"
)
/*
Permisson Policy
This is a permission policy header modifier that changes
the request permission related policy fields
author: tobychui
*/
type PermissionsPolicy struct {
Accelerometer []string `json:"accelerometer"`
AmbientLightSensor []string `json:"ambient_light_sensor"`
Autoplay []string `json:"autoplay"`
Battery []string `json:"battery"`
Camera []string `json:"camera"`
CrossOriginIsolated []string `json:"cross_origin_isolated"`
DisplayCapture []string `json:"display_capture"`
DocumentDomain []string `json:"document_domain"`
EncryptedMedia []string `json:"encrypted_media"`
ExecutionWhileNotRendered []string `json:"execution_while_not_rendered"`
ExecutionWhileOutOfView []string `json:"execution_while_out_of_viewport"`
Fullscreen []string `json:"fullscreen"`
Geolocation []string `json:"geolocation"`
Gyroscope []string `json:"gyroscope"`
KeyboardMap []string `json:"keyboard_map"`
Magnetometer []string `json:"magnetometer"`
Microphone []string `json:"microphone"`
Midi []string `json:"midi"`
NavigationOverride []string `json:"navigation_override"`
Payment []string `json:"payment"`
PictureInPicture []string `json:"picture_in_picture"`
PublicKeyCredentialsGet []string `json:"publickey_credentials_get"`
ScreenWakeLock []string `json:"screen_wake_lock"`
SyncXHR []string `json:"sync_xhr"`
USB []string `json:"usb"`
WebShare []string `json:"web_share"`
XRSpatialTracking []string `json:"xr_spatial_tracking"`
ClipboardRead []string `json:"clipboard_read"`
ClipboardWrite []string `json:"clipboard_write"`
Gamepad []string `json:"gamepad"`
SpeakerSelection []string `json:"speaker_selection"`
ConversionMeasurement []string `json:"conversion_measurement"`
FocusWithoutUserActivation []string `json:"focus_without_user_activation"`
HID []string `json:"hid"`
IdleDetection []string `json:"idle_detection"`
InterestCohort []string `json:"interest_cohort"`
Serial []string `json:"serial"`
SyncScript []string `json:"sync_script"`
TrustTokenRedemption []string `json:"trust_token_redemption"`
Unload []string `json:"unload"`
WindowPlacement []string `json:"window_placement"`
VerticalScroll []string `json:"vertical_scroll"`
}
// GetDefaultPermissionPolicy returns a PermissionsPolicy struct with all policies set to *
func GetDefaultPermissionPolicy() *PermissionsPolicy {
return &PermissionsPolicy{
Accelerometer: []string{"*"},
AmbientLightSensor: []string{"*"},
Autoplay: []string{"*"},
Battery: []string{"*"},
Camera: []string{"*"},
CrossOriginIsolated: []string{"*"},
DisplayCapture: []string{"*"},
DocumentDomain: []string{"*"},
EncryptedMedia: []string{"*"},
ExecutionWhileNotRendered: []string{"*"},
ExecutionWhileOutOfView: []string{"*"},
Fullscreen: []string{"*"},
Geolocation: []string{"*"},
Gyroscope: []string{"*"},
KeyboardMap: []string{"*"},
Magnetometer: []string{"*"},
Microphone: []string{"*"},
Midi: []string{"*"},
NavigationOverride: []string{"*"},
Payment: []string{"*"},
PictureInPicture: []string{"*"},
PublicKeyCredentialsGet: []string{"*"},
ScreenWakeLock: []string{"*"},
SyncXHR: []string{"*"},
USB: []string{"*"},
WebShare: []string{"*"},
XRSpatialTracking: []string{"*"},
ClipboardRead: []string{"*"},
ClipboardWrite: []string{"*"},
Gamepad: []string{"*"},
SpeakerSelection: []string{"*"},
ConversionMeasurement: []string{"*"},
FocusWithoutUserActivation: []string{"*"},
HID: []string{"*"},
IdleDetection: []string{"*"},
InterestCohort: []string{"*"},
Serial: []string{"*"},
SyncScript: []string{"*"},
TrustTokenRedemption: []string{"*"},
Unload: []string{"*"},
WindowPlacement: []string{"*"},
VerticalScroll: []string{"*"},
}
}
// InjectPermissionPolicyHeader inject the permission policy into headers
func InjectPermissionPolicyHeader(w http.ResponseWriter, policy *PermissionsPolicy) {
//Keep the original Permission Policy if exists, or there are no policy given
if policy == nil || w.Header().Get("Permissions-Policy") != "" {
return
}
policyHeader := []string{}
// Helper function to add policy directives
addDirective := func(name string, sources []string) {
if len(sources) > 0 {
if sources[0] == "*" {
//Allow all
policyHeader = append(policyHeader, fmt.Sprintf("%s=%s", name, "*"))
} else {
//Other than "self" which do not need double quote, others domain need double quote in place
formatedSources := []string{}
for _, source := range sources {
if source == "self" {
formatedSources = append(formatedSources, "self")
} else {
formatedSources = append(formatedSources, "\""+source+"\"")
}
}
policyHeader = append(policyHeader, fmt.Sprintf("%s=(%s)", name, strings.Join(formatedSources, " ")))
}
} else {
//There are no setting for this field. Assume no permission
policyHeader = append(policyHeader, fmt.Sprintf("%s=()", name))
}
}
// Add each policy directive to the header
addDirective("accelerometer", policy.Accelerometer)
addDirective("ambient-light-sensor", policy.AmbientLightSensor)
addDirective("autoplay", policy.Autoplay)
addDirective("battery", policy.Battery)
addDirective("camera", policy.Camera)
addDirective("cross-origin-isolated", policy.CrossOriginIsolated)
addDirective("display-capture", policy.DisplayCapture)
addDirective("document-domain", policy.DocumentDomain)
addDirective("encrypted-media", policy.EncryptedMedia)
addDirective("execution-while-not-rendered", policy.ExecutionWhileNotRendered)
addDirective("execution-while-out-of-viewport", policy.ExecutionWhileOutOfView)
addDirective("fullscreen", policy.Fullscreen)
addDirective("geolocation", policy.Geolocation)
addDirective("gyroscope", policy.Gyroscope)
addDirective("keyboard-map", policy.KeyboardMap)
addDirective("magnetometer", policy.Magnetometer)
addDirective("microphone", policy.Microphone)
addDirective("midi", policy.Midi)
addDirective("navigation-override", policy.NavigationOverride)
addDirective("payment", policy.Payment)
addDirective("picture-in-picture", policy.PictureInPicture)
addDirective("publickey-credentials-get", policy.PublicKeyCredentialsGet)
addDirective("screen-wake-lock", policy.ScreenWakeLock)
addDirective("sync-xhr", policy.SyncXHR)
addDirective("usb", policy.USB)
addDirective("web-share", policy.WebShare)
addDirective("xr-spatial-tracking", policy.XRSpatialTracking)
addDirective("clipboard-read", policy.ClipboardRead)
addDirective("clipboard-write", policy.ClipboardWrite)
addDirective("gamepad", policy.Gamepad)
addDirective("speaker-selection", policy.SpeakerSelection)
addDirective("conversion-measurement", policy.ConversionMeasurement)
addDirective("focus-without-user-activation", policy.FocusWithoutUserActivation)
addDirective("hid", policy.HID)
addDirective("idle-detection", policy.IdleDetection)
addDirective("interest-cohort", policy.InterestCohort)
addDirective("serial", policy.Serial)
addDirective("sync-script", policy.SyncScript)
addDirective("trust-token-redemption", policy.TrustTokenRedemption)
addDirective("unload", policy.Unload)
addDirective("window-placement", policy.WindowPlacement)
addDirective("vertical-scroll", policy.VerticalScroll)
// Join the directives and set the header
policyHeaderValue := strings.Join(policyHeader, ", ")
//Inject the new policy into the header
w.Header().Set("Permissions-Policy", policyHeaderValue)
}

View File

@ -0,0 +1,47 @@
package permissionpolicy_test
import (
"net/http/httptest"
"strings"
"testing"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
func TestInjectPermissionPolicyHeader(t *testing.T) {
//Prepare the data for permission policy
testPermissionPolicy := permissionpolicy.GetDefaultPermissionPolicy()
testPermissionPolicy.Geolocation = []string{"self"}
testPermissionPolicy.Microphone = []string{"self", "https://example.com"}
testPermissionPolicy.Camera = []string{"*"}
tests := []struct {
name string
existingHeader string
policy *permissionpolicy.PermissionsPolicy
expectedHeader string
}{
{
name: "Default policy with a few limitations",
existingHeader: "",
policy: testPermissionPolicy,
expectedHeader: `accelerometer=*, ambient-light-sensor=*, autoplay=*, battery=*, camera=*, cross-origin-isolated=*, display-capture=*, document-domain=*, encrypted-media=*, execution-while-not-rendered=*, execution-while-out-of-viewport=*, fullscreen=*, geolocation=(self), gyroscope=*, keyboard-map=*, magnetometer=*, microphone=(self "https://example.com"), midi=*, navigation-override=*, payment=*, picture-in-picture=*, publickey-credentials-get=*, screen-wake-lock=*, sync-xhr=*, usb=*, web-share=*, xr-spatial-tracking=*, clipboard-read=*, clipboard-write=*, gamepad=*, speaker-selection=*, conversion-measurement=*, focus-without-user-activation=*, hid=*, idle-detection=*, interest-cohort=*, serial=*, sync-script=*, trust-token-redemption=*, unload=*, window-placement=*, vertical-scroll=*`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rr := httptest.NewRecorder()
if tt.existingHeader != "" {
rr.Header().Set("Permissions-Policy", tt.existingHeader)
}
permissionpolicy.InjectPermissionPolicyHeader(rr, tt.policy)
gotHeader := rr.Header().Get("Permissions-Policy")
if !strings.Contains(gotHeader, tt.expectedHeader) {
t.Errorf("got header %s, want %s", gotHeader, tt.expectedHeader)
}
})
}
}

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-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) 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() requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //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) r.URL, _ = url.Parse(originalHostHeader)
} }
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain, ProxyDomain: target.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS, UseTLS: target.RequireTLS,
NoCache: h.Parent.Option.NoCache, NoCache: h.Parent.Option.NoCache,
PathPrefix: "", PathPrefix: "",
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
Version: target.parent.Option.HostVersion,
}) })
var dnsError *net.DNSError 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-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) 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" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("Zr-Origin-Upgrade", "websocket") 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) r.URL, _ = url.Parse(originalHostHeader)
} }
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders()
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain, ProxyDomain: target.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS, UseTLS: target.RequireTLS,
PathPrefix: target.MatchingPath, PathPrefix: target.MatchingPath,
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
Version: target.parent.parent.Option.HostVersion,
}) })
var dnsError *net.DNSError var dnsError *net.DNSError

View File

@ -14,7 +14,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
<title>404 - Host Not Found</title> <title>404 - Host Not Found</title>
<style> <style>
h1, h2, h3, h4, h5, p, a, span{ h1, h2, h3, h4, h5, p, a, span, .ui.list .item{
font-family: 'Noto Sans TC', sans-serif; font-family: 'Noto Sans TC', sans-serif;
font-weight: 300; font-weight: 300;
color: rgb(88, 88, 88) color: rgb(88, 88, 88)
@ -22,9 +22,6 @@
.diagram{ .diagram{
background-color: #ebebeb; background-color: #ebebeb;
box-shadow:
inset 0px 11px 8px -10px #CCC,
inset 0px -11px 8px -10px #CCC;
padding-bottom: 2em; padding-bottom: 2em;
} }

View File

@ -72,10 +72,20 @@ type BasicAuthExceptionRule struct {
PathPrefix string 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 // User defined headers to add into a proxy endpoint
type UserDefinedHeader struct { type UserDefinedHeader struct {
Key string Direction HeaderDirection
Value string Key string
Value string
IsRemove bool //Instead of set, remove this key instead
} }
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better // 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 { func (s *Sender) SendEmail(to string, subject string, content string) error {
//Parse the email content // Parse the email content
msg := []byte("To: " + to + "\n" + msg := []byte("To: " + to + "\n" +
"From: Zoraxy <" + s.SenderAddr + ">\n" + "From: Zoraxy <" + s.SenderAddr + ">\n" +
"Subject: " + subject + "\n" + "Subject: " + subject + "\n" +
"MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" + "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" +
content + "\n\n") content + "\n\n")
//Login to the SMTP server // Initialize the auth variable
//Username can be username (e.g. admin) or email (e.g. admin@example.com), depending on SMTP service provider var auth smtp.Auth
auth := smtp.PlainAuth("", s.Username, s.Password, s.Hostname) 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) err := smtp.SendMail(s.Hostname+":"+strconv.Itoa(s.Port), auth, s.SenderAddr, []string{to}, msg)
if err != nil { if err != nil {
return err 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" "time"
) )
//Rewrite url based on proxy root // Rewrite url based on proxy root (default site)
func RewriteURL(rooturl string, requestURL string) (*url.URL, error) { func RewriteURL(rooturl string, requestURL string) (*url.URL, error) {
rewrittenURL := strings.TrimPrefix(requestURL, rooturl) rewrittenURL := strings.TrimPrefix(requestURL, rooturl)
return url.Parse(rewrittenURL) 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 { func IsWebSSHSupported() bool {
//Check if the binary exists in system/gotty/ //Check if the binary exists in system/gotty/
binary := "gotty_" + runtime.GOOS + "_" + runtime.GOARCH binary := "gotty_" + runtime.GOOS + "_" + runtime.GOARCH
@ -34,7 +34,7 @@ func IsWebSSHSupported() bool {
return true 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 { func IsSSHConnectable(ipOrDomain string, port int) bool {
timeout := time.Second * 3 timeout := time.Second * 3
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ipOrDomain, port), timeout) 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" 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 { func isPortInUse(port int) bool {
address := fmt.Sprintf(":%d", port) address := fmt.Sprintf(":%d", port)
listener, err := net.Listen("tcp", address) listener, err := net.Listen("tcp", address)

View File

@ -1,9 +1,10 @@
package tcpprox package streamproxy
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -22,13 +23,13 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
return return
} }
portA, err := utils.PostPara(r, "porta") listenAddr, err := utils.PostPara(r, "listenAddr")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "first address cannot be empty") utils.SendErrorResponse(w, "first address cannot be empty")
return return
} }
portB, err := utils.PostPara(r, "portb") proxyAddr, err := utils.PostPara(r, "proxyAddr")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "second address cannot be empty") utils.SendErrorResponse(w, "second address cannot be empty")
return return
@ -44,27 +45,17 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
} }
} }
modeValue := ProxyMode_Transport useTCP, _ := utils.PostBool(r, "useTCP")
mode, err := utils.PostPara(r, "mode") useUDP, _ := utils.PostBool(r, "useUDP")
if err != nil || mode == "" {
utils.SendErrorResponse(w, "no mode given")
} else if mode == "listen" {
modeValue = ProxyMode_Listen
} else if mode == "transport" {
modeValue = ProxyMode_Transport
} else if mode == "starter" {
modeValue = ProxyMode_Starter
} else {
utils.SendErrorResponse(w, "invalid mode given. Only support listen / transport / starter")
}
//Create the target config //Create the target config
newConfigUUID := m.NewConfig(&ProxyRelayOptions{ newConfigUUID := m.NewConfig(&ProxyRelayOptions{
Name: name, Name: name,
PortA: portA, ListeningAddr: strings.TrimSpace(listenAddr),
PortB: portB, ProxyAddr: strings.TrimSpace(proxyAddr),
Timeout: timeout, Timeout: timeout,
Mode: modeValue, UseTCP: useTCP,
UseUDP: useUDP,
}) })
js, _ := json.Marshal(newConfigUUID) js, _ := json.Marshal(newConfigUUID)
@ -80,22 +71,10 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
} }
newName, _ := utils.PostPara(r, "name") newName, _ := utils.PostPara(r, "name")
newPortA, _ := utils.PostPara(r, "porta") listenAddr, _ := utils.PostPara(r, "listenAddr")
newPortB, _ := utils.PostPara(r, "portb") proxyAddr, _ := utils.PostPara(r, "proxyAddr")
newModeStr, _ := utils.PostPara(r, "mode") useTCP, _ := utils.PostBool(r, "useTCP")
newMode := -1 useUDP, _ := utils.PostBool(r, "useUDP")
if newModeStr != "" {
if newModeStr == "listen" {
newMode = 0
} else if newModeStr == "transport" {
newMode = 1
} else if newModeStr == "starter" {
newMode = 2
} else {
utils.SendErrorResponse(w, "invalid new mode value")
return
}
}
newTimeoutStr, _ := utils.PostPara(r, "timeout") newTimeoutStr, _ := utils.PostPara(r, "timeout")
newTimeout := -1 newTimeout := -1
@ -108,7 +87,7 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
} }
// Call the EditConfig method to modify the configuration // 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 { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
@ -158,6 +137,7 @@ func (m *Manager) HandleStopProxy(w http.ResponseWriter, r *http.Request) {
} }
if !targetProxyConfig.IsRunning() { if !targetProxyConfig.IsRunning() {
targetProxyConfig.Running = false
utils.SendErrorResponse(w, "target proxy service is not running") utils.SendErrorResponse(w, "target proxy service is not running")
return return
} }
@ -180,6 +160,7 @@ func (m *Manager) HandleRemoveProxy(w http.ResponseWriter, r *http.Request) {
} }
if targetProxyConfig.IsRunning() { if targetProxyConfig.IsRunning() {
targetProxyConfig.Running = false
utils.SendErrorResponse(w, "Service is running") utils.SendErrorResponse(w, "Service is running")
return return
} }
@ -209,25 +190,3 @@ func (m *Manager) HandleGetProxyStatus(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(targetConfig) js, _ := json.Marshal(targetConfig)
utils.SendJSONResponse(w, string(js)) 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 ( import (
"testing" "testing"
"time" "time"
"imuslab.com/zoraxy/mod/tcpprox" "imuslab.com/zoraxy/mod/streamproxy"
) )
func TestPort2Port(t *testing.T) { func TestPort2Port(t *testing.T) {
@ -12,7 +12,7 @@ func TestPort2Port(t *testing.T) {
stopChan := make(chan bool) stopChan := make(chan bool)
// Create a ProxyRelayConfig with dummy values // Create a ProxyRelayConfig with dummy values
config := &tcpprox.ProxyRelayConfig{ config := &streamproxy.ProxyRelayConfig{
Timeout: 1, Timeout: 1,
} }
@ -36,7 +36,7 @@ func TestPort2Port(t *testing.T) {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
// If the goroutine is still running, it means it did not stop as expected // 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") 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

@ -4,9 +4,11 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"net/http/cookiejar"
"strings" "strings"
"time" "time"
"golang.org/x/net/publicsuffix"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -217,11 +219,24 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
} }
func getWebsiteStatus(url string) (int, error) { func getWebsiteStatus(url string) (int, error) {
// Create a one-time use cookie jar to store cookies
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
log.Fatal(err)
}
client := http.Client{ client := http.Client{
Jar: jar,
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
} }
resp, err := client.Get(url) req, _ := http.NewRequest("GET", url, nil)
req.Header = http.Header{
"User-Agent": {"zoraxy-uptime/1.1"},
}
resp, err := client.Do(req)
//resp, err := client.Get(url)
if err != nil { if err != nil {
//Try replace the http with https and vise versa //Try replace the http with https and vise versa
rewriteURL := "" rewriteURL := ""
@ -231,7 +246,12 @@ func getWebsiteStatus(url string) (int, error) {
rewriteURL = strings.ReplaceAll(url, "http://", "https://") rewriteURL = strings.ReplaceAll(url, "http://", "https://")
} }
resp, err = client.Get(rewriteURL) req, _ := http.NewRequest("GET", rewriteURL, nil)
req.Header = http.Header{
"User-Agent": {"zoraxy-uptime/1.1"},
}
resp, err := client.Do(req)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") { if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
//Invalid downstream reverse proxy settings, but it is online //Invalid downstream reverse proxy settings, but it is online

View File

@ -68,9 +68,9 @@ func PostBool(r *http.Request, key string) (bool, error) {
x = strings.TrimSpace(x) 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 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 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 // Add a new header to the target endpoint
func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) { func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
epType, err := utils.PostPara(r, "type") rewriteType, err := utils.PostPara(r, "type")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "endpoint type not defined") utils.SendErrorResponse(w, "rewriteType not defined")
return return
} }
@ -1088,6 +1088,12 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
return 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") name, err := utils.PostPara(r, "name")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "HTTP header name not set") 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") value, err := utils.PostPara(r, "value")
if err != nil { if err != nil && rewriteType == "add" {
utils.SendErrorResponse(w, "HTTP header value not set") utils.SendErrorResponse(w, "HTTP header value not set")
return return
} }
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
if epType == "root" { if err != nil {
targetProxyEndpoint = dynamicProxyRouter.Root utils.SendErrorResponse(w, "target endpoint not exists")
} else { return
ep, 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 //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) //Save it (no need reload as header are not handled by dpcore)
err = SaveReverseProxyConfig(targetProxyEndpoint) err = SaveReverseProxyConfig(targetProxyEndpoint)
@ -1128,12 +1154,6 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
// Remove a header from the target endpoint // Remove a header from the target endpoint
func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) { 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") domain, err := utils.PostPara(r, "domain")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "domain or matching rule not defined") utils.SendErrorResponse(w, "domain or matching rule not defined")
@ -1146,20 +1166,17 @@ func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
if epType == "root" { if err != nil {
targetProxyEndpoint = dynamicProxyRouter.Root utils.SendErrorResponse(w, "target endpoint not exists")
} else { return
ep, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
targetProxyEndpoint = ep
} }
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) err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil { if err != nil {

View File

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

View File

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

View File

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

View File

@ -68,12 +68,13 @@
<div class="standardContainer"> <div class="standardContainer">
<div class="ui divider"></div> <div class="ui divider"></div>
<h4>Global Settings</h4> <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"> <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"> <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> </div>
<br> <br><br>
<div id="tls" class="ui toggle notloopbackOnly checkbox"> <div id="tls" class="ui toggle notloopbackOnly checkbox">
<input type="checkbox"> <input type="checkbox">
<label>Use TLS to serve proxy request</label> <label>Use TLS to serve proxy request</label>
@ -160,6 +161,7 @@
</div> </div>
<script> <script>
let loopbackProxiedInterface = false; let loopbackProxiedInterface = false;
let currentListeningPort = 80;
$(".advanceSettings").accordion(); $(".advanceSettings").accordion();
//Initial the start stop button if this is reverse proxied //Initial the start stop button if this is reverse proxied
@ -176,6 +178,8 @@
//Get the latest server status from proxy server //Get the latest server status from proxy server
function initRPStaste(){ function initRPStaste(){
$.get("/api/proxy/status", function(data){ $.get("/api/proxy/status", function(data){
$("#incomingPort").off("change");
if (data.Running == true){ if (data.Running == true){
$("#startbtn").addClass("disabled"); $("#startbtn").addClass("disabled");
if (!loopbackProxiedInterface){ if (!loopbackProxiedInterface){
@ -194,6 +198,15 @@
$("#serverstatus").removeClass("green"); $("#serverstatus").removeClass("green");
} }
$("#incomingPort").val(data.Option.Port); $("#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); msgbox(data.error, false, 5000);
return; return;
} }
msgbox("Setting Updated"); msgbox("Listening Port Updated");
initRPStaste(); 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

@ -22,6 +22,28 @@
<script> <script>
var uptime5xxErrorMessage = {
"500": "Internal Server Error",
"501": "Not Implemented",
"502": "Bad Gateway",
"503": "Service Unavailable",
"504": "Gateway Timeout",
"505": "HTTP Version Not Supported",
"506": "Variant Also Negotiates",
"507": "Insufficient Storage",
"508": "Loop Detected",
"510": "Not Extended",
"511": "Network Authentication Required",
"520": "Web Server Returned an Unknown Error (Cloudflare)",
"521": "Web Server is Down (Cloudflare)",
"522": "Connection Timed Out (Cloudflare)",
"523": "Origin is Unreachable (Cloudflare)",
"524": "A Timeout Occurred (Cloudflare)",
"525": "SSL Handshake Failed (Cloudflare)",
"526": "Invalid SSL Certificate (Cloudflare)",
"527": "Railgun Error (Cloudflare)",
"530": "Site is Frozen (Pantheon)"
}
$('#utmEnable').checkbox({ $('#utmEnable').checkbox({
onChange: function() { onChange: function() {
@ -78,6 +100,14 @@
return(date.toLocaleString()); return(date.toLocaleString());
} }
function resolveUptime5xxErrorMessage(errorCode){
if (uptime5xxErrorMessage[errorCode] != undefined){
return uptime5xxErrorMessage[errorCode]
}else{
return "Unknown Error";
}
}
function renderUptimeData(key, value){ function renderUptimeData(key, value){
if (value.length == 0){ if (value.length == 0){
@ -101,26 +131,33 @@
//Render status to html //Render status to html
let thisStatus = value[i]; let thisStatus = value[i];
let dotType = ""; let dotType = "";
if (thisStatus.Online){ let statusCode = thisStatus.StatusCode;
if (thisStatus.StatusCode < 200 || thisStatus.StatusCode >= 300){
dotType = "error";
}else{
dotType = "online";
}
ontimeRate++;
}else{
if (thisStatus.StatusCode >= 500 && thisStatus.StatusCode < 600){
//Special type of error, cause by downstream reverse proxy
dotType = "error";
}else if (thisStatus.StatusCode == 401){
//Unauthorized error
dotType = "error";
}else{
dotType = "offline";
}
}
if (!thisStatus.Online && statusCode == 0){
dotType = "offline";
}else if (statusCode < 200){
//1xx
dotType = "error";
ontimeRate++;
}else if (statusCode < 300){
//2xx
dotType = "online";
ontimeRate++;
}else if (statusCode < 400){
//3xx
dotType = "online";
ontimeRate++;
}else if (statusCode < 500){
//4xx
dotType = "error";
ontimeRate++;
}else if (statusCode < 600){
//5xx
dotType = "error";
}else {
dotType = "offline";
}
let datetime = format_time(thisStatus.Timestamp); let datetime = format_time(thisStatus.Timestamp);
statusDotList += `<div title="${datetime}" class="${dotType} statusDot"></div>` statusDotList += `<div title="${datetime}" class="${dotType} statusDot"></div>`
} }
@ -141,11 +178,13 @@
onlineStatusCss = `color: #3bd671;`; onlineStatusCss = `color: #3bd671;`;
}else{ }else{
if (value[value.length - 1].StatusCode >= 500 && value[value.length - 1].StatusCode < 600){ if (value[value.length - 1].StatusCode >= 500 && value[value.length - 1].StatusCode < 600){
currentOnlineStatus = `<i class="exclamation circle icon"></i> Misconfigured`; var latestStatusCode = value[value.length - 1].StatusCode
currentOnlineStatus = `<i class="exclamation circle icon"></i>${latestStatusCode} - ${resolveUptime5xxErrorMessage(latestStatusCode)}`;
onlineStatusCss = `color: #f38020;`; onlineStatusCss = `color: #f38020;`;
reminderEle = `<small style="${onlineStatusCss}">Downstream proxy server is online with misconfigured settings</small>`; reminderEle = `<small style="${onlineStatusCss}">Downstream proxy server is responsive but returning server error</small>`;
}else if (value[value.length - 1].StatusCode >= 400 && value[value.length - 1].StatusCode <= 405){ }else if (value[value.length - 1].StatusCode >= 400 && value[value.length - 1].StatusCode <= 405){
switch(value[value.length - 1].StatusCode){ let latestStatusCode = value[value.length - 1].StatusCode;
switch(latestStatusCode){
case 400: case 400:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Bad Request`; currentOnlineStatus = `<i class="exclamation circle icon"></i> Bad Request`;
break; break;
@ -161,6 +200,9 @@
case 405: case 405:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Method Not Allowed`; currentOnlineStatus = `<i class="exclamation circle icon"></i> Method Not Allowed`;
break; break;
default:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Status Code: ${latestStatusCode}`;
break;
} }
onlineStatusCss = `color: #f38020;`; onlineStatusCss = `color: #f38020;`;

View File

@ -217,6 +217,7 @@
$("#zoraxyinfo .uuid").text(data.NodeUUID); $("#zoraxyinfo .uuid").text(data.NodeUUID);
$("#zoraxyinfo .development").text(data.Development?"Development":"Release"); $("#zoraxyinfo .development").text(data.Development?"Development":"Release");
$("#zoraxyinfo .version").text(data.Version); $("#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 .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 .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`); $("#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'); 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 // validate sender address
const senderAddr = form.find('input[name="senderAddr"]').val().trim(); const senderAddr = form.find('input[name="senderAddr"]').val().trim();
if (!emailRegex.test(senderAddr)) { 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"> <a class="item" tag="rules">
<i class="simplistic plus square icon"></i> Create Proxy Rules <i class="simplistic plus square icon"></i> Create Proxy Rules
</a> </a>
<a class="item" tag="tcpprox"> <a class="item" tag="streamproxy">
<i class="simplistic exchange icon"></i> TCP Proxy <i class="simplistic exchange icon"></i> Stream Proxy
</a> </a>
<div class="ui divider menudivider">Access & Connections</div> <div class="ui divider menudivider">Access & Connections</div>
<a class="item" tag="cert"> <a class="item" tag="cert">
@ -125,7 +125,7 @@
<div id="zgrok" class="functiontab" target="zgrok.html"></div> <div id="zgrok" class="functiontab" target="zgrok.html"></div>
<!-- TCP Proxy --> <!-- TCP Proxy -->
<div id="tcpprox" class="functiontab" target="tcpprox.html"></div> <div id="streamproxy" class="functiontab" target="streamprox.html"></div>
<!-- Web Server --> <!-- Web Server -->
<div id="webserv" class="functiontab" target="webserv.html"></div> <div id="webserv" class="functiontab" target="webserv.html"></div>
@ -154,7 +154,7 @@
<br><br> <br><br>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui container" style="color: grey; font-size: 90%"> <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>
<div id="messageBox" class="ui green floating big compact message"> <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 HTTP Proxy & Virtual Directory
*/ */
@ -551,23 +561,23 @@ body{
TCP Proxy TCP Proxy
*/ */
.tcproxConfig td:first-child{ .streamproxConfig td:first-child{
position: relative; position: relative;
} }
.tcproxConfig.running td:first-child{ .streamproxConfig.running td:first-child{
border-left: 0.6em solid #02cb59 !important; border-left: 0.6em solid #01cb55 !important;
} }
.tcproxConfig.stopped td:first-child{ .streamproxConfig.stopped td:first-child{
border-left: 0.6em solid #02032a !important; border-left: 0.6em solid #1b1b1b !important;
} }
.tcproxConfig td:first-child .statusText{ .streamproxConfig td:first-child .statusText{
position: absolute; position: absolute;
bottom: 0.3em; bottom: 0.1em;
left: 0.2em; right: 0.2em;
font-size: 1.4em; font-size: 1em;
color:rgb(224, 224, 224); color:rgb(224, 224, 224);
opacity: 0.7; opacity: 0.7;
pointer-events: none; pointer-events: none;

View File

@ -14,7 +14,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
<title>521 - Web server is down</title> <title>521 - Web server is down</title>
<style> <style>
h1, h2, h3, h4, h5, p, a, span{ h1, h2, h3, h4, h5, p, a, span, .ui.list .item{
font-family: 'Noto Sans TC', sans-serif; font-family: 'Noto Sans TC', sans-serif;
font-weight: 300; font-weight: 300;
color: rgb(88, 88, 88) color: rgb(88, 88, 88)
@ -22,9 +22,6 @@
.diagram{ .diagram{
background-color: #ebebeb; background-color: #ebebeb;
box-shadow:
inset 0px 11px 8px -10px #CCC,
inset 0px -11px 8px -10px #CCC;
padding-bottom: 2em; padding-bottom: 2em;
} }

View File

@ -23,7 +23,7 @@
<table class="ui very basic compacted unstackable celled table"> <table class="ui very basic compacted unstackable celled table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Key</th>
<th>Value</th> <th>Value</th>
<th>Remove</th> <th>Remove</th>
</tr></thead> </tr></thead>
@ -34,18 +34,29 @@
</tbody> </tbody>
</table> </table>
<div class="ui divider"></div> <div class="ui divider"></div>
<h4>Add Custom Header</h4> <h4>Edit Custom Header</h4>
<p>Add custom header(s) into this proxy target</p> <p>Add or remove custom header(s) over this proxy target</p>
<div class="scrolling content ui form"> <div class="scrolling content ui form">
<div class="three small fields credentialEntry"> <div class="five small fields credentialEntry">
<div class="field"> <div class="field" align="center">
<input id="headerName" type="text" placeholder="X-Custom-Header" autocomplete="off"> <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>
<div class="field"> <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"> <input id="headerValue" type="text" placeholder="value1,value2,value3" autocomplete="off">
</div> </div>
<div class="field" > <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>
<div class="ui divider"></div> <div class="ui divider"></div>
</div> </div>
@ -75,6 +86,48 @@
parent.hideSideWrapper(true); 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)); //$("#debug").text(JSON.stringify(editingEndpoint));
function addCustomHeader(){ function addCustomHeader(){
@ -88,18 +141,21 @@
$("#headerName").parent().removeClass("error"); $("#headerName").parent().removeClass("error");
} }
if (value == ""){ if (getHeaderEditMode() == "add"){
$("#headerValue").parent().addClass("error"); if (value == ""){
return $("#headerValue").parent().addClass("error");
}else{ return
$("#headerValue").parent().removeClass("error"); }else{
$("#headerValue").parent().removeClass("error");
}
} }
$.ajax({ $.ajax({
url: "/api/proxy/header/add", url: "/api/proxy/header/add",
data: { data: {
"type": editingEndpoint.ept, "type": getHeaderEditMode(),
"domain": editingEndpoint.ep, "domain": editingEndpoint.ep,
"direction":getHeaderDirection(),
"name": name, "name": name,
"value": value "value": value
}, },
@ -129,7 +185,7 @@
$.ajax({ $.ajax({
url: "/api/proxy/header/remove", url: "/api/proxy/header/remove",
data: { data: {
"type": editingEndpoint.ept, //"type": editingEndpoint.ept,
"domain": editingEndpoint.ep, "domain": editingEndpoint.ep,
"name": name, "name": name,
}, },
@ -157,10 +213,16 @@
$("#headerTable").html(""); $("#headerTable").html("");
data.forEach(header => { 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(` $("#headerTable").append(`
<tr> <tr>
<td>${header.Key}</td> <td>${direction} ${header.Key}</td>
<td>${header.Value}</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> <td><button class="ui basic circular mini red icon button" onclick="deleteCustomHeader('${header.Key}');"><i class="ui trash icon"></i></button></td>
</tr> </tr>
`); `);

View File

@ -1242,15 +1242,32 @@
"Name": "gandiv5", "Name": "gandiv5",
"ConfigableFields": [ "ConfigableFields": [
{ {
"Title": "fieldName", "Title": "BaseURL",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "authZone", "Title": "APIKey",
"Datatype": "string"
},
{
"Title": "PersonalAccessToken",
"Datatype": "string" "Datatype": "string"
} }
], ],
"HiddenFields": [] "HiddenFields": [
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
},
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
}, },
"gcore": { "gcore": {
"Name": "gcore", "Name": "gcore",
@ -2063,35 +2080,40 @@
"Name": "namecheap", "Name": "namecheap",
"ConfigableFields": [ "ConfigableFields": [
{ {
"Title": "domain", "Title": "Debug",
"Datatype": "bool"
},
{
"Title": "BaseURL",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "key", "Title": "APIUser",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "keyFqdn", "Title": "APIKey",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "keyValue", "Title": "ClientIP",
"Datatype": "string"
},
{
"Title": "tld",
"Datatype": "string"
},
{
"Title": "sld",
"Datatype": "string"
},
{
"Title": "host",
"Datatype": "string" "Datatype": "string"
} }
], ],
"HiddenFields": [] "HiddenFields": [
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
},
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
}, },
"namedotcom": { "namedotcom": {
"Name": "namedotcom", "Name": "namedotcom",
@ -2418,26 +2440,38 @@
"Name": "ovh", "Name": "ovh",
"ConfigableFields": [ "ConfigableFields": [
{ {
"Title": "FieldType", "Title": "APIEndpoint",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "SubDomain", "Title": "ApplicationKey",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "Target", "Title": "ApplicationSecret",
"Datatype": "string" "Datatype": "string"
}, },
{ {
"Title": "Zone", "Title": "ConsumerKey",
"Datatype": "string" "Datatype": "string"
} }
], ],
"HiddenFields": [ "HiddenFields": [
{ {
"Title": "ID", "Title": "OAuth2Config",
"Datatype": "int64" "Datatype": "*OAuth2Config"
},
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
},
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
} }
] ]
}, },
@ -2875,15 +2909,28 @@
"Name": "shellrent", "Name": "shellrent",
"ConfigableFields": [ "ConfigableFields": [
{ {
"Title": "domainID", "Title": "Username",
"Datatype": "int" "Datatype": "string"
}, },
{ {
"Title": "recordID", "Title": "Token",
"Datatype": "int" "Datatype": "string"
} }
], ],
"HiddenFields": [] "HiddenFields": [
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
},
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
}
]
}, },
"simply": { "simply": {
"Name": "simply", "Name": "simply",
@ -3034,15 +3081,28 @@
}, },
"ultradns": { "ultradns": {
"Name": "ultradns", "Name": "ultradns",
"ConfigableFields": [], "ConfigableFields": [
"HiddenFields": [
{ {
"Title": "config", "Title": "Username",
"Datatype": "*Config" "Datatype": "string"
}, },
{ {
"Title": "client", "Title": "Password",
"Datatype": "*client.Client" "Datatype": "string"
},
{
"Title": "Endpoint",
"Datatype": "string"
}
],
"HiddenFields": [
{
"Title": "PropagationTimeout",
"Datatype": "time.Duration"
},
{
"Title": "PollingInterval",
"Datatype": "time.Duration"
} }
] ]
}, },

View File

@ -132,6 +132,23 @@ func extractConfigStruct(sourceCode string) (string, string) {
// Extract the struct name and its content. // Extract the struct name and its content.
structName := match[1] structName := match[1]
structContent := match[2] structContent := match[2]
if structName != "Config" {
allStructs := structRegex.FindAllStringSubmatch(sourceCode, 10)
for _, thisStruct := range allStructs {
//fmt.Println("Name => ", test[1])
//fmt.Println("Content => ", test[2])
if thisStruct[1] == "Config" {
structName = "Config"
structContent = thisStruct[2]
break
}
}
if structName != "Config" {
panic("Unable to find Config for this provider")
}
}
return structName, structContent return structName, structContent
} }

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