51 Commits

Author SHA1 Message Date
45506c8772 Added cert resolve viewer
- Added certificate resolve viewer on HTTP proxy rule editor
- Exposed SNI options (wip)
- Code optimize
2025-07-07 14:18:10 +08:00
c091b9d1ca Added content security policy structure
- Added content security policy header generators structure (current not in used)
2025-07-07 13:25:07 +08:00
691cb603ce Merge pull request #724 from 7brend7/sort-list-of-certificates
sort list of loaded certificates by ExpireDate
2025-07-07 10:51:35 +08:00
e53724d6e5 sort list of loaded certificates by ExpireDate 2025-07-06 22:40:10 +03:00
273cae2a98 Merge pull request #719 from jemmy1794/main
Add EnableLogging to Stream Proxy for log control
2025-07-03 20:25:39 +08:00
6b3b89f7bf Add EnableLogging to Stream Proxy for log control
- Add `EnableLogging` to control TCP/UDP Connection logs to reduce log latency.
- Add `Enable Logging` Option in Stream Proxy rule.
- Update Stream Proxy UI.
2025-07-03 09:01:46 +08:00
2d611a559a Optimized structure for stream proxy
- Separated instance and config for stream proxy
2025-07-02 21:03:57 +08:00
6c5eba01c2 Update README.md
Added more contributors in community maintained section name list
2025-07-02 20:42:14 +08:00
f641797d10 Merge pull request #718 from jemmy1794/Stream-Proxy
Add Proxy Protocol V1 option in TCP Stream Proxy and update Stream Proxy UI
2025-07-02 20:40:08 +08:00
f92ff068f3 Added Proxy Protocol V1 to Stream Proxy UI
- Added a checkbox for Proxy Protocol V1.
- Modified related Config setting function.
2025-07-02 18:04:26 +08:00
b59ac47c8c Added Proxy Protocol V1 function.
- Added useProxyProtocol in ProxyRelayConfig
- Added writeProxyProtocolHeaderV1 function
2025-07-02 17:58:26 +08:00
8030f3d62a Fixed #688
- Added auto restart after config change in static web server
2025-06-30 20:34:42 +08:00
f8f623e3e4 Update .gitignore
Ignored dist folder
2025-06-28 17:00:31 +08:00
061839756c Merge pull request #711 from Morethanevil/main
Update CHANGELOG.md
2025-06-28 14:34:58 +08:00
1dcaa0c257 Update CHANGELOG.md 2025-06-28 08:31:20 +02:00
ffd3909964 Merge pull request #710 from tobychui/v3.2.4
V3.2.4 update
2025-06-28 10:06:23 +08:00
3ddccdffce Merge branch 'v3.2.4' of https://github.com/tobychui/zoraxy into v3.2.4 2025-06-27 22:02:29 +08:00
929d4cc82a Optimized SSO UI
- Added tab menu to SSO settings
2025-06-27 22:02:28 +08:00
4f1cd8a571 Merge pull request #705 from jemmy1794/v3.2.4
Fix: #659
2025-06-24 14:24:26 +08:00
f6b3656bb1 Fix: #659
Listen UDP port on (0.0.0.0)* address.
2025-06-24 13:10:58 +08:00
74a816216e Merge pull request #702 from PassiveLemon/main
Release type Docker workflows
2025-06-19 07:11:14 +08:00
4a093cf096 Merge branch 'tobychui:main' into main 2025-06-18 16:54:24 -04:00
68f9fccf3a refactor: release type workflows 2025-06-18 16:53:51 -04:00
f276040ad0 Added experimental fix for #695
Added prefix trim and location filter for oauth authrozied redirection
2025-06-16 21:21:50 +08:00
2f40593daf Updated version code 2025-06-16 21:12:49 +08:00
0b6dbd49bb Fixed #694
- Uncommented the delete proxy rule button
- Added redirection path escape in dpcore
2025-06-16 20:16:36 +08:00
eb07917c14 Merge pull request #693 from tobychui/v3.2.3
- Added new HTTP proxy UI
- Added inbound host name edit function
- Merged SSO implementations
- Added disable chunked transfer encoding checkbox
2025-06-15 21:54:29 +08:00
217bc48001 Merge branch 'main' into v3.2.3 2025-06-15 21:49:48 +08:00
38cfab4a09 Merge pull request #692 from james-d-elliott/feat-forward-auth-improvements
feat(sso): forward auth improvements
2025-06-15 14:50:29 +08:00
217e5e90ff Fixed #672
Fixing a minor log print logic
2025-06-15 13:56:10 +08:00
4a37a989a0 Added Disable Chunk Transfer Encoding option
- Added disable chunk transfer encoding on UI #685
- Added optional to disable static web server listen to all interface #688
2025-06-15 13:46:35 +08:00
eb540b774d refactor: factorize 500 errors
This just factorizes the handling of 500 Internal Server Errors.
2025-06-15 12:14:14 +10:00
26d03f9ad4 feat(sso): forward auth improvements
This adds a couple of key improvements to the Forward Auth SSO implementation. Primarily it adds an included cookies setting which allows filtering cookies to the authorization server. Secondly it fixes a bug where the headerCopyIncluded function was case-sensitive. Documentation in the code and on the web UI is clearer to resolve some common questions and issues. Lastly it moves a lot of funcs to the util.go file and adds fairly comprehensive tests.
2025-06-15 11:57:38 +10:00
31ba4f20ae Added inbound hostname edit function
- Added inbound hostname edit function
- Removed all "proxy root" and replaced with "default site"
2025-06-12 20:53:06 +08:00
650d61ba24 Added plugin doc link to homepage
- Fixed plugin doc css error
- Added plugin doc link to homepage footer
2025-06-12 19:26:15 +08:00
6d0c0be8c2 Added RWD to new HTTP Proxy UI 2025-06-11 21:24:43 +08:00
366a44a992 Restored old httprp page
- Restored old httprp page
- Moved dev page to httprp_new.html
2025-06-10 22:19:33 +08:00
7164b74d4a Merge pull request #687 from tobychui/main
synchronize changes from main
2025-06-10 22:15:59 +08:00
b01a21f318 Merge branch 'v3.2.3' into main 2025-06-10 22:11:50 +08:00
809e1fa815 Flattened HTTP proxy rule edit menu 2025-06-10 22:04:04 +08:00
c7b5e0994e Added more wip UI elements 2025-06-09 22:01:30 +08:00
1f8684481a New UI for proxy editor 2025-06-08 21:56:51 +08:00
0e74ff69c3 Fixed build error after merge
- Fixed buid error in new merge for Oauth2
- Updated version no.
- Optimized css in sso page
2025-06-07 12:20:09 +08:00
f0fa71c5b4 Update .gitignore 2025-06-07 12:08:45 +08:00
8cb47e19fa Merge branch 'main' of https://github.com/kjagosz/zoraxy into v3.2.3 2025-06-07 12:08:23 +08:00
475650de0d Updated readme
- Updated readme
- Updated social banner and title assets
2025-05-31 15:05:36 +08:00
b19867865c Merge branch 'main' of https://github.com/tobychui/zoraxy 2025-05-31 14:42:08 +08:00
df636c9f76 Added link to plugin doc 2025-05-31 14:41:33 +08:00
e6b2cf09d7 Merge pull request #679 from tobychui/plugin_doc
- Added plugin documentation
- Added documentation generator (markdown to html)
- Added API doc generating script (using go doc command)
- Added example plugins for static and dynamic captures
- Removed ztnc from plugin example doc (moved to official plugin repo)
2025-05-31 14:39:31 +08:00
e2882b6436 Some cleanup for unused things 2025-04-29 14:46:18 +02:00
61b873451f Added OAuth2 support for SSO 2025-04-29 01:05:48 +02:00
55 changed files with 2998 additions and 837 deletions

View File

@ -2,7 +2,7 @@ name: Build and push Docker image
on:
release:
types: [ published ]
types: [ released, prereleased ]
jobs:
setup-build-push:
@ -33,7 +33,8 @@ jobs:
run: |
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/src/
- name: Build and push Docker image
- name: Build and push Docker image (Release)
if: "!github.event.release.prerelease"
uses: docker/build-push-action@v6
with:
context: ./docker
@ -45,3 +46,15 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push Docker image (Prerelease)
if: "github.event.release.prerelease"
uses: docker/build-push-action@v6
with:
context: ./docker
push: true
platforms: linux/amd64,linux/arm64
tags: |
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
cache-from: type=gha
cache-to: type=gha,mode=max

8
.gitignore vendored
View File

@ -50,5 +50,11 @@ src/log/
example/plugins/ztnc/ztnc.db
example/plugins/ztnc/authtoken.secret
example/plugins/ztnc/ztnc.db.lock
docs/plugins/docs.exe
.idea
conf
log
tmp
sys.*
www/html/index.html
*.exe
/src/dist

View File

@ -1,3 +1,36 @@
# v3.2.4 28 Jun 2025
A big release since v3.1.9. Versions from 3.2.0 to 3.2.3 were prereleases.
+ Added Authentik support by [JokerQyou](https://github.com/tobychui/zoraxy/commits?author=JokerQyou)
+ Added pluginsystem and moved GAN and Zerotier to plugins
+ Add loopback detection [#573](https://github.com/tobychui/zoraxy/issues/573)
+ Fixed Dark theme not working with Advanced Option accordion [#591](https://github.com/tobychui/zoraxy/issues/591)
+ Update logger to include UserAgent by [Raithmir](https://github.com/Raithmir)
+ Fixed memory usage in UI [#600](https://github.com/tobychui/zoraxy/issues/600)
+ Added docker-compose.yml by [SamuelPalubaCZ](https://github.com/tobychui/zoraxy/commits?author=SamuelPalubaCZ)
+ Added more statistics for proxy hosts [#201](https://github.com/tobychui/zoraxy/issues/201) and [#608](https://github.com/tobychui/zoraxy/issues/608)
+ Fixed origin field in logs [#618](https://github.com/tobychui/zoraxy/issues/618)
+ Added FreeBSD support by Andreas Burri
+ Fixed HTTP proxy redirect [#626](https://github.com/tobychui/zoraxy/issues/626)
+ Fixed proxy handling #629](https://github.com/tobychui/zoraxy/issues/629)
+ Move Scope ID handling into CIDR check by [Nirostar](https://github.com/tobychui/zoraxy/commits?author=Nirostar)
+ Prevent the browser from filling the saved Zoraxy login account by [WHFo](https://github.com/tobychui/zoraxy/commits?author=WHFo)
+ Added port number and http proto to http proxy list link
+ Fixed headers for authelia by [james-d-elliott](https://github.com/tobychui/zoraxy/commits?author=james-d-elliott)
+ Refactored docker container list and UI improvements by [eyerrock](https://github.com/tobychui/zoraxy/commits?author=eyerrock)
+ Refactored Dockerfile by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
+ Added new HTTP proxy UI
+ Added inbound host name edit function
+ Added static web server option to disable listen to all interface
+ Merged SSO implementations (Oauth2) [#649](https://github.com/tobychui/zoraxy/pull/649)
+ Merged forward-auth optimization [#692(https://github.com/tobychui/zoraxy/pull/692)
+ Optimized SSO UI
+ Refactored docker image workflows by [PassiveLemon](https://github.com/tobychui/zoraxy/commits?author=PassiveLemon)
+ Added disable chunked transfer encoding checkbox (for upstreams that uses legacy HTTP implementations)
+ Bug fixes [#694](https://github.com/tobychui/zoraxy/issues/694), [#659](https://github.com/tobychui/zoraxy/issues/659) by [jemmy1794](https://github.com/tobychui/zoraxy/commits?author=jemmy1794), [#695](https://github.com/tobychui/zoraxy/issues/695)
# v3.1.9 1 Mar 2025
+ Fixed netstat underflow bug

View File

@ -13,22 +13,24 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
- Basic Auth
- Alias Hostnames
- Custom Headers
- Load Balancing
- Redirection Rules
- TLS / SSL setup and deploy
- ACME features like auto-renew to serve your sites in http**s**
- 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)
- Global Area Network Controller Web UI (ZeroTier not included)
- Stream Proxy (TCP & UDP)
- Integrated Up-time Monitor
- Web-SSH Terminal
- Plugin System
- Utilities
- CIDR IP converters
- mDNS Scanner
- Wake-On-Lan
- Debug Forward Proxy
- IP Scanner
- Port Scanner
- Others
- Basic single-admin management mode
- External permission management system for easy system integration
@ -107,6 +109,8 @@ Usage of zoraxy:
If web server is enabled by default (default true)
-default_inbound_port int
Default web server listening port (default 443)
-dev
Use external web folder for UI development
-docker
Run Zoraxy in docker compatibility mode
-earlyrenew int
@ -164,19 +168,7 @@ There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychu
## Global Area Network Controller
This project also compatible with [ZeroTier](https://www.zerotier.com/). However, due to licensing issues, ZeroTier is not included in the binary.
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags:
```bash
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
```
The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
This allows you to have an infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
Moved to official plugin repo, see [ztnc](https://github.com/aroz-online/zoraxy-official-plugins/tree/main/src/ztnc) plugin
## Web SSH
@ -199,10 +191,24 @@ Loopback web SSH connections, by default, are disabled. This means that if you a
Some section of Zoraxy are contributed by our amazing community and if you have any issues regarding those sections, it would be more efficient if you can tag them directly when creating an issue report.
- Authelia Support added by [@7brend7](https://github.com/7brend7)
- Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
- Forward Auth [@james-d-elliott](https://github.com/james-d-elliott)
- (Legacy) Authelia Support added by [@7brend7](https://github.com/7brend7)
- (Legacy) Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
- Stream Proxy [@jemmy1794](https://github.com/jemmy1794)
- Change Log [@Morethanevil](https://github.com/Morethanevil)
### Looking for Maintainer
- ACME DNS Challenge Module
- Logging (including analysis & attack prevention) Module
Thank you so much for your contributions!
## Sponsor This Project
@ -210,7 +216,7 @@ Thank you so much for your contributions!
If you like the project and want to support us, please consider a donation. You can use the links below
- [tobychui (Primary author)](https://paypal.me/tobychui)
- PassiveLemon (Docker compatibility maintainer)
- [PassiveLemon (Docker compatibility maintainer)](https://github.com/PassiveLemon)
## License

View File

@ -457,7 +457,7 @@
</div>
</a>
<i class="divider"> </i>
<a class="section externallink" href="" target="_blank">
<a class="section externallink" href="https://zoraxy.aroz.org/plugins/html/" target="_blank">
<div class="ui icon header">
<i class="green code icon"></i>
<div class="content" i18n>
@ -519,8 +519,8 @@
</div>
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki" target="_blank">Zoraxy Wiki</a></div>
<div class="item"><a href="https://github.com/tobychui/zoraxy" target="_blank">Source Code</a></div>
<div class="item"><a href="" target="_blank">Offical Plugin List</a></div>
<div class="item"><a href="" target="_blank">Plugin Development Guide</a></div>
<div class="item"><a href="https://github.com/aroz-online/zoraxy-official-plugins" target="_blank">Offical Plugin List</a></div>
<div class="item"><a href="https://zoraxy.aroz.org/plugins/html/" target="_blank">Plugin Development Guide</a></div>
</div>
</div>
<div class="three wide column">

View File

@ -318,7 +318,7 @@ If everything is correctly setup, you should see the following page when request
Example terminal output for requesting `/foobar/*`:
```
```html
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET

View File

@ -615,7 +615,7 @@ func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
:
</p>
</p>
<pre><span class="ts-text is-code">[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
<pre><code class="language-html">[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Request captured by dynamic sniff path: /d_sniff/
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Header:
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Method: GET
[2025-05-30 20:44:26.142645] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Hostname: a.localhost
@ -641,7 +641,7 @@ func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] Proto: HTTP/2.0
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMajor: 2
[2025-05-30 20:44:26.143165] [plugin-manager] [system:info] [Dynamic Capture Example:22964] ProtoMinor: 0
</span></pre>
</code></pre>
<div class="ts-divider has-top-spaced-large"></div>
<p>
<p class="ts-text">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

View File

@ -34,6 +34,8 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
authRouter.HandleFunc("/api/proxy/setTlsConfig", ReverseProxyHandleSetTlsConfig)
authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname)
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS)
@ -78,11 +80,13 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
authRouter.HandleFunc("/api/cert/resolve", handleCertTryResolve)
}
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions)
authRouter.HandleFunc("/api/sso/OAuth2", oauth2Router.HandleSetOAuth2Settings)
}
// Register the APIs for redirection rules management functions
@ -191,6 +195,7 @@ func RegisterStaticWebServerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
authRouter.HandleFunc("/api/webserv/disableListenAllInterface", staticWebServer.SetDisableListenToAllInterface)
/* File Manager */
if *allowWebFileManager {
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)

View File

@ -9,6 +9,7 @@ import (
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
@ -101,6 +102,13 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
results = append(results, &thisCertInfo)
}
// convert ExpireDate to date object and sort asc
sort.Slice(results, func(i, j int) bool {
date1, _ := time.Parse("2006-01-02 15:04:05", results[i].ExpireDate)
date2, _ := time.Parse("2006-01-02 15:04:05", results[j].ExpireDate)
return date1.Before(date2)
})
js, _ := json.Marshal(results)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
@ -352,6 +360,87 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "File upload successful!")
}
func handleCertTryResolve(w http.ResponseWriter, r *http.Request) {
// get the domain
domain, err := utils.GetPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "invalid domain given")
return
}
// get the proxy rule, the pass in domain value must be root or matching domain
proxyRule, err := dynamicProxyRouter.GetProxyEndpointById(domain, false)
if err != nil {
//Try to resolve the domain via alias
proxyRule, err = dynamicProxyRouter.GetProxyEndpointByAlias(domain)
if err != nil {
//No matching rule found
utils.SendErrorResponse(w, "proxy rule not found for domain: "+domain)
return
}
}
// list all the alias domains for this rule
allDomains := []string{proxyRule.RootOrMatchingDomain}
aliasDomains := []string{}
for _, alias := range proxyRule.MatchingDomainAlias {
if alias != "" {
aliasDomains = append(aliasDomains, alias)
allDomains = append(allDomains, alias)
}
}
// Try to resolve the domain
domainKeyPairs := map[string]string{}
for _, thisDomain := range allDomains {
pubkey, prikey, err := tlsCertManager.GetCertificateByHostname(thisDomain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Make sure pubkey and private key are not empty
if pubkey == "" || prikey == "" {
domainKeyPairs[thisDomain] = ""
} else {
//Store the key pair
keyname := strings.TrimSuffix(filepath.Base(pubkey), filepath.Ext(pubkey))
if keyname == "localhost" {
//Internal certs like localhost should not be used
//report as "fallback" key
keyname = "fallback certificate"
}
domainKeyPairs[thisDomain] = keyname
}
}
//A domain must be UseDNSValidation if it is a wildcard domain or its alias is a wildcard domain
useDNSValidation := strings.HasPrefix(proxyRule.RootOrMatchingDomain, "*")
for _, alias := range aliasDomains {
if strings.HasPrefix(alias, "*") || strings.HasPrefix(domain, "*") {
useDNSValidation = true
}
}
type CertInfo struct {
Domain string `json:"domain"`
AliasDomains []string `json:"alias_domains"`
DomainKeyPair map[string]string `json:"domain_key_pair"`
UseDNSValidation bool `json:"use_dns_validation"`
}
result := &CertInfo{
Domain: proxyRule.RootOrMatchingDomain,
AliasDomains: aliasDomains,
DomainKeyPair: domainKeyPairs,
UseDNSValidation: useDNSValidation,
}
js, _ := json.Marshal(result)
utils.SendJSONResponse(w, string(js))
}
// Handle cert remove
func handleCertRemove(w http.ResponseWriter, r *http.Request) {
domain, err := utils.PostPara(r, "domain")

View File

@ -15,6 +15,7 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/utils"
)
@ -59,12 +60,18 @@ func LoadReverseProxyConfig(configFilepath string) error {
thisConfigEndpoint.Tags = []string{}
}
//Make sure the TLS options are not nil
if thisConfigEndpoint.TlsOptions == nil {
thisConfigEndpoint.TlsOptions = tlscert.GetDefaultHostSpecificTlsBehavior()
}
//Matching domain not set. Assume root
if thisConfigEndpoint.RootOrMatchingDomain == "" {
thisConfigEndpoint.RootOrMatchingDomain = "/"
}
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
switch thisConfigEndpoint.ProxyType {
case dynamicproxy.ProxyTypeRoot:
//This is a root config file
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
@ -73,7 +80,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost {
case dynamicproxy.ProxyTypeHost:
//This is a host config file
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
@ -81,7 +88,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
}
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
} else {
default:
return errors.New("not supported proxy type")
}

View File

@ -13,6 +13,8 @@ import (
"net/http"
"time"
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
@ -42,7 +44,7 @@ import (
const (
/* Build Constants */
SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.2.2"
SYSTEM_VERSION = "3.2.4"
DEVELOPMENT_BUILD = false
/* System Constants */
@ -144,6 +146,7 @@ var (
//Authentication Provider
forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
oauth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending

View File

@ -16,35 +16,18 @@ require (
github.com/grandcat/zeroconf v1.0.0
github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.26
github.com/monperrus/crawler-user-agents v1.1.0
github.com/shirou/gopsutil/v4 v4.25.1
github.com/stretchr/testify v1.10.0
github.com/syndtr/goleveldb v1.0.0
golang.org/x/net v0.33.0
golang.org/x/oauth2 v0.24.0
golang.org/x/text v0.21.0
)
require (
cloud.google.com/go/auth v0.13.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
github.com/monperrus/crawler-user-agents v1.1.0 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/peterhellberg/link v1.2.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/vultr/govultr/v3 v3.9.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
golang.org/x/sys v0.28.0 // indirect
)
require (
cloud.google.com/go/compute/metadata v0.6.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
@ -53,6 +36,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
@ -82,6 +66,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@ -94,6 +79,7 @@ require (
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
@ -102,11 +88,14 @@ require (
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-resty/resty/v2 v2.16.2 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
@ -119,6 +108,7 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
@ -155,29 +145,36 @@ require (
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/ovh/go-ovh v1.6.0 // indirect
github.com/peterhellberg/link v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/sacloud/api-client-go v0.2.10 // indirect
github.com/sacloud/go-http v0.1.8 // indirect
github.com/sacloud/iaas-api-go v1.14.0 // indirect
github.com/sacloud/packages-go v0.0.10 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.1.7 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/vultr/govultr/v3 v3.9.1 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
@ -187,8 +184,8 @@ require (
go.uber.org/ratelimit v0.3.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect
google.golang.org/api v0.214.0 // indirect

View File

@ -37,6 +37,7 @@ import (
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
@ -125,7 +126,13 @@ func main() {
//Start the finalize sequences
finalSequence()
if strings.HasPrefix(*webUIPort, ":") {
//Bind to all interfaces, issue #672
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://localhost" + *webUIPort)
} else {
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://" + *webUIPort)
}
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
if err != nil {

View File

@ -56,7 +56,7 @@ func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter,
return
}
useHTTPS, err := utils.PostBool(r, "useHTTPS")
useHTTPS, err := utils.PostBool(r, "authentikUseHttps")
if err != nil {
useHTTPS = false
}

View File

@ -11,6 +11,7 @@ const (
DatabaseKeyResponseHeaders = "responseHeaders"
DatabaseKeyResponseClientHeaders = "responseClientHeaders"
DatabaseKeyRequestHeaders = "requestHeaders"
DatabaseKeyRequestIncludedCookies = "requestIncludedCookies"
DatabaseKeyRequestExcludedCookies = "requestExcludedCookies"
HeaderXForwardedProto = "X-Forwarded-Proto"

View File

@ -3,7 +3,6 @@ package forward
import (
"encoding/json"
"io"
"net"
"net/http"
"strings"
@ -28,6 +27,10 @@ type AuthRouterOptions struct {
// headers are copied.
RequestHeaders []string
// RequestIncludedCookies is a list of cookie keys that if defined will be the only cookies sent in the request to
// the authorization server.
RequestIncludedCookies []string
// RequestExcludedCookies is a list of cookie keys that should be removed from every request sent to the upstream.
RequestExcludedCookies []string
@ -47,16 +50,18 @@ func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
//Read settings from database if available.
options.Database.Read(DatabaseTable, DatabaseKeyAddress, &options.Address)
responseHeaders, responseClientHeaders, requestHeaders, requestExcludedCookies := "", "", "", ""
responseHeaders, responseClientHeaders, requestHeaders, requestIncludedCookies, requestExcludedCookies := "", "", "", "", ""
options.Database.Read(DatabaseTable, DatabaseKeyResponseHeaders, &responseHeaders)
options.Database.Read(DatabaseTable, DatabaseKeyResponseClientHeaders, &responseClientHeaders)
options.Database.Read(DatabaseTable, DatabaseKeyRequestHeaders, &requestHeaders)
options.Database.Read(DatabaseTable, DatabaseKeyRequestIncludedCookies, &requestIncludedCookies)
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
options.ResponseHeaders = strings.Split(responseHeaders, ",")
options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
options.RequestHeaders = strings.Split(requestHeaders, ",")
options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",")
options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
return &AuthRouter{
@ -82,11 +87,12 @@ func (ar *AuthRouter) HandleAPIOptions(w http.ResponseWriter, r *http.Request) {
}
func (ar *AuthRouter) handleOptionsGET(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(map[string]interface{}{
js, _ := json.Marshal(map[string]any{
DatabaseKeyAddress: ar.options.Address,
DatabaseKeyResponseHeaders: ar.options.ResponseHeaders,
DatabaseKeyResponseClientHeaders: ar.options.ResponseClientHeaders,
DatabaseKeyRequestHeaders: ar.options.RequestHeaders,
DatabaseKeyRequestIncludedCookies: ar.options.RequestIncludedCookies,
DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies,
})
@ -108,6 +114,7 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
responseHeaders, _ := utils.PostPara(r, DatabaseKeyResponseHeaders)
responseClientHeaders, _ := utils.PostPara(r, DatabaseKeyResponseClientHeaders)
requestHeaders, _ := utils.PostPara(r, DatabaseKeyRequestHeaders)
requestIncludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestIncludedCookies)
requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies)
// Write changes to runtime
@ -115,6 +122,7 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
ar.options.ResponseHeaders = strings.Split(responseHeaders, ",")
ar.options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
ar.options.RequestHeaders = strings.Split(requestHeaders, ",")
ar.options.RequestIncludedCookies = strings.Split(requestIncludedCookies, ",")
ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
// Write changes to database
@ -122,6 +130,7 @@ func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request)
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders)
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseClientHeaders, responseClientHeaders)
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestHeaders, requestHeaders)
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestIncludedCookies, requestIncludedCookies)
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies)
utils.SendOK(w)
@ -136,60 +145,40 @@ func (ar *AuthRouter) handleOptionsMethodNotAllowed(w http.ResponseWriter, r *ht
// HandleAuthProviderRouting is the internal handler for Forward Auth authentication.
func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.Request) error {
if ar.options.Address == "" {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Address not set", nil)
return ErrInternalServerError
return ar.handle500Error(w, nil, "Address not set")
}
// Make a request to Authz Server to verify the request
// TODO: Add opt-in support for copying the request body to the forward auth request. Currently it's just an
// empty body which is usually fine in most instances. It's likely best to see if anyone wants this feature
// as I'm unaware of any specific forward auth implementation that needs it.
req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to create request", err)
return ErrInternalServerError
return ar.handle500Error(w, err, "Unable to create request")
}
// TODO: Add opt-in support for copying the request body to the forward auth request.
headerCopyIncluded(r.Header, req.Header, ar.options.RequestHeaders, true)
headerCookieRedact(r, ar.options.RequestIncludedCookies, false)
// TODO: Add support for upstream headers.
// TODO: Add support for headers from upstream proxies. This will likely involve implementing some form of
// proxy specific trust system within Zoraxy.
rSetForwardedHeaders(r, req)
// Make the Authz Request.
respForwarded, err := ar.client.Do(req)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to perform forwarded auth due to a request error", err)
return ErrInternalServerError
return ar.handle500Error(w, err, "Unable to perform forwarded auth due to a request error")
}
defer respForwarded.Body.Close()
body, err := io.ReadAll(respForwarded.Body)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to read response to forward auth request", err)
return ErrInternalServerError
}
// Responses within the 200-299 range are considered successful and allow the proxy to handle the request.
if respForwarded.StatusCode >= http.StatusOK && respForwarded.StatusCode < http.StatusMultipleChoices {
if len(ar.options.ResponseClientHeaders) != 0 {
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseClientHeaders, false)
}
if len(ar.options.RequestExcludedCookies) != 0 {
// If the user has specified a list of cookies to be removed from the request, deterministically remove them.
headerCookieRedact(r, ar.options.RequestExcludedCookies)
}
headerCookieRedact(r, ar.options.RequestExcludedCookies, true)
if len(ar.options.ResponseHeaders) != 0 {
// Copy specific user-specified headers from the response of the forward auth request to the request sent to the
@ -197,138 +186,32 @@ func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.R
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseHeaders, false)
}
// Return the request to the proxy for forwarding to the backend.
return nil
}
// Copy the response.
// Copy the unsuccessful response.
headerCopyExcluded(respForwarded.Header, w.Header(), nil)
w.WriteHeader(respForwarded.StatusCode)
body, err := io.ReadAll(respForwarded.Body)
if err != nil {
return ar.handle500Error(w, err, "Unable to read response to forward auth request")
}
if _, err = w.Write(body); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to write response", err)
return ErrInternalServerError
return ar.handle500Error(w, err, "Unable to write response")
}
return ErrUnauthorized
}
func scheme(r *http.Request) string {
if r.TLS != nil {
return "https"
}
// handle500Error is func intended on factorizing a commonly repeated functional flow within this provider.
func (ar *AuthRouter) handle500Error(w http.ResponseWriter, err error, message string) error {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return "http"
}
ar.options.Logger.PrintAndLog(LogTitle, message, err)
func headerCookieRedact(r *http.Request, excluded []string) {
original := r.Cookies()
if len(original) == 0 {
return
}
var cookies []string
for _, cookie := range original {
if stringInSlice(cookie.Name, excluded) {
continue
}
cookies = append(cookies, cookie.String())
}
r.Header.Set(HeaderCookie, strings.Join(cookies, "; "))
}
func headerCopyExcluded(original, destination http.Header, excludedHeaders []string) {
for key, values := range original {
// We should never copy the headers in the below list.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
if stringInSliceFold(key, excludedHeaders) {
continue
}
destination[key] = append(destination[key], values...)
}
}
func headerCopyIncluded(original, destination http.Header, includedHeaders []string, allIfEmpty bool) {
if allIfEmpty && len(includedHeaders) == 0 {
headerCopyAll(original, destination)
} else {
headerCopyIncludedExact(original, destination, includedHeaders)
}
}
func headerCopyAll(original, destination http.Header) {
for key, values := range original {
// We should never copy the headers in the below list, even if they're in the list provided by a user.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
destination[key] = append(destination[key], values...)
}
}
func headerCopyIncludedExact(original, destination http.Header, keys []string) {
for _, key := range keys {
// We should never copy the headers in the below list, even if they're in the list provided by a user.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
if values, ok := original[key]; ok {
destination[key] = append(destination[key], values...)
}
}
}
func stringInSlice(needle string, haystack []string) bool {
if len(haystack) == 0 {
return false
}
for _, v := range haystack {
if needle == v {
return true
}
}
return false
}
func stringInSliceFold(needle string, haystack []string) bool {
if len(haystack) == 0 {
return false
}
for _, v := range haystack {
if strings.EqualFold(needle, v) {
return true
}
}
return false
}
func rSetForwardedHeaders(r, req *http.Request) {
if r.RemoteAddr != "" {
before, _, _ := strings.Cut(r.RemoteAddr, ":")
if ip := net.ParseIP(before); ip != nil {
req.Header.Set(HeaderXForwardedFor, ip.String())
}
}
req.Header.Set(HeaderXForwardedMethod, r.Method)
req.Header.Set(HeaderXForwardedProto, scheme(r))
req.Header.Set(HeaderXForwardedHost, r.Host)
req.Header.Set(HeaderXForwardedURI, r.URL.Path)
return ErrInternalServerError
}

View File

@ -0,0 +1,137 @@
package forward
import (
"net"
"net/http"
"strings"
)
func scheme(r *http.Request) string {
if r.TLS != nil {
return "https"
}
return "http"
}
func headerCookieRedact(r *http.Request, names []string, exclude bool) {
if len(names) == 0 {
return
}
original := r.Cookies()
if len(original) == 0 {
return
}
var cookies []string
for _, cookie := range original {
if exclude && stringInSlice(cookie.Name, names) {
continue
} else if !exclude && !stringInSlice(cookie.Name, names) {
continue
}
cookies = append(cookies, cookie.String())
}
value := strings.Join(cookies, "; ")
r.Header.Set(HeaderCookie, value)
return
}
func headerCopyExcluded(original, destination http.Header, excludedHeaders []string) {
for key, values := range original {
// We should never copy the headers in the below list.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
if stringInSliceFold(key, excludedHeaders) {
continue
}
destination[key] = append(destination[key], values...)
}
}
func headerCopyIncluded(original, destination http.Header, includedHeaders []string, allIfEmpty bool) {
if allIfEmpty && len(includedHeaders) == 0 {
headerCopyAll(original, destination)
} else {
headerCopyIncludedExact(original, destination, includedHeaders)
}
}
func headerCopyAll(original, destination http.Header) {
for key, values := range original {
// We should never copy the headers in the below list, even if they're in the list provided by a user.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
destination[key] = append(destination[key], values...)
}
}
func headerCopyIncludedExact(original, destination http.Header, keys []string) {
for key, values := range original {
// We should never copy the headers in the below list, even if they're in the list provided by a user.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
if !stringInSliceFold(key, keys) {
continue
}
destination[key] = append(destination[key], values...)
}
}
func stringInSlice(needle string, haystack []string) bool {
if len(haystack) == 0 {
return false
}
for _, v := range haystack {
if needle == v {
return true
}
}
return false
}
func stringInSliceFold(needle string, haystack []string) bool {
if len(haystack) == 0 {
return false
}
for _, v := range haystack {
if strings.EqualFold(needle, v) {
return true
}
}
return false
}
func rSetForwardedHeaders(r, req *http.Request) {
if r.RemoteAddr != "" {
before, _, _ := strings.Cut(r.RemoteAddr, ":")
if ip := net.ParseIP(before); ip != nil {
req.Header.Set(HeaderXForwardedFor, ip.String())
}
}
req.Header.Set(HeaderXForwardedMethod, r.Method)
req.Header.Set(HeaderXForwardedProto, scheme(r))
req.Header.Set(HeaderXForwardedHost, r.Host)
req.Header.Set(HeaderXForwardedURI, r.URL.Path)
}

View File

@ -0,0 +1,217 @@
package forward
import (
"crypto/tls"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestScheme(t *testing.T) {
testCases := []struct {
name string
have *http.Request
expected string
}{
{
"ShouldHandleDefault",
&http.Request{},
"http",
},
{
"ShouldHandleExplicit",
&http.Request{
TLS: nil,
},
"http",
},
{
"ShouldHandleHTTPS",
&http.Request{
TLS: &tls.ConnectionState{},
},
"https",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, scheme(tc.have))
})
}
}
func TestHeaderCookieRedact(t *testing.T) {
testCases := []struct {
name string
have string
names []string
expectedInclude string
expectedExclude string
}{
{
"ShouldHandleIncludeEmptyWithoutSettings",
"",
nil,
"",
"",
},
{
"ShouldHandleIncludeEmptyWithSettings",
"",
[]string{"include"},
"",
"",
},
{
"ShouldHandleValueWithoutSettings",
"include=value; exclude=value",
nil,
"include=value; exclude=value",
"include=value; exclude=value",
},
{
"ShouldHandleValueWithSettings",
"include=value; exclude=value",
[]string{"include"},
"include=value",
"exclude=value",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var include, exclude *http.Request
include, exclude = &http.Request{Header: http.Header{}}, &http.Request{Header: http.Header{}}
if tc.have != "" {
include.Header.Set(HeaderCookie, tc.have)
exclude.Header.Set(HeaderCookie, tc.have)
}
headerCookieRedact(include, tc.names, false)
assert.Equal(t, tc.expectedInclude, include.Header.Get(HeaderCookie))
headerCookieRedact(exclude, tc.names, true)
assert.Equal(t, tc.expectedExclude, exclude.Header.Get(HeaderCookie))
})
}
}
func TestHeaderCopyExcluded(t *testing.T) {
testCases := []struct {
name string
original http.Header
excluded []string
expected http.Header
}{
{
"ShouldHandleNoSettingsNoHeaders",
http.Header{},
nil,
http.Header{},
},
{
"ShouldHandleNoSettingsWithHeaders",
http.Header{
"Example": []string{"value", "other"},
"Exclude": []string{"value", "other"},
HeaderUpgrade: []string{"do", "not", "copy"},
},
nil,
http.Header{
"Example": []string{"value", "other"},
"Exclude": []string{"value", "other"},
},
},
{
"ShouldHandleSettingsWithHeaders",
http.Header{
"Example": []string{"value", "other"},
"Exclude": []string{"value", "other"},
HeaderUpgrade: []string{"do", "not", "copy"},
},
[]string{"exclude"},
http.Header{
"Example": []string{"value", "other"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
headers := http.Header{}
headerCopyExcluded(tc.original, headers, tc.excluded)
assert.Equal(t, tc.expected, headers)
})
}
}
func TestHeaderCopyIncluded(t *testing.T) {
testCases := []struct {
name string
original http.Header
included []string
expected http.Header
expectedAll http.Header
}{
{
"ShouldHandleNoSettingsNoHeaders",
http.Header{},
nil,
http.Header{},
http.Header{},
},
{
"ShouldHandleNoSettingsWithHeaders",
http.Header{
"Example": []string{"value", "other"},
"Include": []string{"value", "other"},
HeaderUpgrade: []string{"do", "not", "copy"},
},
nil,
http.Header{},
http.Header{
"Example": []string{"value", "other"},
"Include": []string{"value", "other"},
},
},
{
"ShouldHandleSettingsWithHeaders",
http.Header{
"Example": []string{"value", "other"},
"Include": []string{"value", "other"},
HeaderUpgrade: []string{"do", "not", "copy"},
},
[]string{"include"},
http.Header{
"Include": []string{"value", "other"},
},
http.Header{
"Include": []string{"value", "other"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
headers := http.Header{}
headerCopyIncluded(tc.original, headers, tc.included, false)
assert.Equal(t, tc.expected, headers)
headers = http.Header{}
headerCopyIncluded(tc.original, headers, tc.included, true)
assert.Equal(t, tc.expectedAll, headers)
})
}
}

View File

@ -0,0 +1,299 @@
package oauth2
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
"golang.org/x/oauth2"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type OAuth2RouterOptions struct {
OAuth2ServerURL string //The URL of the OAuth 2.0 server server
OAuth2TokenURL string //The URL of the OAuth 2.0 token server
OAuth2ClientId string //The client id for OAuth 2.0 Application
OAuth2ClientSecret string //The client secret for OAuth 2.0 Application
OAuth2WellKnownUrl string //The well-known url for OAuth 2.0 server
OAuth2UserInfoUrl string //The URL of the OAuth 2.0 user info endpoint
OAuth2Scopes string //The scopes for OAuth 2.0 Application
Logger *logger.Logger
Database *database.Database
}
type OIDCDiscoveryDocument struct {
AuthorizationEndpoint string `json:"authorization_endpoint"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
Issuer string `json:"issuer"`
JwksURI string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
ScopesSupported []string `json:"scopes_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
TokenEndpoint string `json:"token_endpoint"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
}
type OAuth2Router struct {
options *OAuth2RouterOptions
}
// NewOAuth2Router creates a new OAuth2Router object
func NewOAuth2Router(options *OAuth2RouterOptions) *OAuth2Router {
options.Database.NewTable("oauth2")
//Read settings from database, if exists
options.Database.Read("oauth2", "oauth2WellKnownUrl", &options.OAuth2WellKnownUrl)
options.Database.Read("oauth2", "oauth2ServerUrl", &options.OAuth2ServerURL)
options.Database.Read("oauth2", "oauth2TokenUrl", &options.OAuth2TokenURL)
options.Database.Read("oauth2", "oauth2ClientId", &options.OAuth2ClientId)
options.Database.Read("oauth2", "oauth2ClientSecret", &options.OAuth2ClientSecret)
options.Database.Read("oauth2", "oauth2UserInfoUrl", &options.OAuth2UserInfoUrl)
options.Database.Read("oauth2", "oauth2Scopes", &options.OAuth2Scopes)
return &OAuth2Router{
options: options,
}
}
// HandleSetOAuth2Settings is the internal handler for setting the OAuth URL and HTTPS
func (ar *OAuth2Router) HandleSetOAuth2Settings(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current settings
js, _ := json.Marshal(map[string]interface{}{
"oauth2WellKnownUrl": ar.options.OAuth2WellKnownUrl,
"oauth2ServerUrl": ar.options.OAuth2ServerURL,
"oauth2TokenUrl": ar.options.OAuth2TokenURL,
"oauth2UserInfoUrl": ar.options.OAuth2UserInfoUrl,
"oauth2Scopes": ar.options.OAuth2Scopes,
"oauth2ClientSecret": ar.options.OAuth2ClientSecret,
"oauth2ClientId": ar.options.OAuth2ClientId,
})
utils.SendJSONResponse(w, string(js))
return
} else if r.Method == http.MethodPost {
//Update the settings
var oauth2ServerUrl, oauth2TokenURL, oauth2Scopes, oauth2UserInfoUrl string
oauth2WellKnownUrl, err := utils.PostPara(r, "oauth2WellKnownUrl")
if err != nil {
oauth2ServerUrl, err = utils.PostPara(r, "oauth2ServerUrl")
if err != nil {
utils.SendErrorResponse(w, "oauth2ServerUrl not found")
return
}
oauth2TokenURL, err = utils.PostPara(r, "oauth2TokenUrl")
if err != nil {
utils.SendErrorResponse(w, "oauth2TokenUrl not found")
return
}
oauth2Scopes, err = utils.PostPara(r, "oauth2Scopes")
if err != nil {
utils.SendErrorResponse(w, "oauth2Scopes not found")
return
}
oauth2UserInfoUrl, err = utils.PostPara(r, "oauth2UserInfoUrl")
if err != nil {
utils.SendErrorResponse(w, "oauth2UserInfoUrl not found")
return
}
}
oauth2ClientId, err := utils.PostPara(r, "oauth2ClientId")
if err != nil {
utils.SendErrorResponse(w, "oauth2ClientId not found")
return
}
oauth2ClientSecret, err := utils.PostPara(r, "oauth2ClientSecret")
if err != nil {
utils.SendErrorResponse(w, "oauth2ClientSecret not found")
return
}
//Write changes to runtime
ar.options.OAuth2WellKnownUrl = oauth2WellKnownUrl
ar.options.OAuth2ServerURL = oauth2ServerUrl
ar.options.OAuth2TokenURL = oauth2TokenURL
ar.options.OAuth2UserInfoUrl = oauth2UserInfoUrl
ar.options.OAuth2ClientId = oauth2ClientId
ar.options.OAuth2ClientSecret = oauth2ClientSecret
ar.options.OAuth2Scopes = oauth2Scopes
//Write changes to database
ar.options.Database.Write("oauth2", "oauth2WellKnownUrl", oauth2WellKnownUrl)
ar.options.Database.Write("oauth2", "oauth2ServerUrl", oauth2ServerUrl)
ar.options.Database.Write("oauth2", "oauth2TokenUrl", oauth2TokenURL)
ar.options.Database.Write("oauth2", "oauth2UserInfoUrl", oauth2UserInfoUrl)
ar.options.Database.Write("oauth2", "oauth2ClientId", oauth2ClientId)
ar.options.Database.Write("oauth2", "oauth2ClientSecret", oauth2ClientSecret)
ar.options.Database.Write("oauth2", "oauth2Scopes", oauth2Scopes)
utils.SendOK(w)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
func (ar *OAuth2Router) fetchOAuth2Configuration(config *oauth2.Config) (*oauth2.Config, error) {
req, err := http.NewRequest("GET", ar.options.OAuth2WellKnownUrl, nil)
if err != nil {
return nil, err
}
client := &http.Client{}
if resp, err := client.Do(req); err != nil {
return nil, err
} else {
defer resp.Body.Close()
oidcDiscoveryDocument := OIDCDiscoveryDocument{}
if err := json.NewDecoder(resp.Body).Decode(&oidcDiscoveryDocument); err != nil {
return nil, err
}
if len(config.Scopes) == 0 {
config.Scopes = oidcDiscoveryDocument.ScopesSupported
}
if config.Endpoint.AuthURL == "" {
config.Endpoint.AuthURL = oidcDiscoveryDocument.AuthorizationEndpoint
}
if config.Endpoint.TokenURL == "" {
config.Endpoint.TokenURL = oidcDiscoveryDocument.TokenEndpoint
}
if ar.options.OAuth2UserInfoUrl == "" {
ar.options.OAuth2UserInfoUrl = oidcDiscoveryDocument.UserinfoEndpoint
}
}
return config, nil
}
func (ar *OAuth2Router) newOAuth2Conf(redirectUrl string) (*oauth2.Config, error) {
config := &oauth2.Config{
ClientID: ar.options.OAuth2ClientId,
ClientSecret: ar.options.OAuth2ClientSecret,
RedirectURL: redirectUrl,
Endpoint: oauth2.Endpoint{
AuthURL: ar.options.OAuth2ServerURL,
TokenURL: ar.options.OAuth2TokenURL,
},
}
if ar.options.OAuth2Scopes != "" {
config.Scopes = strings.Split(ar.options.OAuth2Scopes, ",")
}
if ar.options.OAuth2WellKnownUrl != "" && (config.Endpoint.AuthURL == "" || config.Endpoint.TokenURL == "" ||
ar.options.OAuth2UserInfoUrl == "") {
return ar.fetchOAuth2Configuration(config)
}
return config, nil
}
// HandleOAuth2Auth is the internal handler for OAuth authentication
// Set useHTTPS to true if your OAuth server is using HTTPS
// Set OAuthURL to the URL of the OAuth server, e.g. OAuth.example.com
func (ar *OAuth2Router) HandleOAuth2Auth(w http.ResponseWriter, r *http.Request) error {
const callbackPrefix = "/internal/oauth2"
const tokenCookie = "z-token"
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
reqUrl := scheme + "://" + r.Host + r.RequestURI
oauthConfig, err := ar.newOAuth2Conf(scheme + "://" + r.Host + callbackPrefix)
if err != nil {
ar.options.Logger.PrintAndLog("OAuth2Router", "Failed to fetch OIDC configuration:", err)
w.WriteHeader(500)
return errors.New("failed to fetch OIDC configuration")
}
if oauthConfig.Endpoint.AuthURL == "" || oauthConfig.Endpoint.TokenURL == "" || ar.options.OAuth2UserInfoUrl == "" {
ar.options.Logger.PrintAndLog("OAuth2Router", "Invalid OAuth2 configuration", nil)
w.WriteHeader(500)
return errors.New("invalid OAuth2 configuration")
}
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
if r.Method == http.MethodGet && strings.HasPrefix(r.RequestURI, callbackPrefix) && code != "" && state != "" {
ctx := context.Background()
token, err := oauthConfig.Exchange(ctx, code)
if err != nil {
ar.options.Logger.PrintAndLog("OAuth2", "Token exchange failed", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
if !token.Valid() {
ar.options.Logger.PrintAndLog("OAuth2", "Invalid token", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
cookie := http.Cookie{Name: tokenCookie, Value: token.AccessToken, Path: "/"}
if scheme == "https" {
cookie.Secure = true
cookie.SameSite = http.SameSiteLaxMode
}
w.Header().Add("Set-Cookie", cookie.String())
//Fix for #695
location := strings.TrimPrefix(state, "/internal/")
//Check if the location starts with http:// or https://. if yes, this is full URL
decodedLocation, err := url.PathUnescape(location)
if err == nil && (strings.HasPrefix(decodedLocation, "http://") || strings.HasPrefix(decodedLocation, "https://")) {
//Redirect to the full URL
http.Redirect(w, r, decodedLocation, http.StatusTemporaryRedirect)
} else {
//Redirect to a relative path
http.Redirect(w, r, state, http.StatusTemporaryRedirect)
}
return errors.New("authorized")
}
unauthorized := false
cookie, err := r.Cookie(tokenCookie)
if err == nil {
if cookie.Value == "" {
unauthorized = true
} else {
ctx := context.Background()
client := oauthConfig.Client(ctx, &oauth2.Token{AccessToken: cookie.Value})
req, err := client.Get(ar.options.OAuth2UserInfoUrl)
if err != nil {
ar.options.Logger.PrintAndLog("OAuth2", "Failed to get user info", err)
unauthorized = true
}
defer req.Body.Close()
if req.StatusCode != http.StatusOK {
ar.options.Logger.PrintAndLog("OAuth2", "Failed to get user info", err)
unauthorized = true
}
}
} else {
unauthorized = true
}
if unauthorized {
state := url.QueryEscape(reqUrl)
url := oauthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline)
http.Redirect(w, r, url, http.StatusFound)
return errors.New("unauthorized")
}
return nil
}

View File

@ -46,6 +46,12 @@ func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *htt
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
return true
}
case AuthMethodOauth2:
err := h.handleOAuth2Auth(w, r)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
return true
}
}
//No authentication provider, do not need to handle
@ -108,3 +114,7 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
func (h *ProxyHandler) handleForwardAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.ForwardAuthRouter.HandleAuthProviderRouting(w, r)
}
func (h *ProxyHandler) handleOAuth2Auth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.OAuth2Router.HandleOAuth2Auth(w, r)
}

View File

@ -72,6 +72,7 @@ type ResponseRewriteRuleSet struct {
/* Advance Usecase Options */
HostHeaderOverwrite string //Force overwrite of request "Host" header (advanced usecase)
NoRemoveHopByHop bool //Do not remove hop-by-hop headers (advanced usecase)
DisableChunkedTransferEncoding bool //Disable chunked transfer encoding
/* System Information Payload */
Version string //Version number of Zoraxy, use for X-Proxy-By
@ -287,7 +288,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
rewriteUserAgent(outreq.Header, "Zoraxy/"+rrr.Version)
//Fix proxmox transfer encoding bug if detected Proxmox Cookie
if domainsniff.IsProxmox(req) {
if rrr.DisableChunkedTransferEncoding || domainsniff.IsProxmox(req) {
outreq.TransferEncoding = []string{"identity"}
}
@ -329,7 +330,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
locationRewrite := res.Header.Get("Location")
originLocation := res.Header.Get("Location")
res.Header.Set("zr-origin-location", originLocation)
decodedOriginLocation, err := url.PathUnescape(originLocation)
if err == nil {
originLocation = decodedOriginLocation
}
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
//Full path
//Replace the forwarded target with expected Host

View File

@ -0,0 +1,123 @@
package permissionpolicy
import (
"net/http"
"strings"
)
/*
Content Security Policy
This is a content security policy header modifier that changes
the request content security policy fields
author: tobychui
//TODO: intergrate this with the dynamic proxy module
*/
type ContentSecurityPolicy struct {
DefaultSrc []string `json:"default_src"`
ScriptSrc []string `json:"script_src"`
StyleSrc []string `json:"style_src"`
ImgSrc []string `json:"img_src"`
ConnectSrc []string `json:"connect_src"`
FontSrc []string `json:"font_src"`
ObjectSrc []string `json:"object_src"`
MediaSrc []string `json:"media_src"`
FrameSrc []string `json:"frame_src"`
WorkerSrc []string `json:"worker_src"`
ChildSrc []string `json:"child_src"`
ManifestSrc []string `json:"manifest_src"`
PrefetchSrc []string `json:"prefetch_src"`
FormAction []string `json:"form_action"`
FrameAncestors []string `json:"frame_ancestors"`
BaseURI []string `json:"base_uri"`
Sandbox []string `json:"sandbox"`
ReportURI []string `json:"report_uri"`
ReportTo []string `json:"report_to"`
UpgradeInsecureRequests bool `json:"upgrade_insecure_requests"`
BlockAllMixedContent bool `json:"block_all_mixed_content"`
}
// GetDefaultContentSecurityPolicy returns a ContentSecurityPolicy struct with default permissive settings
func GetDefaultContentSecurityPolicy() *ContentSecurityPolicy {
return &ContentSecurityPolicy{
DefaultSrc: []string{"*"},
ScriptSrc: []string{"*"},
StyleSrc: []string{"*"},
ImgSrc: []string{"*"},
ConnectSrc: []string{"*"},
FontSrc: []string{"*"},
ObjectSrc: []string{"*"},
MediaSrc: []string{"*"},
FrameSrc: []string{"*"},
WorkerSrc: []string{"*"},
ChildSrc: []string{"*"},
ManifestSrc: []string{"*"},
PrefetchSrc: []string{"*"},
FormAction: []string{"*"},
FrameAncestors: []string{"*"},
BaseURI: []string{"*"},
Sandbox: []string{},
ReportURI: []string{},
ReportTo: []string{},
UpgradeInsecureRequests: false,
BlockAllMixedContent: false,
}
}
// ToHeader converts a ContentSecurityPolicy struct into a CSP header key-value pair
func (csp *ContentSecurityPolicy) ToHeader() []string {
directives := []string{}
addDirective := func(name string, sources []string) {
if len(sources) > 0 {
directives = append(directives, name+" "+strings.Join(sources, " "))
}
}
addDirective("default-src", csp.DefaultSrc)
addDirective("script-src", csp.ScriptSrc)
addDirective("style-src", csp.StyleSrc)
addDirective("img-src", csp.ImgSrc)
addDirective("connect-src", csp.ConnectSrc)
addDirective("font-src", csp.FontSrc)
addDirective("object-src", csp.ObjectSrc)
addDirective("media-src", csp.MediaSrc)
addDirective("frame-src", csp.FrameSrc)
addDirective("worker-src", csp.WorkerSrc)
addDirective("child-src", csp.ChildSrc)
addDirective("manifest-src", csp.ManifestSrc)
addDirective("prefetch-src", csp.PrefetchSrc)
addDirective("form-action", csp.FormAction)
addDirective("frame-ancestors", csp.FrameAncestors)
addDirective("base-uri", csp.BaseURI)
if len(csp.Sandbox) > 0 {
directives = append(directives, "sandbox "+strings.Join(csp.Sandbox, " "))
}
if len(csp.ReportURI) > 0 {
addDirective("report-uri", csp.ReportURI)
}
if len(csp.ReportTo) > 0 {
addDirective("report-to", csp.ReportTo)
}
if csp.UpgradeInsecureRequests {
directives = append(directives, "upgrade-insecure-requests")
}
if csp.BlockAllMixedContent {
directives = append(directives, "block-all-mixed-content")
}
headerValue := strings.Join(directives, "; ")
return []string{"Content-Security-Policy", headerValue}
}
// InjectContentSecurityPolicyHeader injects the CSP header into the response
func InjectContentSecurityPolicyHeader(w http.ResponseWriter, csp *ContentSecurityPolicy) {
if csp == nil || w.Header().Get("Content-Security-Policy") != "" {
return
}
headerKV := csp.ToHeader()
w.Header().Set(headerKV[0], headerKV[1])
}

View File

@ -11,7 +11,6 @@ import (
"sort"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils"
@ -193,6 +192,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: "",
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
DisableChunkedTransferEncoding: target.DisableChunkedTransferEncoding,
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
NoRemoveHopByHop: headerRewriteOptions.DisableHopByHopHeaderRemoval,
Version: target.parent.Option.HostVersion,
@ -244,8 +244,8 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
h.Parent.logRequest(r, true, 101, "vdir-websocket", r.Host, target.Domain)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.parent.EnableWebsocketCustomHeaders, //You should not use websocket via virtual directory. But keep this to true for compatibility
CopyAllHeaders: domainsniff.RequireWebsocketHeaderCopy(r), //Left this as default to prevent nginx user setting / as vdir
SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility
CopyAllHeaders: target.parent.EnableWebsocketCustomHeaders, //Left this as default to prevent nginx user setting / as vdir
UserDefinedHeaders: target.parent.HeaderRewriteRules.UserDefinedHeaders,
Logger: h.Parent.Option.Logger,
})
@ -286,6 +286,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: target.MatchingPath,
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
DisableChunkedTransferEncoding: target.parent.DisableChunkedTransferEncoding,
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
Version: target.parent.parent.Option.HostVersion,
})

View File

@ -8,6 +8,7 @@ import (
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/utils"
)
/*
@ -105,3 +106,49 @@ func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain str
return targetEpt.Remove()
}
// GetProxyEndpointById retrieves a proxy endpoint by its ID from the Router's ProxyEndpoints map.
// It returns the ProxyEndpoint if found, or an error if not found.
func (h *Router) GetProxyEndpointById(searchingDomain string, includeAlias bool) (*ProxyEndpoint, error) {
var found *ProxyEndpoint
h.ProxyEndpoints.Range(func(key, value interface{}) bool {
proxy, ok := value.(*ProxyEndpoint)
if ok && (proxy.RootOrMatchingDomain == searchingDomain || (includeAlias && utils.StringInArray(proxy.MatchingDomainAlias, searchingDomain))) {
found = proxy
return false // stop iteration
}
return true // continue iteration
})
if found != nil {
return found, nil
}
return nil, errors.New("proxy rule with given id not found")
}
func (h *Router) GetProxyEndpointByAlias(alias string) (*ProxyEndpoint, error) {
var found *ProxyEndpoint
h.ProxyEndpoints.Range(func(key, value interface{}) bool {
proxy, ok := value.(*ProxyEndpoint)
if !ok {
return true
}
//Also check for wildcard aliases that matches the alias
for _, thisAlias := range proxy.MatchingDomainAlias {
if ok && thisAlias == alias {
found = proxy
return false // stop iteration
} else if ok && strings.HasPrefix(thisAlias, "*") {
//Check if the alias matches a wildcard alias
if strings.HasSuffix(alias, thisAlias[1:]) {
found = proxy
return false // stop iteration
}
}
}
return true // continue iteration
})
if found != nil {
return found, nil
}
return nil, errors.New("proxy rule with given alias not found")
}

View File

@ -13,6 +13,8 @@ import (
"net/http"
"sync"
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/auth/sso/forward"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
@ -64,6 +66,7 @@ type RouterOption struct {
/* Authentication Providers */
ForwardAuthRouter *forward.AuthRouter
OAuth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication
/* Utilities */
Logger *logger.Logger //Logger for reverse proxy requets
@ -173,6 +176,7 @@ type ProxyEndpoint struct {
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
TlsOptions *tlscert.HostSpecificTlsBehavior //TLS options for this endpoint, if nil, use global TLS options
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
@ -191,6 +195,9 @@ type ProxyEndpoint struct {
//Uptime Monitor
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
// Chunked Transfer Encoding
DisableChunkedTransferEncoding bool //Disable chunked transfer encoding for this endpoint
//Access Control
AccessFilterUUID string //Access filter ID

View File

@ -1,5 +1,5 @@
//go:build (windows && amd64) || (linux && mipsle) || (linux && riscv64) || (freebsd && amd64)
// +build windows,amd64 linux,mipsle linux,riscv64 freebsd,amd64
//go:build (windows && amd64) || (linux && mipsle) || (linux && riscv64) || (freebsd && amd64) || (darwin && arm64)
// +build windows,amd64 linux,mipsle linux,riscv64 freebsd,amd64 darwin,arm64
package sshprox

View File

@ -47,6 +47,8 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
useTCP, _ := utils.PostBool(r, "useTCP")
useUDP, _ := utils.PostBool(r, "useUDP")
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
enableLogging, _ := utils.PostBool(r, "enableLogging")
//Create the target config
newConfigUUID := m.NewConfig(&ProxyRelayOptions{
@ -56,6 +58,8 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
Timeout: timeout,
UseTCP: useTCP,
UseUDP: useUDP,
UseProxyProtocol: useProxyProtocol,
EnableLogging: enableLogging,
})
js, _ := json.Marshal(newConfigUUID)
@ -75,6 +79,8 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
proxyAddr, _ := utils.PostPara(r, "proxyAddr")
useTCP, _ := utils.PostBool(r, "useTCP")
useUDP, _ := utils.PostBool(r, "useUDP")
useProxyProtocol, _ := utils.PostBool(r, "useProxyProtocol")
enableLogging, _ := utils.PostBool(r, "enableLogging")
newTimeoutStr, _ := utils.PostPara(r, "timeout")
newTimeout := -1
@ -86,8 +92,21 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
}
}
// Create a new ProxyRuleUpdateConfig with the extracted parameters
newConfig := &ProxyRuleUpdateConfig{
InstanceUUID: configUUID,
NewName: newName,
NewListeningAddr: listenAddr,
NewProxyAddr: proxyAddr,
UseTCP: useTCP,
UseUDP: useUDP,
UseProxyProtocol: useProxyProtocol,
EnableLogging: enableLogging,
NewTimeout: newTimeout,
}
// Call the EditConfig method to modify the configuration
err = m.EditConfig(configUUID, newName, listenAddr, proxyAddr, useTCP, useUDP, newTimeout)
err = m.EditConfig(newConfig)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return

View File

@ -0,0 +1,110 @@
package streamproxy
/*
Instances.go
This file contains the methods to start, stop, and manage the proxy relay instances.
*/
import (
"errors"
"log"
"time"
)
func (c *ProxyRelayInstance) LogMsg(message string, originalError error) {
if !c.EnableLogging {
return
}
if originalError != nil {
log.Println(message, "error:", originalError)
} else {
log.Println(message)
}
}
// Start a proxy if stopped
func (c *ProxyRelayInstance) 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.udpStopChan = nil
c.parent.SaveConfigToDatabase()
}
c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
}
}()
}
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.tcpStopChan = nil
c.parent.SaveConfigToDatabase()
c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
}
}()
}
//Successfully spawned off the proxy routine
c.Running = true
c.parent.SaveConfigToDatabase()
return nil
}
// Return if a proxy config is running
func (c *ProxyRelayInstance) IsRunning() bool {
return c.tcpStopChan != nil || c.udpStopChan != nil
}
// Restart a proxy config
func (c *ProxyRelayInstance) Restart() {
if c.IsRunning() {
c.Stop()
}
time.Sleep(3000 * time.Millisecond)
c.Start()
}
// Stop a running proxy if running
func (c *ProxyRelayInstance) Stop() {
c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
if c.udpStopChan != nil {
c.parent.logf("Stopping UDP for "+c.Name, nil)
c.udpStopChan <- true
c.udpStopChan = nil
}
if c.tcpStopChan != nil {
c.parent.logf("Stopping TCP for "+c.Name, nil)
c.tcpStopChan <- true
c.tcpStopChan = nil
}
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
c.Running = false
//Update the running status
c.parent.SaveConfigToDatabase()
}

View File

@ -8,7 +8,6 @@ import (
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/info/logger"
@ -30,9 +29,25 @@ type ProxyRelayOptions struct {
Timeout int
UseTCP bool
UseUDP bool
UseProxyProtocol bool
EnableLogging bool
}
type ProxyRelayConfig struct {
// ProxyRuleUpdateConfig is used to update the proxy rule config
type ProxyRuleUpdateConfig struct {
InstanceUUID string //The target instance UUID to update
NewName string //New name for the instance, leave empty for no change
NewListeningAddr string //New listening address, leave empty for no change
NewProxyAddr string //New proxy target address, leave empty for no change
UseTCP bool //Enable TCP proxy, default to false
UseUDP bool //Enable UDP proxy, default to false
UseProxyProtocol bool //Enable Proxy Protocol, default to false
EnableLogging bool //Enable Logging TCP/UDP Message, default to true
NewTimeout int //New timeout for the connection, leave -1 for no change
}
type ProxyRelayInstance struct {
/* Runtime Config */
UUID string //A UUIDv4 representing this config
Name string //Name of the config
Running bool //Status, read only
@ -41,7 +56,11 @@ type ProxyRelayConfig struct {
ProxyTargetAddr string //Proxy target address
UseTCP bool //Enable TCP proxy
UseUDP bool //Enable UDP proxy
UseProxyProtocol bool //Enable Proxy Protocol
EnableLogging bool //Enable logging for ProxyInstance
Timeout int //Timeout for connection in sec
/* Internal */
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
@ -60,13 +79,14 @@ type Options struct {
type Manager struct {
//Config and stores
Options *Options
Configs []*ProxyRelayConfig
Configs []*ProxyRelayInstance
//Realtime Statistics
Connections int //currently connected connect counts
}
// NewStreamProxy creates a new stream proxy manager with the given options
func NewStreamProxy(options *Options) (*Manager, error) {
if !utils.FileExists(options.ConfigStore) {
err := os.MkdirAll(options.ConfigStore, 0775)
@ -76,7 +96,7 @@ func NewStreamProxy(options *Options) (*Manager, error) {
}
//Load relay configs from db
previousRules := []*ProxyRelayConfig{}
previousRules := []*ProxyRelayInstance{}
streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
if err != nil {
return nil, err
@ -89,7 +109,7 @@ func NewStreamProxy(options *Options) (*Manager, error) {
options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err)
continue
}
thisRelayConfig := &ProxyRelayConfig{}
thisRelayConfig := &ProxyRelayInstance{}
err = json.Unmarshal(configBytes, thisRelayConfig)
if err != nil {
options.Logger.PrintAndLog("stream-prox", "Unmarshal stream proxy config failed", err)
@ -142,6 +162,7 @@ func (m *Manager) logf(message string, originalError error) {
m.Options.Logger.PrintAndLog("stream-prox", message, originalError)
}
// NewConfig creates a new proxy relay config with the given options
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
//Generate two zero value for atomic int64
aAcc := atomic.Int64{}
@ -150,13 +171,15 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
bAcc.Store(0)
//Generate a new config from options
configUUID := uuid.New().String()
thisConfig := ProxyRelayConfig{
thisConfig := ProxyRelayInstance{
UUID: configUUID,
Name: config.Name,
ListeningAddress: config.ListeningAddr,
ProxyTargetAddr: config.ProxyAddr,
UseTCP: config.UseTCP,
UseUDP: config.UseUDP,
UseProxyProtocol: config.UseProxyProtocol,
EnableLogging: config.EnableLogging,
Timeout: config.Timeout,
tcpStopChan: nil,
udpStopChan: nil,
@ -170,7 +193,7 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
return configUUID
}
func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error) {
func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayInstance, error) {
// Find and return the config with the specified UUID
for _, config := range m.Configs {
if config.UUID == configUUID {
@ -181,32 +204,34 @@ func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error)
}
// 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 {
func (m *Manager) EditConfig(newConfig *ProxyRuleUpdateConfig) error {
// Find the config with the specified UUID
foundConfig, err := m.GetConfigByUUID(configUUID)
foundConfig, err := m.GetConfigByUUID(newConfig.InstanceUUID)
if err != nil {
return err
}
// Validate and update the fields
if newName != "" {
foundConfig.Name = newName
if newConfig.NewName != "" {
foundConfig.Name = newConfig.NewName
}
if newListeningAddr != "" {
foundConfig.ListeningAddress = newListeningAddr
if newConfig.NewListeningAddr != "" {
foundConfig.ListeningAddress = newConfig.NewListeningAddr
}
if newProxyAddr != "" {
foundConfig.ProxyTargetAddr = newProxyAddr
if newConfig.NewProxyAddr != "" {
foundConfig.ProxyTargetAddr = newConfig.NewProxyAddr
}
foundConfig.UseTCP = useTCP
foundConfig.UseUDP = useUDP
foundConfig.UseTCP = newConfig.UseTCP
foundConfig.UseUDP = newConfig.UseUDP
foundConfig.UseProxyProtocol = newConfig.UseProxyProtocol
foundConfig.EnableLogging = newConfig.EnableLogging
if newTimeout != -1 {
if newTimeout < 0 {
if newConfig.NewTimeout != -1 {
if newConfig.NewTimeout < 0 {
return errors.New("invalid timeout value given")
}
foundConfig.Timeout = newTimeout
foundConfig.Timeout = newConfig.NewTimeout
}
m.SaveConfigToDatabase()
@ -215,12 +240,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
if foundConfig.IsRunning() {
foundConfig.Restart()
}
return nil
}
// Remove the config from file by UUID
func (m *Manager) RemoveConfig(configUUID string) error {
//Remove the config from file
err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config"))
if err != nil {
return err
@ -250,91 +274,3 @@ func (m *Manager) SaveConfigToDatabase() {
}
}
}
/*
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.udpStopChan = nil
c.parent.SaveConfigToDatabase()
}
c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
}
}()
}
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.tcpStopChan = nil
c.parent.SaveConfigToDatabase()
c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
}
}()
}
//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(3000 * time.Millisecond)
c.Start()
}
// Stop a running proxy if running
func (c *ProxyRelayConfig) Stop() {
c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
if c.udpStopChan != nil {
c.parent.logf("Stopping UDP for "+c.Name, nil)
c.udpStopChan <- true
c.udpStopChan = nil
}
if c.tcpStopChan != nil {
c.parent.logf("Stopping TCP for "+c.Name, nil)
c.tcpStopChan <- true
c.tcpStopChan = nil
}
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
c.Running = false
//Update the running status
c.parent.SaveConfigToDatabase()
}

View File

@ -12,7 +12,7 @@ func TestPort2Port(t *testing.T) {
stopChan := make(chan bool)
// Create a ProxyRelayConfig with dummy values
config := &streamproxy.ProxyRelayConfig{
config := &streamproxy.ProxyRelayInstance{
Timeout: 1,
}

View File

@ -2,6 +2,7 @@ package streamproxy
import (
"errors"
"fmt"
"io"
"log"
"net"
@ -30,31 +31,50 @@ func isValidPort(port string) bool {
return true
}
func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *atomic.Int64) {
func (c *ProxyRelayInstance) 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()+"]")
c.LogMsg("[←] close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]", nil)
//conn2.Close()
//log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]")
//c.LogMsg("[←] close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]", nil)
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())
func writeProxyProtocolHeaderV1(dst net.Conn, src net.Conn) error {
clientAddr, ok1 := src.RemoteAddr().(*net.TCPAddr)
proxyAddr, ok2 := src.LocalAddr().(*net.TCPAddr)
if !ok1 || !ok2 {
return errors.New("invalid TCP address for proxy protocol")
}
header := fmt.Sprintf("PROXY TCP4 %s %s %d %d\r\n",
clientAddr.IP.String(),
proxyAddr.IP.String(),
clientAddr.Port,
proxyAddr.Port)
_, err := dst.Write([]byte(header))
return err
}
func (c *ProxyRelayInstance) forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) {
msg := fmt.Sprintf("[+] start transmit. [%s],[%s] <-> [%s],[%s]",
conn1.LocalAddr().String(), conn1.RemoteAddr().String(),
conn2.LocalAddr().String(), conn2.RemoteAddr().String())
c.LogMsg(msg, nil)
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
go c.connCopy(conn1, conn2, &wg, aTob)
go c.connCopy(conn2, conn1, &wg, bToa)
wg.Wait()
}
func (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
func (c *ProxyRelayInstance) accept(listener net.Listener) (net.Conn, error) {
conn, err := listener.Accept()
if err != nil {
return nil, err
@ -65,13 +85,13 @@ func (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
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")
c.LogMsg("[x] Connection from "+addr.IP.String()+" rejected by access control policy", nil)
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
c.LogMsg("[√] accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]", nil)
return conn, nil
}
func startListener(address string) (net.Listener, error) {
@ -92,7 +112,7 @@ func startListener(address string) (net.Listener, error) {
portA -> server
server -> portB
*/
func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, stopChan chan bool) error {
func (c *ProxyRelayInstance) Port2host(allowPort string, targetAddress string, stopChan chan bool) error {
listenerStartingAddr := allowPort
if isValidPort(allowPort) {
//number only, e.g. 8080
@ -112,7 +132,7 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
//Start stop handler
go func() {
<-stopChan
log.Println("[x]", "Received stop signal. Exiting Port to Host forwarder")
c.LogMsg("[x] Received stop signal. Exiting Port to Host forwarder", nil)
server.Close()
}()
@ -129,18 +149,32 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
}
go func(targetAddress string) {
log.Println("[+]", "start connect host:["+targetAddress+"]")
c.LogMsg("[+] start connect host:["+targetAddress+"]", nil)
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. ")
c.LogMsg("[x] connect target address ["+targetAddress+"] failed. retry in "+strconv.Itoa(c.Timeout)+" seconds.", nil)
conn.Close()
log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]")
c.LogMsg("[←] close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]", nil)
time.Sleep(time.Duration(c.Timeout) * time.Second)
return
}
log.Println("[→]", "connect target address ["+targetAddress+"] success.")
forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
c.LogMsg("[→] connect target address ["+targetAddress+"] success.", nil)
if c.UseProxyProtocol {
c.LogMsg("[+] write proxy protocol header to target address ["+targetAddress+"]", nil)
err = writeProxyProtocolHeaderV1(target, conn)
if err != nil {
c.LogMsg("[x] Write proxy protocol header failed: "+err.Error(), nil)
target.Close()
conn.Close()
c.LogMsg("[←] close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]", nil)
time.Sleep(time.Duration(c.Timeout) * time.Second)
return
}
}
c.forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
}(targetAddress)
}
}

View File

@ -53,7 +53,7 @@ func initUDPConnections(listenAddr string, targetAddress string) (*net.UDPConn,
}
// Go routine which manages connection from server to single client
func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lisenter *net.UDPConn) {
func (c *ProxyRelayInstance) RunUDPConnectionRelay(conn *udpClientServerConn, lisenter *net.UDPConn) {
var buffer [1500]byte
for {
// Read from server
@ -74,7 +74,7 @@ func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lise
}
// Close all connections that waiting for read from server
func (c *ProxyRelayConfig) CloseAllUDPConnections() {
func (c *ProxyRelayInstance) CloseAllUDPConnections() {
c.udpClientMap.Range(func(clientAddr, clientServerConn interface{}) bool {
conn := clientServerConn.(*udpClientServerConn)
conn.ServerConn.Close()
@ -82,7 +82,7 @@ func (c *ProxyRelayConfig) CloseAllUDPConnections() {
})
}
func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan bool) error {
func (c *ProxyRelayInstance) 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) {
@ -90,8 +90,8 @@ func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan b
address1 = ":" + address1
}
if strings.HasPrefix(address1, ":") {
//Prepend 127.0.0.1 to the address
address1 = "127.0.0.1" + address1
//Prepend 0.0.0.0 to the address
address1 = "0.0.0.0" + address1
}
lisener, targetAddr, err := initUDPConnections(address1, address2)
@ -138,12 +138,12 @@ func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan b
continue
}
c.udpClientMap.Store(saddr, conn)
log.Println("[UDP] Created new connection for client " + saddr)
c.LogMsg("[UDP] Created new connection for client "+saddr, nil)
// Fire up routine to manage new connection
go c.RunUDPConnectionRelay(conn, lisener)
} else {
log.Println("[UDP] Found connection for client " + saddr)
c.LogMsg("[UDP] Found connection for client "+saddr, nil)
conn = rawConn.(*udpClientServerConn)
}

View File

@ -20,10 +20,20 @@ type CertCache struct {
PriKey string
}
type HostSpecificTlsBehavior struct {
DisableSNI bool //If SNI is enabled for this server name
DisableLegacyCertificateMatching bool //If legacy certificate matching is disabled for this server name
EnableAutoHTTPS bool //If auto HTTPS is enabled for this server name
PreferredCertificate string //Preferred certificate for this server name, if empty, use the first matching certificate
}
type Manager struct {
CertStore string //Path where all the certs are stored
LoadedCerts []*CertCache //A list of loaded certs
Logger *logger.Logger //System wide logger for debug mesage
/* External handlers */
hostSpecificTlsBehavior func(serverName string) (*HostSpecificTlsBehavior, error) // Function to get host specific TLS behavior, if nil, use global TLS options
verbal bool
}
@ -52,6 +62,7 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
thisManager := Manager{
CertStore: certStore,
LoadedCerts: []*CertCache{},
hostSpecificTlsBehavior: defaultHostSpecificTlsBehavior, //Default to no SNI and no auto HTTPS
verbal: verbal,
Logger: logger,
}
@ -64,6 +75,21 @@ func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager,
return &thisManager, nil
}
// Default host specific TLS behavior
// This is used when no specific TLS behavior is defined for a server name
func GetDefaultHostSpecificTlsBehavior() *HostSpecificTlsBehavior {
return &HostSpecificTlsBehavior{
DisableSNI: false,
DisableLegacyCertificateMatching: false,
EnableAutoHTTPS: false,
PreferredCertificate: "",
}
}
func defaultHostSpecificTlsBehavior(serverName string) (*HostSpecificTlsBehavior, error) {
return GetDefaultHostSpecificTlsBehavior(), nil
}
// Update domain mapping from file
func (m *Manager) UpdateLoadedCertList() error {
//Get a list of certificates from file
@ -161,24 +187,11 @@ func (m *Manager) ListCerts() ([]string, error) {
// Get a certificate from disk where its certificate matches with the helloinfo
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
//Check if the domain corrisponding cert exists
pubKey := "./tmp/localhost.pem"
priKey := "./tmp/localhost.key"
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
//Direct hit
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
} else if m.CertMatchExists(helloInfo.ServerName) {
//Use x509
pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
} else {
//Fallback to legacy method of matching certificates
if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key")
}
//Look for the certificate by hostname
pubKey, priKey, err := m.GetCertificateByHostname(helloInfo.ServerName)
if err != nil {
m.Logger.PrintAndLog("tls-router", "Failed to get certificate for "+helloInfo.ServerName, err)
return nil, err
}
//Load the cert and serve it
@ -190,6 +203,51 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
return &cer, nil
}
// GetCertificateByHostname returns the certificate and private key for a given hostname
func (m *Manager) GetCertificateByHostname(hostname string) (string, string, error) {
//Check if the domain corrisponding cert exists
pubKey := "./tmp/localhost.pem"
priKey := "./tmp/localhost.key"
tlsBehavior, err := m.hostSpecificTlsBehavior(hostname)
if err != nil {
tlsBehavior, _ = defaultHostSpecificTlsBehavior(hostname)
}
if tlsBehavior.DisableSNI && tlsBehavior.PreferredCertificate != "" &&
utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem")) &&
utils.FileExists(filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key")) {
//User setup a Preferred certificate, use the preferred certificate directly
pubKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".pem")
priKey = filepath.Join(m.CertStore, tlsBehavior.PreferredCertificate+".key")
} else {
if !tlsBehavior.DisableLegacyCertificateMatching &&
utils.FileExists(filepath.Join(m.CertStore, hostname+".pem")) &&
utils.FileExists(filepath.Join(m.CertStore, hostname+".key")) {
//Legacy filename matching, use the file names directly
//This is the legacy method of matching certificates, it will match the file names directly
//This is used for compatibility with Zoraxy v2 setups
pubKey = filepath.Join(m.CertStore, hostname+".pem")
priKey = filepath.Join(m.CertStore, hostname+".key")
} else if !tlsBehavior.DisableSNI &&
m.CertMatchExists(hostname) {
//SNI scan match, find the first matching certificate
pubKey, priKey = m.GetCertByX509CNHostname(hostname)
} else if tlsBehavior.EnableAutoHTTPS {
//Get certificate from CA, WIP
//TODO: Implement AutoHTTPS
} else {
//Fallback to legacy method of matching certificates
if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key")
}
}
}
return pubKey, priKey, nil
}
// Check if both the default cert public key and private key exists
func (m *Manager) DefaultCertExists() bool {
return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
@ -220,7 +278,6 @@ func (m *Manager) RemoveCert(domain string) error {
//Update the cert list
m.UpdateLoadedCertList()
return nil
}

View File

@ -22,6 +22,7 @@ type StaticWebServerStatus struct {
WebRoot string
Running bool
EnableWebDirManager bool
DisableListenToAllInterface bool
}
// Handle getting current static web server status
@ -33,6 +34,7 @@ func (ws *WebServer) HandleGetStatus(w http.ResponseWriter, r *http.Request) {
WebRoot: ws.option.WebRoot,
Running: ws.isRunning,
EnableWebDirManager: ws.option.EnableWebDirManager,
DisableListenToAllInterface: ws.option.DisableListenToAllInterface,
}
js, _ := json.Marshal(currentStatus)
@ -67,6 +69,12 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
return
}
// Check if newPort is a valid TCP port number (1-65535)
if newPort < 1 || newPort > 65535 {
utils.SendErrorResponse(w, "invalid port number given")
return
}
err = ws.ChangePort(strconv.Itoa(newPort))
if err != nil {
utils.SendErrorResponse(w, err.Error())
@ -91,3 +99,30 @@ func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Re
ws.option.EnableDirectoryListing = enableList
utils.SendOK(w)
}
// Get or set disable listen to all interface settings
func (ws *WebServer) SetDisableListenToAllInterface(w http.ResponseWriter, r *http.Request) {
disableListen, err := utils.PostBool(r, "disable")
if err != nil {
utils.SendErrorResponse(w, "invalid setting given")
return
}
err = ws.option.Sysdb.Write("webserv", "disableListenToAllInterface", disableListen)
if err != nil {
utils.SendErrorResponse(w, "unable to save setting")
return
}
// Update the option in the web server instance
ws.option.DisableListenToAllInterface = disableListen
// If the server is running and the setting is changed, we need to restart the server
if ws.IsRunning() {
err = ws.Restart()
if err != nil {
utils.SendErrorResponse(w, "unable to restart web server: "+err.Error())
return
}
}
utils.SendOK(w)
}

View File

@ -25,11 +25,19 @@ import (
//go:embed templates/*
var templates embed.FS
/*
WebServerOptions define the default option for the webserv
might get override by user settings loaded from db
Any changes in here might need to also update the StaticWebServerStatus struct
in handler.go. See handler.go for more information.
*/
type WebServerOptions struct {
Port string //Port for listening
EnableDirectoryListing bool //Enable listing of directory
WebRoot string //Folder for stroing the static web folders
EnableWebDirManager bool //Enable web file manager to handle files in web directory
DisableListenToAllInterface bool // Disable listening to all interfaces, only listen to localhost
Logger *logger.Logger //System logger
Sysdb *database.Database //Database for storing configs
}
@ -92,6 +100,11 @@ func (ws *WebServer) RestorePreviousState() {
ws.option.Sysdb.Read("webserv", "dirlist", &enableDirList)
ws.option.EnableDirectoryListing = enableDirList
//Set disable listen to all interface
disableListenToAll := ws.option.DisableListenToAllInterface
ws.option.Sysdb.Read("webserv", "disableListenToAllInterface", &disableListenToAll)
ws.option.DisableListenToAllInterface = disableListenToAll
//Check the running state
webservRunning := true
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
@ -156,8 +169,12 @@ func (ws *WebServer) Start() error {
fs := http.FileServer(http.Dir(filepath.Join(ws.option.WebRoot, "html")))
ws.mux.Handle("/", ws.fsMiddleware(fs))
listenAddr := ":" + ws.option.Port
if ws.option.DisableListenToAllInterface {
listenAddr = "127.0.0.1:" + ws.option.Port
}
ws.server = &http.Server{
Addr: ":" + ws.option.Port,
Addr: listenAddr,
Handler: ws.mux,
}
@ -193,6 +210,27 @@ func (ws *WebServer) Stop() error {
return nil
}
func (ws *WebServer) Restart() error {
if ws.isRunning {
if err := ws.Stop(); err != nil {
return err
}
}
if err := ws.Start(); err != nil {
return err
}
ws.option.Logger.PrintAndLog("static-webserv", "Static Web Server restarted. Listening on :"+ws.option.Port, nil)
return nil
}
func (ws *WebServer) IsRunning() bool {
ws.mu.Lock()
defer ws.mu.Unlock()
return ws.isRunning
}
// UpdateDirectoryListing enables or disables directory listing.
func (ws *WebServer) UpdateDirectoryListing(enable bool) {
ws.option.EnableDirectoryListing = enable

View File

@ -15,6 +15,7 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
)
@ -116,6 +117,7 @@ func ReverseProxtInit() {
WebDirectory: *path_webserver,
AccessController: accessController,
ForwardAuthRouter: forwardAuthRouter,
OAuth2Router: oauth2Router,
LoadBalancer: loadBalancer,
PluginManager: pluginManager,
/* Utilities */
@ -333,7 +335,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
tags = filteredTags
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
if eptype == "host" {
switch eptype {
case "host":
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
if err != nil {
utils.SendErrorResponse(w, "hostname not defined")
@ -414,7 +417,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
proxyEndpointCreated = &thisProxyEndpoint
} else if eptype == "root" {
case "root":
//Get the default site options and target
dsOptString, err := utils.PostPara(r, "defaultSiteOpt")
if err != nil {
@ -468,7 +471,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
proxyEndpointCreated = &rootRoutingEndpoint
} else {
default:
//Invalid eptype
utils.SendErrorResponse(w, "invalid endpoint type")
return
@ -555,6 +558,9 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
proxyRateLimit = 1000
}
// Disable chunked Encoding
disableChunkedEncoding, _ := utils.PostBool(r, "dChunkedEnc")
//Load the previous basic auth credentials from current proxy rules
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
if err != nil {
@ -595,6 +601,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
newProxyEndpoint.RateLimit = proxyRateLimit
newProxyEndpoint.UseStickySession = useStickySession
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
newProxyEndpoint.DisableChunkedTransferEncoding = disableChunkedEncoding
newProxyEndpoint.Tags = tags
//Prepare to replace the current routing rule
@ -672,6 +679,142 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w)
}
func ReverseProxyHandleSetTlsConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
utils.SendErrorResponse(w, "Method not supported")
return
}
rootnameOrMatchingDomain, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
}
tlsConfig, err := utils.PostPara(r, "tlsConfig")
if err != nil {
utils.SendErrorResponse(w, "Invalid TLS config given")
return
}
tlsConfig = strings.TrimSpace(tlsConfig)
if tlsConfig == "" {
utils.SendErrorResponse(w, "TLS config cannot be empty")
return
}
newTlsConfig := &tlscert.HostSpecificTlsBehavior{}
err = json.Unmarshal([]byte(tlsConfig), newTlsConfig)
if err != nil {
utils.SendErrorResponse(w, "Invalid TLS config given: "+err.Error())
return
}
//Load the target endpoint
ept, err := dynamicProxyRouter.LoadProxy(rootnameOrMatchingDomain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
ept.TlsOptions = newTlsConfig
//Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(ept)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
//Save it to file
err = SaveReverseProxyConfig(ept)
if err != nil {
utils.SendErrorResponse(w, "Failed to save TLS config: "+err.Error())
return
}
utils.SendOK(w)
}
func ReverseProxyHandleSetHostname(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
utils.SendErrorResponse(w, "Method not supported")
return
}
originalRootnameOrMatchingDomain, err := utils.PostPara(r, "oldHostname")
if err != nil {
utils.SendErrorResponse(w, "Invalid original hostname given")
return
}
newHostname, err := utils.PostPara(r, "newHostname")
if err != nil {
utils.SendErrorResponse(w, "Invalid new hostname given")
return
}
originalRootnameOrMatchingDomain = strings.TrimSpace(originalRootnameOrMatchingDomain)
newHostname = strings.TrimSpace(newHostname)
if newHostname == "/" {
//Reserevd, reutrn error
utils.SendErrorResponse(w, "Invalid new hostname: system reserved path")
return
}
//Check if the endpoint already exists
_, err = dynamicProxyRouter.LoadProxy(newHostname)
if err == nil {
//Endpoint already exists, return error
utils.SendErrorResponse(w, "Endpoint with this hostname already exists")
return
}
//Clone, edit the endpoint and remove the original one
ept, err := dynamicProxyRouter.LoadProxy(originalRootnameOrMatchingDomain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
newEndpoint := ept.Clone()
newEndpoint.RootOrMatchingDomain = newHostname
//Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Remove the old endpoint from runtime
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(originalRootnameOrMatchingDomain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Remove the config from file
err = RemoveReverseProxyConfig(originalRootnameOrMatchingDomain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Add the new endpoint to runtime
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
//Save it to file
SaveReverseProxyConfig(newEndpoint)
//Update uptime monitor targets
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
ep, err := utils.PostPara(r, "ep")
if err != nil {
@ -933,6 +1076,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
js, err := json.Marshal(dynamicProxyRouter)
if err != nil {
SystemWideLogger.PrintAndLog("proxy-config", "Unable to marshal status data", err)
utils.SendErrorResponse(w, "Unable to marshal status data")
return
}

View File

@ -1,6 +1,7 @@
package main
import (
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
"log"
"net/http"
"os"
@ -147,6 +148,11 @@ func startupSequence() {
Database: sysdb,
})
oauth2Router = oauth2.NewOAuth2Router(&oauth2.OAuth2RouterOptions{
Logger: SystemWideLogger,
Database: sysdb,
})
//Create a statistic collector
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
Database: sysdb,

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Default Site</h2>
<p>Default routing options for inbound traffic (previously called Proxy Root)</p>
<p>Default routing options for inbound traffic</p>
<div class="ui form">
<div class="grouped fields">
<label>What to show when Zoraxy is hit with an unknown Host?</label>
@ -209,14 +209,13 @@
})
}
//Set the new proxy root option
//Set the new proxy root (aka default site) option
function setProxyRoot(btn=undefined){
var newpr = $("#proxyRoot").val();
if (newpr.trim() == "" && currentDefaultSiteOption == 0){
//Fill in the web server info
newpr = "127.0.0.1:" + $("#webserv_listenPort").val();
$("#proxyRoot").val(newpr);
}
var rootReqTls = $("#rootReqTLS")[0].checked;

View File

@ -1,20 +1,17 @@
<div class="standardContainer">
<div class="sso standardContainer">
<div class="ui basic segment">
<h2>SSO</h2>
<p>Single Sign-On (SSO) and authentication providers settings </p>
</div>
<div class="ui basic segment">
<div class="ui yellow message">
<div class="header">
Experimental Feature
</div>
<p>Please note that this feature is still in development and may not work as expected.</p>
</div>
</div>
<div class="ui divider"></div>
<div class="ui basic segment">
<h3>Forward Auth</h3>
<div class="ui top attached tabular menu ssoTabs">
<a class="item active" data-tab="forward_auth_tab">Forward Auth</a>
<a class="item" data-tab="oauth2_tab">Oauth2</a>
<!-- <a class="item" data-tab="zoraxy_sso_tab">Zoraxy SSO</a> -->
</div>
<div class="ui bottom attached tab segment active" data-tab="forward_auth_tab">
<!-- Forward Auth -->
<h2>Forward Auth</h2>
<p>Configuration settings for the Forward Auth provider.</p>
<p>The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:</p>
<ul>
@ -25,8 +22,9 @@
<ul>
<li><a href="https://www.authelia.com" rel=”noopener noreferrer target="_blank">Authelia</a></li>
<li><a href="https://goauthentik.io/" rel=”noopener noreferrer target="_blank">Authentik</a></li>
<li><a href="https://oauth2-proxy.github.io/oauth2-proxy/" rel=”noopener noreferrer target="_blank">OAuth2 Proxy</a></li>
</ul>
<form class="ui form">
<form class="ui form" action="#" id="forwardAuthSettings">
<div class="field">
<label for="forwardAuthAddress">Address</label>
<input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address">
@ -42,38 +40,111 @@
<div class="field">
<label for="forwardAuthResponseHeaders">Response Headers</label>
<input type="text" id="forwardAuthResponseHeaders" name="forwardAuthResponseHeaders" placeholder="Enter Forward Auth Response Headers">
<small>Comma separated list of case-insensitive headers to copy from the authorization servers response to the request sent to the backend. If not set no headers are copied. <br>
<strong>Example:</strong> <code>Remote-User,Remote-Groups,Remote-Email,Remote-Name</code></small>
<small>
Comma separated list of case-insensitive headers to copy from the authorization servers response to the request sent to the backend. If not set no headers are copied. <br>
<strong>Example:</strong> <code>Remote-User,Remote-Groups,Remote-Email,Remote-Name</code>
</small>
</div>
<div class="field">
<label for="forwardAuthResponseClientHeaders">Response Client Headers</label>
<input type="text" id="forwardAuthResponseClientHeaders" name="forwardAuthResponseClientHeaders" placeholder="Enter Forward Auth Response Client Headers">
<small>Comma separated list of case-insensitive headers to copy from the authorization servers response to the response sent to the client. If not set no headers are copied. <br>
<strong>Example:</strong> <code>Set-Cookie,WWW-Authenticate</code></small>
<small>
Comma separated list of case-insensitive headers to copy from the authorization servers response to the <b><i>response sent to the client</i></b>. If not set no headers are copied. <br>
<strong>Example:</strong> <code>Set-Cookie,WWW-Authenticate</code>
</small>
</div>
<div class="field">
<label for="forwardAuthRequestHeaders">Request Headers</label>
<input type="text" id="forwardAuthRequestHeaders" name="forwardAuthRequestHeaders" placeholder="Enter Forward Auth Request Headers">
<small>Comma separated list of case-insensitive headers to copy from the original request to the request made to the authorization server. If not set all headers are copied. <br>
<strong>Example:</strong> <code>Cookie,Authorization</code></small>
<small>
Comma separated list of case-insensitive headers to copy from the original request to the <b><i>request made to the authorization server</i></b>. If not set all headers are copied. <br>
<strong>Recommendation:</strong> Generally it's recommended to leave this blank or use the below example for predictable results. <br>
<strong>Example:</strong> <code>Accept,X-Requested-With,Cookie,Authorization,Proxy-Authorization</code>
</small>
</div>
<div class="field">
<label for="forwardAuthRequestIncludedCookies">Request Included Cookies</label>
<input type="text" id="forwardAuthRequestIncludedCookies" name="forwardAuthRequestIncludedCookies" placeholder="Enter Forward Auth Request Included Cookies">
<small>
Comma separated list of case-sensitive cookie names to copy from the original request to the <b><i>request made to the authorization server</i></b>. If not set all cookies are included. This allows omitting all cookies not required by the authorization server.<br>
<strong>Example:</strong> <code>authelia_session,another_session</code>
</small>
</div>
<div class="field">
<label for="forwardAuthRequestExcludedCookies">Request Excluded Cookies</label>
<input type="text" id="forwardAuthRequestExcludedCookies" name="forwardAuthRequestExcludedCookies" placeholder="Enter Forward Auth Request Excluded Cookies">
<small>Comma separated list of case-sensitive cookie names to exclude from the request to the backend. If not set no cookies are excluded. <br>
<strong>Example:</strong> <code>authelia_session,another_session</code></small>
<small>
Comma separated list of case-sensitive cookie names to exclude from the <b><i>request made to the backend application</i></b>. If not set no cookies are excluded. This allows omitting the cookie intended only for the authorization server.<br>
<strong>Example:</strong> <code>authelia_session,another_session</code>
</small>
</div>
</div>
</div>
</div>
<button class="ui basic button" onclick="event.preventDefault(); updateForwardAuthSettings();"><i class="green check icon"></i> Apply Change</button>
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
</form>
</div>
<div class="ui divider"></div>
<div class="ui bottom attached tab segment" data-tab="oauth2_tab">
<!-- Oauth 2 -->
<h2>OAuth 2.0</h2>
<p>Configuration settings for OAuth 2.0 authentication provider.</p>
<form class="ui form" action="#" id="oauth2Settings">
<div class="field">
<label for="oauth2ClientId">Client ID</label>
<input type="text" id="oauth2ClientId" name="oauth2ClientId" placeholder="Enter Client ID">
<small>Public identifier of the OAuth2 application</small>
</div>
<div class="field">
<label for="oauth2ClientId">Client Secret</label>
<input type="password" id="oauth2ClientSecret" name="oauth2ClientSecret" placeholder="Enter Client Secret">
<small>Secret key of the OAuth2 application</small>
</div>
<div class="field">
<label for="oauth2WellKnownUrl">OIDC well-known URL</label>
<input type="text" id="oauth2WellKnownUrl" name="oauth2WellKnownUrl" placeholder="Enter Well-Known URL">
<small>URL to the OIDC discovery document (usually ending with /.well-known/openid-configuration). Used to automatically fetch provider settings.</small>
</div>
<div class="field">
<label for="oauth2ServerUrl">Authorization URL</label>
<input type="text" id="oauth2ServerUrl" name="oauth2ServerUrl" placeholder="Enter Authorization URL">
<small>URL used to authenticate against the OAuth2 provider. Will redirect the user to the OAuth2 provider login view. Optional if Well-Known url is configured.</small>
</div>
<div class="field">
<label for="oauth2TokenUrl">Token URL</label>
<input type="text" id="oauth2TokenUrl" name="oauth2TokenUrl" placeholder="Enter Token URL">
<small>URL used by Zoraxy to exchange a valid OAuth2 authentication code for an access token. Optional if Well-Known url is configured.</small>
</div>
<div class="field">
<label for="oauth2UserInfoURL">User Info URL</label>
<input type="text" id="oauth2UserInfoURL" name="oauth2UserInfoURL" placeholder="Enter User Info URL">
<small>URL used by the OAuth2 provider to validate generated token. Optional if Well-Known url is configured.</small>
</div>
<div class="field">
<label for="oauth2Scopes">Scopes</label>
<input type="text" id="oauth2Scopes" name="oauth2Scopes" placeholder="Enter Scopes">
<small>Scopes required by the OAuth2 provider to retrieve information about the authenticated user. Refer to your OAuth2 provider documentation for more information about this. Optional if Well-Known url is configured.</small>
</div>
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
</form>
</div>
<div class="ui bottom attached tab segment" data-tab="zoraxy_sso_tab">
<!-- Zoraxy SSO -->
<h3>Zoraxy SSO</h3>
<p>Configuration settings for Zoraxy SSO provider.</p>
<p>Currently not implemented.</p>
</div>
</div>
<script>
$(".ssoTabs .item").tab();
$(document).ready(function() {
/* Load forward-auth settings from backend */
$.cjax({
url: '/api/sso/forward-auth',
method: 'GET',
@ -83,19 +154,46 @@
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
$('#forwardAuthRequestIncludedCookies').val(data.requestIncludedCookies.join(","));
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);
}
});
/* Load Oauth2 settings from backend */
$.cjax({
url: '/api/sso/OAuth2',
method: 'GET',
dataType: 'json',
success: function(data) {
$('#oauth2WellKnownUrl').val(data.oauth2WellKnownUrl);
$('#oauth2ServerUrl').val(data.oauth2ServerUrl);
$('#oauth2TokenUrl').val(data.oauth2TokenUrl);
$('#oauth2UserInfoUrl').val(data.oauth2UserInfoUrl);
$('#oauth2ClientId').val(data.oauth2ClientId);
$('#oauth2ClientSecret').val(data.oauth2ClientSecret);
$('#oauth2Scopes').val(data.oauth2Scopes);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);
}
});
/* Add more initialization code here if needed */
});
/*
Function to update Forward Auth settings.
*/
function updateForwardAuthSettings() {
const address = $('#forwardAuthAddress').val();
const responseHeaders = $('#forwardAuthResponseHeaders').val();
const responseClientHeaders = $('#forwardAuthResponseClientHeaders').val();
const requestHeaders = $('#forwardAuthRequestHeaders').val();
const requestIncludedCookies = $('#forwardAuthRequestIncludedCookies').val();
const requestExcludedCookies = $('#forwardAuthRequestExcludedCookies').val();
console.log(`Updating Forward Auth settings. Address: ${address}. Response Headers: ${responseHeaders}. Response Client Headers: ${responseClientHeaders}. Request Headers: ${requestHeaders}. Request Excluded Cookies: ${requestExcludedCookies}.`);
@ -123,4 +221,62 @@
}
});
}
$("#forwardAuthSettings").on("submit", function(event) {
event.preventDefault();
updateForwardAuthSettings();
});
/*
Oauth2 settings update handler.
*/
$( "#authentikSettings" ).on( "submit", function( event ) {
event.preventDefault();
$.cjax({
url: '/api/sso/forward-auth',
method: 'POST',
data: {
address: address,
responseHeaders: responseHeaders,
responseClientHeaders: responseClientHeaders,
requestHeaders: requestHeaders,
requestExcludedCookies: requestExcludedCookies
},
success: function(data) {
if (data.error !== undefined) {
msgbox(data.error, false);
return;
}
msgbox('Forward Auth settings updated', true);
console.log('Forward Auth settings updated:', data);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error updating Forward Auth settings:', textStatus, errorThrown);
}
});
});
$( "#oauth2Settings" ).on( "submit", function( event ) {
event.preventDefault();
$.cjax({
url: '/api/sso/OAuth2',
method: 'POST',
data: $( this ).serialize(),
success: function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
return;
}
msgbox('OAuth2 settings updated', true);
console.log('OAuth2 settings updated:', data);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error updating OAuth2 settings:', textStatus, errorThrown);
msgbox('Error updating OAuth2 settings, check console', false);
}
});
});
/* Bind UI events */
$(".sso .advanceSettings").accordion();
</script>

View File

@ -16,6 +16,7 @@
<th>Target Address</th>
<th>Mode</th>
<th>Timeout (s)</th>
<th>Enable Logging</th>
<th>Actions</th>
</tr>
</thead>
@ -73,6 +74,22 @@
<small>Forward UDP request on this listening socket</small></label>
</div>
</div>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" tabindex="0" name="useProxyProtocol" class="hidden">
<label>Enable Proxy Protocol V1<br>
<small>Enable TCP Proxy Protocol header V1</small>
</label>
</div>
</div>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" tabindex="0" name="enableLogging" class="hidden">
<label>Enable Logging<br>
<small>Enable logging of connection status and errors for this rule</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, this);" 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>
@ -195,6 +212,10 @@
modeText.push("UDP")
}
if (config.UseProxyProtocol){
modeText.push("ProxyProtocol V1")
}
modeText = modeText.join(" & ")
var thisConfig = encodeURIComponent(JSON.stringify(config));
@ -207,6 +228,10 @@
row.append($('<td>').text(config.ProxyTargetAddr));
row.append($('<td>').text(modeText));
row.append($('<td>').text(config.Timeout));
row.append($('<td>').html(config.EnableLogging ?
'<i class="green check icon" title="Logging Enabled"></i>' :
'<i class="red times icon" title="Logging Disabled"></i>'
));
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>
@ -252,6 +277,22 @@
$(checkboxEle).checkbox("set unchecked");
}
return;
}else if (key == "UseProxyProtocol"){
let checkboxEle = $("#streamProxyForm input[name=useProxyProtocol]").parent();
if (value === true){
$(checkboxEle).checkbox("set checked");
}else{
$(checkboxEle).checkbox("set unchecked");
}
return;
}else if (key == "EnableLogging"){
let checkboxEle = $("#streamProxyForm input[name=enableLogging]").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"){
@ -301,6 +342,8 @@
proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(),
useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked ,
useUDP: $("#streamProxyForm input[name=useUDP]")[0].checked ,
useProxyProtocol: $("#streamProxyForm input[name=useProxyProtocol]")[0].checked ,
enableLogging: $("#streamProxyForm input[name=enableLogging]")[0].checked ,
timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
},
success: function(response) {

View File

@ -29,6 +29,13 @@
<small>If this folder do not contains any index files, list the directory of this folder.</small>
</div>
</div>
<div class="inline field">
<div class="ui toggle checkbox">
<input id="webserv_enableAllInterfaces" type="checkbox" class="hidden">
<label>Listening to All Interfaces</label>
<small>When disabled, the web server will only listen to localhost (127.0.0.1) and only reachable via reverse proxy rules.</small>
</div>
</div>
<div class="field">
<label>Document Root Folder</label>
<input id="webserv_docRoot" type="text" readonly="true">
@ -136,6 +143,13 @@
$("#webserv_dirManager").remove();
}
if (!data.DisableListenToAllInterface){
//Options on UI is flipped
$("#webserv_enableAllInterfaces").parent().checkbox("set checked");
}else{
$("#webserv_enableAllInterfaces").parent().checkbox("set unchecked");
}
$("#webserv_listenPort").val(data.ListeningPort);
updateWebServLinkExample(data.ListeningPort);
@ -178,6 +192,23 @@
}
})
});
$("#webserv_enableAllInterfaces").off("change").on("change", function(){
let disable = !$(this)[0].checked;
$.cjax({
url: "/api/webserv/disableListenAllInterface",
method: "POST",
data: {"disable": disable},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Listening interface setting updated");
}
}
})
});
$("#webserv_listenPort").off("change").on("change", function(){
let newPort = $(this).val();

View File

@ -184,7 +184,8 @@ body.darkTheme .ui.input input::placeholder {
body.darkTheme .ui.label,
body.darkTheme .ui.label .detail,
body.darkTheme .ui.label .icon {
color: #ffffff !important;
background-color: var(--buttom_toggle_disabled);
color: var(--text_color) !important;
}
body.darkTheme .advanceoptions .title {

View File

@ -72,7 +72,7 @@
<i class="simplistic lock icon"></i> TLS / SSL certificates
</a>
<a class="item" tag="sso">
<i class="simplistic user circle icon"></i> SSO / Oauth
<i class="simplistic user circle icon"></i> SSO / OAuth2
</a>
<div class="ui divider menudivider">Others</div>
<a class="item" tag="webserv">
@ -120,7 +120,7 @@
<!-- Create Rules -->
<div id="rules" class="functiontab" target="rules.html"></div>
<!-- Set proxy root -->
<!-- Set default site -->
<div id="setroot" class="functiontab" target="rproot.html"></div>
<!-- Set TLS cert -->
@ -334,6 +334,7 @@
}
function toggleTheme(){
let editorSideWrapper = $("#httprpEditModal .wrapper_frame");
if ($("body").hasClass("darkTheme")){
setDarkTheme(false);
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
@ -341,6 +342,12 @@
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false);
}
$(editorSideWrapper).each(function(){
if ($(this)[0].contentWindow.setDarkTheme){
$(this)[0].contentWindow.setDarkTheme(false);
}
})
if ($("#pluginContextLoader").is(":visible")){
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(false);
}
@ -350,6 +357,11 @@
if ($(".sideWrapper").is(":visible")){
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
}
$(editorSideWrapper).each(function(){
if ($(this)[0].contentWindow.setDarkTheme){
$(this)[0].contentWindow.setDarkTheme(true);
}
})
if ($("#pluginContextLoader").is(":visible")){
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);
}
@ -515,6 +527,12 @@
}
function hideSideWrapper(discardFrameContent = false){
if ($("#httprpEditModal").length && $("#httprpEditModal").is(":visible")) {
//HTTP Proxy Rule editor side wrapper implementation
$("#httprpEditModal .editor_side_wrapper").hide();
}
//Original side wrapper implementation
if (discardFrameContent){
$(".sideWrapper iframe").attr("src", "snippet/placeholder.html");
}

View File

@ -264,7 +264,6 @@
}
}
document.getElementById('accessRuleSelector').addEventListener('change', handleSelectEditingAccessRule);
document.getElementById('accessRuleForm').addEventListener('submit', handleCreateNewAccessRule);

View File

@ -14,17 +14,18 @@
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<!--
<div class="ui header">
<div class="content">
Alias Hostname
<div class="sub header epname"></div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui divider"></div>-->
<div class="scrolling content ui form">
<div id="inlineEditBasicAuthCredentials" class="field">
<p>Enter alias hostname or wildcard matching keywords for <code class="epname"></code></p>
<table class="ui very basic compacted unstackable celled table">
<table class="ui basic very compact unstackable celled table">
<thead>
<tr>
<th>Alias Hostname</th>
@ -50,10 +51,6 @@
</div>
</div>
</div>
<div class="ui divider"></div>
<div class="field" >
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
</div>
</div>
<br><br><br><br>
@ -164,7 +161,7 @@
}
$("#inlineEditTable").append(`<tr>
<td>${domainLink}</td>
<td><button class="ui basic button" onclick="removeAliasDomain('${aliasDomain}');"><i class="red remove icon"></i> Remove</button></td>
<td><button class="ui basic mini circular icon button" onclick="removeAliasDomain('${aliasDomain}');"><i class="red trash icon"></i></button></td>
</tr>`);
});

View File

@ -14,18 +14,11 @@
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Basic Auth Settings
<div class="sub header" id="epname"></div>
</div>
</div>
<div class="ui divider"></div>
<h3 class="ui header">Basic Auth Credential</h3>
<div class="scrolling content ui form">
<div id="inlineEditBasicAuthCredentials" class="field">
<p>Enter the username and password for allowing them to access this proxy endpoint</p>
<table class="ui very basic compacted unstackable celled table">
<table class="ui basic very compacted unstackable celled table">
<thead>
<tr>
<th>Username</th>
@ -56,7 +49,7 @@
<h3 class="ui header">Authentication Exclusion Paths</h3>
<div class="scrolling content ui form">
<p>Exclude specific directories / paths which contains the following subpath prefix from authentication. Useful if you are hosting services require remote API access.</p>
<table class="ui very basic compacted unstackable celled table">
<table class="ui basic very compacted unstackable celled table">
<thead>
<tr>
<th>Path Prefix</th>
@ -86,10 +79,6 @@
<code>/public/res/far/boo/</code></p>
</div>
</div>
<div class="ui divider"></div>
<div class="field" >
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
</div>
</div>
<br><br><br><br>
@ -232,7 +221,7 @@
data.forEach(function(rule){
$("#exclusionPaths").append(` <tr>
<td>${rule.PathPrefix}</td>
<td><button class="ui red basic mini icon button" onclick="removeExceptionPath(this);" prefix="${rule.PathPrefix}"><i class="ui red times icon"></i></button></td>
<td><button class="ui red basic mini circular icon button" onclick="removeExceptionPath(this);" prefix="${rule.PathPrefix}"><i class="ui red times icon"></i></button></td>
</tr>`);
})
}
@ -261,7 +250,7 @@
var row = '<tr>' +
'<td>' + username + '</td>' +
'<td>' + password + '</td>' +
'<td><button class="ui basic button" onclick="removeCredentialFromEditingList(' + i + ');"><i class="red remove icon"></i> Remove</button></td>' +
'<td><button class="ui basic tiny circular button" onclick="removeCredentialFromEditingList(' + i + ');"><i class="red remove icon"></i> Remove</button></td>' +
'</tr>';
tableBody.append(row);

View File

@ -27,6 +27,11 @@
body.darkTheme #permissionPolicyEditor .experimental{
background-color: rgb(41, 41, 41);
}
.advanceoptions{
background: var(--theme_advance) !important;
border-radius: 0.4em !important;
}
</style>
</head>
<body>
@ -34,19 +39,12 @@
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Custom Headers
<div class="sub header" id="epname"></div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui small pointing secondary menu">
<a class="item active narrowpadding" data-tab="customheaders">Custom Headers</a>
<a class="item narrowpadding" data-tab="security">Security Headers</a>
</div>
<div class="ui tab basic segment active" data-tab="customheaders">
<table class="ui very basic compacted unstackable celled table">
<table class="ui basic very compacted unstackable celled table">
<thead>
<tr>
<th>Key</th>
@ -171,10 +169,6 @@
<br><br>
<button class="ui basic button" onclick="savePermissionPolicy();"><i class="green save icon"></i> Save</button>
</div>
<div class="field" >
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
</div>
</div>
<br><br><br><br>
@ -189,7 +183,7 @@
let payloadHash = window.location.hash.substr(1);
try{
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
$("#epname").text(payloadHash.ep);
//$("#epname").text(payloadHash.ep);
editingEndpoint = payloadHash;
}catch(ex){
console.log("Unable to load endpoint data from hash")

View File

@ -35,7 +35,7 @@
#accessRuleList{
padding: 0.6em;
border: 1px solid rgb(228, 228, 228);
/* border: 1px solid rgb(228, 228, 228); */
border-radius: 0.4em !important;
max-height: calc(100vh - 15em);
min-height: 300px;
@ -65,13 +65,6 @@
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Host Access Settings
<div class="sub header" id="epname"></div>
</div>
</div>
<div class="ui divider"></div>
<p>Select an access rule to apply blacklist / whitelist filtering</p>
<div id="accessRuleList">
<div class="ui segment accessRule">
@ -85,9 +78,7 @@
</div>
</div>
<br>
<button class="ui basic button" onclick="applyChangeAndClose()"><i class="ui green check icon"></i> Apply Change</button>
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
<!-- <button class="ui basic button" onclick="applyChange()"><i class="ui green check icon"></i> Apply Change</button> -->
<br><br><br>
</div>
@ -174,6 +165,35 @@
let accessRuleID = $(accessRuleObject).attr("ruleid");
$(".accessRule").removeClass('active');
$(accessRuleObject).addClass('active');
//Updates 2025-06-10: Added auto save on change feature
applyChange();
}
function applyChange(){
let newAccessRuleID = $(".accessRule.active").attr("ruleid");
let targetEndpoint = editingEndpoint.ep;
$.cjax({
url: "/api/access/attach",
method: "POST",
data: {
id: newAccessRuleID,
host: targetEndpoint
},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false);
}else{
parent.msgbox("Access Rule Updated");
//Modify the parent list if exists
if (parent != undefined && parent.updateAccessRuleNameUnderHost){
parent.updateAccessRuleNameUnderHost(targetEndpoint, newAccessRuleID);
}
}
}
});
}
function applyChangeAndClose(){

View File

@ -21,15 +21,8 @@
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Edit Tags
<div class="sub header" id="epname"></div>
</div>
</div>
<div class="ui divider"></div>
<p>Tags currently applied to this host name / proxy rule</p>
<div style="max-height: 300px; overflow-y: scroll;">
<div>
<table class="ui compact basic unstackable celled table">
<thead>
<tr>
@ -68,9 +61,7 @@
</div>
<button class="ui basic button" onclick="joinSelectedTagGroups();"><i class="ui blue plus icon"></i> Join tag group(s)</button>
</div>
<div class="ui divider"></div>
<!-- <button class="ui basic button" onclick="saveTags();"><i class="ui green save icon"></i> Save Changes</button> -->
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
<br><br>
</div>
<script>
let editingEndpoint = {};
@ -164,6 +155,10 @@
function addSelectedTags() {
let tags = $('#tagsInput').val().split(',').map(tag => tag.trim());
if (tags.length == 0 || (tags.length == 1 && tags[0] == "")){
parent.msgbox("Please enter at least one tag", false);
return;
}
tags.forEach(tag => {
if (tag && !tagAlreadyExistsInTable(tag)) {
addTagRow(tag);
@ -210,8 +205,8 @@
const row = `<tr class="tagEntry" value="${tag}">
<td><div class="ui circular label tag-color" style="background-color: ${getTagColorByName(tag)};"></div> ${tag}</td>
<td>
<button title="Delete Tag" class="ui circular mini red basic icon button" onclick="removeTag('${tag}')">
<i class="trash icon"></i>
<button title="Delete Tag" class="ui circular mini basic button" onclick="removeTag('${tag}')">
<i class="red trash icon"></i> Delete
</button>
</td>
</tr>`;

View File

@ -75,13 +75,6 @@
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Upstreams / Load Balance
<div class="sub header epname"></div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui small pointing secondary menu">
<a class="item active narrowpadding" data-tab="upstreamlist">Upstreams</a>
<a class="item narrowpadding" data-tab="newupstream">Add Upstream</a>
@ -159,10 +152,6 @@
<br><br>
<button class="ui basic button" onclick="addNewUpstream();"><i class="ui green circle add icon"></i> Create</button>
</div>
<div class="ui divider"></div>
<div class="field" >
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
</div>
</div>
<br><br><br><br>