251 Commits

Author SHA1 Message Date
0e5550487e Merge pull request #644 from tobychui/v3.2.1
- Merged in authentik forward auth support
- Merged IPv6 whitelist patch
- Added support for basic per host name statistic
- Added experimental plugin store
- Added `$remote_ip` in custom header that filters port number from `$remote_addr`
- Fixed origin is not populated in log bug
- Fixed redirection location rewrite bug
2025-04-27 14:54:27 +08:00
9781735983 Moved dev settings to flag
- Moved internal DEVELOPMENT_MODE flag to start parameters
2025-04-27 13:55:54 +08:00
ffc67ede12 Added working plugin store prototype
- Added plugin install and remove api
2025-04-24 21:19:16 +08:00
6750c7fe3d Added wip plugin store
- Added plugin store snippet
- Added plugin list sync functions
- Work in progress install / uninstall plugin function
2025-04-22 07:15:30 +08:00
36c2c9a00e Merge pull request #641 from james-d-elliott/fix-authelia-headers
fix(authelia): original headers
2025-04-21 18:56:28 +08:00
4f026e8c07 fix(authelia): original headers
This fixes the original headers imparted to authelia.
2025-04-21 13:15:21 +10:00
72b100aab0 Update README.md
Fixed contributor list format
2025-04-19 08:45:57 +08:00
291f12e5ea Update README.md
Added community maintained section contact list
2025-04-19 08:44:22 +08:00
0c8dfd8aa0 Added http proxy list link port
- Added port number and http proto to http proxy list link #635
2025-04-12 11:32:22 +08:00
76e2861fea Merge pull request #633 from WHFo/v3.2.1
Update httprp.html
2025-04-11 20:09:36 +08:00
b23b967165 Update httprp.html
Prevent the browser from filling the saved Zoraxy login account into the  input searchInput
2025-04-11 12:02:28 +08:00
d682d52eb7 Merge pull request #631 from Nirostar/patch-1
Fix IPv6 whitelisting for Link-Local addresses by removing the scope ID
2025-04-08 21:12:07 +08:00
23eeeee701 Move Scope ID handling into CIDR check 2025-04-08 15:06:20 +02:00
e961e52dea Fix IPv6 whitelisting for Link-Local addresses by removing the scope ID 2025-04-08 14:44:11 +02:00
b863a9720f Fixed #629
- Added $remote_ip to remote port number from remote address
2025-04-07 20:02:04 +08:00
ca7cd0476c Updated location rewrite logic
- Updated to a much more relax logic for handling domain with port redirection
2025-04-06 17:05:30 +08:00
a3cccee162 Added "blocks common exploits" module
- Added blocks common exploits prototype
- Added bot detection function (not included in dpcore yet) #615
2025-04-06 16:50:59 +08:00
b9b992a817 Fixed #626
- Added checks for port in hostname redirection in dpcore util
2025-04-06 16:49:44 +08:00
19d5695f1a Merge pull request #627 from andyburri/freebsd
Add FreeBSD amd64 support
2025-04-06 15:21:28 +08:00
bcfc777d15 Feat: Add FreeBSD amd64 support 2025-04-06 07:09:54 +00:00
caa64ada76 Update Dockerfile 2025-04-05 15:41:29 -04:00
ac91a3fef1 Fixed #201 and #608
- Added domain / host name specific statistics
- Added upstream forward request count
2025-04-03 13:35:34 +08:00
05f1743ecd Fixed #681
- Fixed origin is not populated in log bug
2025-04-02 20:11:43 +08:00
d4c1225f75 Merge pull request #568 from JokerQyou/feature/authentik-forward-auth
[WIP] Add Authentik forward auth support
2025-03-31 20:05:52 +08:00
f245a61d32 Merge pull request #616 from SamuelPalubaCZ/main
Create docker-compose.yml
2025-03-31 18:47:40 +08:00
5c2b8e4c31 Create docker-compose.yml
So portainer can use latest compose automaticaly
2025-03-31 01:01:22 +02:00
f6eef46d3f Update README.md
Removed GAN related parameters
2025-03-30 19:18:10 +08:00
3adc669db9 Update main.css
Optimized web css
2025-03-30 19:16:56 +08:00
85201885f0 Update main.css 2025-03-30 12:35:39 +08:00
44b65d1bfa Fixed homepage css bug on mobile view 2025-03-30 12:34:37 +08:00
6cb9e8e427 Fix Docker workflow 2025-03-29 11:34:54 -04:00
d4b1cc8c57 Fixed css bug
- Fixed menu become transport on mobile view
- Fixed css font not correctly loaded on page initiate
2025-03-29 16:09:35 +08:00
0e749e8a41 Fixed favicon path 2025-03-29 14:21:22 +08:00
2c219eceef Merge pull request #601 from tobychui/v3.2.0
- Removed GAN and moved to plugin
- Added upnp port forwarder plugin
- Added loopback detection to whitelist
- Optimized embed web resources router implementation
- Completed plugin prototype interface
- Updated dpcore concurrent implementation
- Merged traffic log update
- Updated homepage
- Fixed memory leak on netstat
- Fixed and updated minor dark theme color pallets
2025-03-29 13:03:40 +08:00
92a27cbeb8 Merge pull request #614 from PassiveLemon/docker-320
Update Docker image to 3.2.0
2025-03-29 12:47:44 +08:00
b8a47dc620 Update Docker build workflow
Added a pull for the previous latest so the layers from those can be reused
2025-03-28 23:59:44 -04:00
c4266559be Update Docker image for 3.2.0 2025-03-28 23:58:56 -04:00
136989f2ea Added plugin dir parameter
- Added plugin dir parameter
- Fixed critical architectural bug that effects plugin UI in production mode
- Updated implementation of embed FS routing
- Minor dark theme update
- Fixed ztnc UI bug for msgbox and confirm box
2025-03-28 21:24:18 +08:00
3e031605fc Disable dev mode for release 2025-03-25 19:11:56 +08:00
eb265e3e94 Added comments to warn future dev
- Added timestamps label cleaning
- Added comments so it doesn't get commented again in future (related to #600)
2025-03-25 07:55:13 +08:00
8504ff16cb Fixed #600
- Added experimental fix to memory issue (not sure if this is related but it seems after uncommenting these the memory issue no longer show up on Firefox and Edge)
2025-03-25 07:40:39 +08:00
b71437058f Fine tuned dark theme color 2025-03-23 12:47:23 +08:00
4d16758e0a Merge pull request #593 from Raithmir/main
Update logger to include UserAgent
2025-03-22 12:32:13 +08:00
f2b4c47805 Update trafficlog.go
Update logger to include browser UserAgent string in log lines. This will allow Crowdsec to filter for known bad useragents.
2025-03-21 16:59:22 +00:00
7dff4f83b4 Update trafficlog.go
Add UserAgent to log lines.
2025-03-21 16:48:47 +00:00
eb24bc0391 Fixed #591
- Updated dark theme color pallets
- Added automatic background change on login page
2025-03-20 21:51:47 +08:00
dac3e8c925 Added German localization to homepage 2025-03-19 21:27:41 +08:00
3f1c1f1395 Added screenshot zoom feature 2025-03-19 21:09:42 +08:00
cd15fdf3c1 Updated new project hompage (wip) 2025-03-18 21:43:00 +08:00
0fdfda436b Added plugin info view
- Added plugin info view
- Removed zerotier related start parameters
- Added automatic tag filter reset on tab change in http proxy page
2025-03-16 11:41:46 +08:00
f8270e46c2 Added UI for plugin system and upnp example
- Added wip UI for plugin tag system
- Added upnp port forwarder plugin
- Added error and fatal printout for plugins
- Optimized UI flow for plugin context window
- Added dev web server for plugin development purpose
2025-03-15 21:02:44 +08:00
4a99afa2f0 Fixed deadlock in plugin manager close
- Moved all mutex away from defer
- Updated plugin manager close logic to prevent deadlock
2025-03-11 21:22:09 +08:00
dfd5ef5578 Modernized dpcore code
- Rewritten dpcore transport object with deprecated API removed
- Optimized concurrent connection counts from 32 to 256
- Updated random port range for plugins
- Added debug output to plugin library
2025-03-10 22:00:33 +08:00
3e57a90bb6 Fixed #573
- Added whitelist loopback quick toggle
- Fixed plugin exit stuck bug
2025-03-09 17:02:48 +08:00
23d4df1ed7 Added dynamic capture capabilities to plugin API
- Added dynamic capture ingress and sniff endpoint
- Removed static capture example (API update)
2025-03-02 12:26:44 +08:00
39d6d16c2a Updated plugin interface
- Updated plugin interface to support static path routing
- Added autosave for statistic data (workaround for #561)
2025-03-02 09:15:50 +08:00
b7e3888513 Removed zerotier related info
- Removed zt info from utils.html
2025-03-02 09:14:29 +08:00
fd41a1cb91 Fixed dpcore TLSClientConfig is nil bug 2025-03-02 09:12:29 +08:00
75c351e7e2 Removed GAN
- Removed UI and module of GAN
- this feature is moved to plugin
2025-03-02 09:12:07 +08:00
6a8057c3a7 fix passing wrong URI to Authentik outpost 2025-03-02 00:04:08 +08:00
ebf6ad6600 Add Authentik forward auth support 2025-03-01 15:29:36 +08:00
549e492ffd Merge pull request #567 from Morethanevil/main
Update CHANGELOG.md
2025-03-01 15:02:06 +08:00
6351f25c00 Update CHANGELOG.md 2025-03-01 06:23:44 +01:00
560b0058cd Merge pull request #566 from tobychui/v3.1.9
- Fixed netstat underflow bug
- Fixed origin picker cookie bug
- Added prototype plugin system
- Added plugin examples
- Added notice for build-in Zerotier network controller deprecation (and will be moved to plugins)
- Added country code display for quickban list
2025-03-01 10:09:46 +08:00
28a0a837ba Plugin lifecycle optimization
- Added term flow before plugin is killed
- Updated example implementations
- Added SIGINT to Zoraxy for shutdown sequence (Fixes #561 ?)
2025-03-01 10:00:33 +08:00
14e1341c34 Removed legacy example plugin files 2025-02-28 22:07:08 +08:00
5abc4ac606 Added plugin context view
- Added plugin context view
- Moved plugin type definition to separate file
- Added wip request forwarder
2025-02-28 22:05:14 +08:00
214b69b0b8 Updated example plugins
- Updated example plugins
- Added debugger
- Removed some trash files
2025-02-28 22:03:08 +08:00
3993ac954c Fixed #247
- Added country of origin row for quickban list
2025-02-28 22:01:11 +08:00
53657e8716 Added embed server for plugin library
- Added embeded resources server for plugin library
- Added ztnc plugin for global area network
- Added wide mode for side wrapper
2025-02-28 15:46:57 +08:00
bddff0cf2f Added working plugin manager prototype
- Added experimental plugin UI proxy
- Added plugin icon loader
- Added plugin table renderer
2025-02-27 22:27:13 +08:00
dd4df0b4db Update originPicker.go
- Removed unused function
2025-02-26 21:20:35 +08:00
85709dacf6 Fixed #550
- Updated to not set the session cookie and lets the fallback method to detect for change in upstreams
2025-02-26 21:19:41 +08:00
ad13b33283 Added plugin prototype
- Added proof of concept plugin prototype
- Added wip plugin page
2025-02-25 21:14:03 +08:00
20959cd6cc Fixed #554
- Removed passive load balancer and default to active lb only
2025-02-20 20:25:20 +08:00
394cf50e1d #550
- Instead of clearing the Zoraxy cookie on the client side, set the Zoraxy session in the server side to an empty value instead
2025-02-19 21:38:27 +08:00
1116b643b5 Added plugin interface definations
- Added wip plugin interface
- Merged in PR for lego update
- Minor code optimization
2025-02-19 21:25:50 +08:00
2e9d70da83 Merge pull request #545 from tobychui/bugfix_acme_LE_http01
Fixed lets encrypt ACME fail bug
2025-02-18 18:50:50 +08:00
6130459f7c Update issue templates
Added more details for bug templates
2025-02-18 18:50:34 +08:00
2d29065812 Merge pull request #546 from Morethanevil/main
Update CHANGELOG.md
2025-02-17 06:23:57 +08:00
2be7f711ba Update CHANGELOG.md 2025-02-16 19:42:23 +01:00
de9d3bfb65 Fixed netstat underflow bug
- Fixed netstat sometime underflow to a large negative number bug
2025-02-16 21:10:56 +08:00
3e4c66b34f Updated lego
- Updated lego to fix ACME issue on lets encrypt
2025-02-16 20:44:02 +08:00
895ee1e53f Merge pull request #544 from tobychui/v3.1.8
- Exposed timeout value from dpcore to UI
- Added active load balancing (if uptime monitor is enabled on that rule)
- Refactorized io stats and remove dependencies over wmic
- Removed SMTP input validation
- Fixed sticky session bug
- Fixed passive load balancer bug
- Fixed dockerfile bug
2025-02-16 17:13:37 +08:00
caf4ab331b Exposed dpcore timeout options
- Exposed idle timeout and response timeout option
- Updated upstream edit UI to use the new API
- Updated geodb
2025-02-16 16:58:25 +08:00
36c1f149e6 Fixed #497
- Removed SMTP input validations
- Updated version no.
- Added todo for removing SMTP all together in future revisions
2025-02-16 09:08:08 +08:00
b0dc4d6670 Fix #542 2025-02-15 16:26:10 -05:00
5d8bec7f24 Fixed sticky session bug
- Fixed sticky session bug in new active fallback lb implementation
2025-02-14 22:53:29 +08:00
32f60dfba6 Fixed #523
- Fixed passive fallback logic
- Added active fallback setting notify from uptime monitor
2025-02-14 22:04:51 +08:00
0abe4c12cf Fixed #526
- Fixed typos
2025-02-12 20:58:22 +08:00
7555611ba5 Fixed h2c enable crash bug
- Moved h2c roundtripper to a dedicated module
- Fixed h2c enable crash bug
2025-02-11 21:53:21 +08:00
e624227dae Merge pull request #520 from eyerrock/wmic-refactor
Remove WMIC dependency and unify network stats retrieval
2025-02-11 19:38:56 +08:00
27695584ab Update README.md
Added new start flags into README
2025-02-09 13:44:55 +08:00
e47a7a8357 Merge pull request #525 from Morethanevil/main
Update CHANGELOG.md
2025-02-09 10:53:13 +08:00
3246f8ea2c Update CHANGELOG.md
:)
2025-02-08 23:52:37 +01:00
ccbda6d7c2 refactored io stats 2025-02-08 16:11:47 +01:00
a7285438af Merge pull request #522 from tobychui/v3.1.7
- Merged and added new tagging system for HTTP Proxy rules
- Added inline editing for redirection rules
- Added uptime monitor status dot detail info (now clickable)
- Added close connection support to port 80 listener
- Optimized port collision check on startup
- Optimized dark theme color scheme (Free consultation by [3S Design studio](https://www.3sdesign.io/))
- Fixed capital letter rule unable to delete bug
2025-02-08 18:40:15 +08:00
693dba07b7 Updated tag filtering
- Added automatic empty tag removal when creating new proxy rule
2025-02-08 17:07:26 +08:00
9b64278200 Merge pull request #521 from PassiveLemon/docker-term-fix
Refactor: Launch services in background and trap Docker TERM signal
2025-02-08 16:09:38 +08:00
d04eff2bda Updated geodb
- Updated geoip database
2025-02-08 16:08:33 +08:00
3320b56b19 Update tagEditor.html
- Optimized UX for tag editor
- Finished integration of tag system
2025-02-08 15:19:36 +08:00
99728144b3 Refactor: Launch services in background and trap Docker TERM signal 2025-02-08 01:37:03 -05:00
05511ed4ca Updated tag system design
- Added search-able tag dropdown
- Implemented realtime quick search
- Added better tag coloring
2025-02-07 22:08:56 +08:00
70abfe6fcf Restore dockerfile
- The docker file change shd be included in another PR
2025-02-06 20:36:23 +08:00
6ab91c377f Merge pull request #509 from adoolaard/dev-tags
Add Tagging Feature for Reverse Proxy Hosts + Search & Filter
2025-02-06 20:35:32 +08:00
1863af0d63 Minor css update
- Changed inline edit button for redirection rule to circular to match http proxy rule page
2025-02-05 20:33:38 +08:00
2a9d87787d Fixed #510
- Added inline edit for redirection rule
2025-02-05 20:24:42 +08:00
f753becd66 The proxy hosts broke on import, because the tags were missing. This is now fixed. 2025-02-03 15:10:13 +01:00
bb2d0d5b46 Fixed #507 2025-02-03 21:10:24 +08:00
07dc63a82c Added H2C (experimental)
- Added experimental H2C transporter
- Exposed default listening port and web server listen state to start parameters #474
2025-02-03 20:36:34 +08:00
97a6cf016a Point on the I 2025-01-31 00:17:10 +01:00
8df68f1f4e Zoeken en filteren werkt ook! 2025-01-30 22:48:48 +01:00
e4ad505f2a Tags editor works! 2025-01-30 22:42:06 +01:00
a402c4f326 Tags are working, just not yet editable 2025-01-30 22:22:42 +01:00
791fbfa1b4 Updated gitignore 2025-01-30 21:48:40 +01:00
c49f2fd1db Changed dockerfile to better cache 2025-01-30 21:22:19 +01:00
7d9f240d56 Updated Close Conn resp for TLS
- Use No Resp instead of 200 for close connection mode default site settings
2025-01-18 22:10:45 +08:00
e20f816080 Fixed #467
- Added status dot info in uptime monitor
- simplified the no response record to no_resp in default site
2025-01-18 21:49:35 +08:00
eeb438eb18 Fixed #474
- Added automatic port check and reminder for beginners
2025-01-18 15:19:55 +08:00
bfd64a885e Removed confirm from access
- Removed troublesome confirm popup from black / whitelist
- Minor fix to checkbox css
2025-01-15 20:59:09 +08:00
45f61b3053 Optimized dark theme mode
- Make dark theme mode less dark
2025-01-15 20:44:20 +08:00
0d4c71d0f6 Fixed #450 2024-12-31 22:56:51 +08:00
d1e5581eea Merge pull request #449 from tobychui/v3.1.6
- Exposed log file, sys.uuid and static web server path to start flag
- Optimized connection close implementation
- Added toggle for uptime monitor
- Added optional copy HTTP custom headers to websocket connection
2024-12-31 21:49:41 +08:00
be5797c8a5 Updated geodb and minor instructions 2024-12-31 21:47:19 +08:00
ebd316a7f1 Exposed log and db filepath setting 2024-12-31 21:14:37 +08:00
84aec4387a Added CF and Fastly IP in access list
Added CF and Fastly Client IP passthrough header for access control ip resolver
2024-12-31 20:30:36 +08:00
30dfb9cb65 Added new UI feature
- Added toggle for uptime monitor
- Added toggle for enable custom header passthrough to websocket
2024-12-30 21:41:15 +08:00
0b1768ab5b Added manual toggle for websocket header copy
- Added setting for toggling websocket header copy
- Added close connection in TLS mode
2024-12-30 21:07:29 +08:00
ad4721820b Added websocket header test and benchmark tool 2024-12-30 21:01:45 +08:00
1d4c275db3 Fixed nil pointer exception in new setups 2024-12-29 16:11:00 +08:00
b3ad97743c Fixed #444
- Restored legacy behavior if proxmox cookie is detected in request
2024-12-29 15:09:24 +08:00
1a6a87e79b Merge pull request #443 from Morethanevil/main
Update CHANGELOG.md
2024-12-28 15:19:43 +08:00
749fd4b7af Update CHANGELOG.md 2024-12-28 05:25:00 +01:00
85422c0a74 Merge pull request #439 from tobychui/v3.1.5
Fixed hostname case sensitive bug
Fixed ACME table too wide css bug
Fixed HSTS toggle button bug
Fixed slow GeoIP resolve mode concurrent r/w bug
Added close connection as default site option
Added experimental authelia support
Added custom header support to websocket
Added levelDB as database implementation (not currently used)
Added external GeoIP db loading support
Restructured a lot of modules
2024-12-27 22:12:55 +08:00
73999c1ae9 Merge pull request #440 from PassiveLemon/docker-3.1.5
Add 2 new flags to Docker container and image build instructions
2024-12-27 21:26:18 +08:00
0ad84b3415 Add 2 new flags 2024-12-26 16:17:02 -05:00
64b6769695 Added external geoip db option
- Added support for loading geoip db from external file
- Added -update_geoip flag for automatically update the geoip
2024-12-24 21:12:26 +08:00
e72b2f9e09 Updated geoip database 2024-12-24 20:34:10 +08:00
992dd231f2 Fixed #435 2024-12-22 13:25:16 +08:00
49555c1191 Fixed #430
+ Added no response and I'm a Teapot (config file editing only) to default site options
2024-12-17 22:08:32 +08:00
2fca458bd0 Image building instructions and README touch-ups 2024-12-16 18:14:02 -05:00
2423d0fb3a Added experimental authelia support
- Integrated #33 code snippet
- Added UI for setting Authelia server address
- Updated authentication provider implementation
2024-12-15 15:52:59 +08:00
bb0f55018c System arch optimization
- Optimized types and definitions
- Moved shutdown seq to start.go file
- Moved authelia to auth/sso module
- Added different auth types support (wip)
- Updated proxy config structure
- Added v3.1.4 to v3.1.5 auto upgrade utilities
- Fixed #426
- Optimized status page UI
- Added options to disable uptime montior in config
2024-12-12 20:49:53 +08:00
9e95d84627 Fixed #422
- Added scroll to acme domain table
2024-12-10 21:13:26 +08:00
e73841786b Merge pull request #421 from 7brend7/authelia-integration
Add authelia-verify support
2024-12-10 21:02:58 +08:00
d5449c947a Add authelia-verify suppport 2024-12-09 15:19:07 +02:00
8ff51044bb Fixed #414
- Added sticky menu
- Optimized terminate routine for nil check
- Added test case for statistic module
2024-12-08 12:54:50 +08:00
cc08c704de Database update
- Removed read-only mode
- Added LevelDB for big data storage

TODO: Update backup utilities to support new db structure
2024-12-06 23:34:21 +08:00
2f1a6b5ba4 Merge pull request #416 from tobychui/main
Sync update from main branch
2024-12-06 19:46:51 +08:00
4d163fe80f Merge pull request #406 from Sickjuicy/main
Domain Name Server Option
2024-12-06 19:29:29 +08:00
24371ed22e Fixed #415
- Fixed UI issue on the HSTS toggle
- Added error message on save error for HSTS
2024-12-06 19:06:59 +08:00
12358d3522 added downward compality and spaces are cut from the json 2024-12-01 16:01:23 +01:00
c39af1ff8e Update def.go
Updated version number
2024-12-01 21:22:43 +08:00
6bf944e13c Fixed #401
- Fixed high concurrency panic on slow geoIP resolve mode
- Added test case for concurrent geodb access
2024-12-01 21:21:53 +08:00
b653b805b8 Update autorenew.go 2024-12-01 04:29:29 +01:00
eb91865b70 Added to read json for the renew cert and fixed bug where on creation of a new cert the old NameServer ware used 2024-12-01 04:25:01 +01:00
57e72a8a90 added some commands back 2024-11-30 04:38:29 +01:00
4dbf110edc more Cleanup 2024-11-30 04:20:39 +01:00
1eefa99b72 Cleanup 2024-11-30 03:35:30 +01:00
e6b2d458f7 Added Custom Name Server Option 2024-11-26 23:30:24 +01:00
4a4483e09d Merge pull request #400 from Morethanevil/main
Update CHANGELOG.md
2024-11-24 20:05:02 +08:00
4485d1f811 Update CHANGELOG.md
Updated changelog

Great Work as always, dark mode looks cool. If you want a suggestion about colors, I recommend [Catppuccin](https://github.com/catppuccin)
2024-11-24 11:52:03 +01:00
0eb0696670 Merge pull request #399 from tobychui/v3.1.4
V3.1.4
2024-11-24 14:47:53 +08:00
9fca2354c6 Update darktheme.css
Fixed docker container list text theme color
2024-11-24 14:41:01 +08:00
e56b045689 Added dark theme to docker container list 2024-11-24 13:58:46 +08:00
763ccb4d60 Remove deprecated ZeroTier config directory from Docker readme 2024-11-24 00:44:52 -05:00
4d4492069d Merge branch 'main' into v3.1.4 2024-11-24 12:37:58 +08:00
f3591aa171 Update dockerContainersList.html
Merged PR into dark theme branch
2024-11-24 12:35:26 +08:00
2dcf578cbe Update README.md 2024-11-24 11:52:57 +08:00
23a5c6ceb0 Updated geoIP database 2024-11-24 11:46:49 +08:00
015889851a Optimized UX and code structure
+ Added automatic self-sign certificate sniffing
+ Moved all constant into def.go
+ Added auto restart on port change when proxy server is running
+ Optimized slow search geoIP resolver by introducing new cache mechanism
+ Updated default incoming port to HTTPS instead of HTTP
2024-11-24 11:38:01 +08:00
093ed9c212 Merge pull request #395 from eyerrock/container-searchbar
search bar for Docker container list
2024-11-21 21:47:38 +08:00
0af8c67346 Updated API register function
- Seperated different register for APIs
2024-11-19 21:13:02 +08:00
c5170bcb94 Refactorized main entry function
- Moved constants to def.go
- Added acme close function (not used for now)
- Added robots.txt to prevent webmin panel being scanned by search engine
2024-11-19 20:30:36 +08:00
cd48388c02 refactored docker container list 2024-11-18 21:01:54 +01:00
373845f8fd added searchbar to docker container list 2024-11-18 18:16:07 +01:00
293a527ffc Completed dark theme 2024-11-18 21:04:25 +08:00
e4facbc7b6 Added more dark themes
- Added wrappers for snippet dark theme
- Optimized color pallets
2024-11-17 17:41:22 +08:00
1c79fa4e96 Fixed #394 2024-11-17 08:38:13 +08:00
6515eb99e3 Fixed #393
Updated version code manually
2024-11-15 06:48:35 +08:00
ec5c24b9b8 Added more darktheme
- Added more dark theme css
- Merged main branch fixes and new features
- Added todo tag for custom timeout
2024-11-14 21:18:05 +08:00
df88084375 Merge pull request #391 from eyerrock/list-containers-with-unexposed-ports
list containers with unexposed ports
2024-11-14 20:06:31 +08:00
74017baecf Merge pull request #392 from PassiveLemon/zoraxy-volume
Symlink ZeroTier var to Zoraxy config
2024-11-13 18:42:35 +08:00
294d504ee6 Symlink ZeroTier var to Zoraxy config 2024-11-12 12:40:08 -05:00
477429900e list containers with unexposed ports 2024-11-11 21:07:07 +01:00
2e9bc77a5d Merge commit from fork
Fixed web ssh security bug
2024-11-10 13:57:01 +08:00
ed178d857a Fixed web ssh security bug 2024-11-10 13:22:32 +08:00
4cf5d29692 Added more dark theme 2024-11-09 16:12:41 +08:00
634e9c9855 v3.1.3 init commit
- Fixed #378
- Added wip dark theme
- Fixed in code typo
- Fixed int conversion bug in some DNS challenge supplier
2024-11-08 22:24:07 +08:00
e79a70b7ac Merge pull request #376 from PassiveLemon/actions-cache
Add layer caching to Docker action
2024-11-06 06:58:52 +08:00
779115d06b Add layer caching to Docker action 2024-11-04 20:39:47 -05:00
9cb315ea67 Merge pull request #373 from Morethanevil/main
Update CHANGELOG.md
2024-11-03 17:41:49 +08:00
43ba00ec8d Update CHANGELOG.md
Thanks for your work :)
2024-11-03 10:11:20 +01:00
4577fb1f2f Merge pull request #368 from tobychui/v3.1.2
v3.1.2
2024-11-03 10:57:06 +08:00
f877bf9eda Update reverseproxy.go
Fixed typo
2024-11-03 09:31:24 +08:00
363b9b6d94 Merge branch 'main' into v3.1.2 2024-11-02 15:26:41 +08:00
c5ca68868b Optimized ACME logic
- Added automatic port 80 listener enable for those who don't read our wiki
- Reduced default interval for polling and propagation timeout
2024-10-28 21:40:58 +08:00
f927bb539a Updated geodb
- Updated geoip table
- Fixed bug in streamproxy delete in dev version
- Commented SSO related features (WIP) for release
2024-10-27 16:35:59 +08:00
5f64b622b5 Fixed #353 and #327
- Added user defined polling and propagation timeout option in ACME
- Updated lego and added a few new DNS challenge providers
- Updated code gen to support new parameters
2024-10-27 16:17:44 +08:00
9a371f5bcb Updated code generator for lego
- Removed windows 7 support
2024-10-27 15:40:53 +08:00
172c5afa60 Added support for custom header variables
- Added support for using nginx-like variables in custom headers
- Supported variables includes: $host, $remote_addr, $request_uri, $request_method, $content_length, $content_type, $uri, $args, $scheme, $query_string, $http_user_agent and $http_referer
- Added test case for custom header variable rewriter
2024-10-27 14:47:01 +08:00
f98e04a9fc Fixed #318
- Added support for automatic X-Remote-User header when basic auth is enabled
- Moved header logic to rewrite module (new module)
- Added default site automatic fix for URL missing http:// or https:// prefix
2024-10-26 22:21:49 +08:00
99295cad86 Fixed #342
- Added port scanner
- Moved handlers for IP scanner into ipscan module
-Minor code optimization
2024-10-26 19:41:43 +08:00
95d0a98576 Merge pull request #358 from eltociear/patch-1
Fixed typo in reverseproxy.go
2024-10-26 18:46:51 +08:00
00bfa262cb docs: update reverseproxy.go
Defination -> Definition
2024-10-26 18:46:42 +09:00
528be69fe0 Optimized stream proxy codebase
- Moved stream proxy config from database to file based conf
- Optimized implementation for detecting proxy rule running
- Fixed #320 (hopefully)
2024-10-25 23:30:44 +08:00
6923f0d200 Fixed #328
- Fixed register enter not working
- Updated all link to new project domain (aroz.org)
2024-10-23 21:31:06 +08:00
7255b62e31 Merge pull request #344 from tobychui/main
Update development branch to match new project URL and docker config
2024-10-23 21:11:09 +08:00
cf14d12c31 Update index.html
Updated all links to aroz.org
2024-10-20 17:36:51 +08:00
90cf26306a Update CNAME 2024-10-20 17:25:28 +08:00
cab2f4e63a Fixed #316
Fixed early renew day not passed into auto renewer config bug
2024-09-26 22:57:49 +08:00
75d773887c Merge pull request #308 from PassiveLemon/Fix_307
Fix #307
2024-09-17 10:58:27 +08:00
a944c3ff36 Fix #307 2024-09-16 13:09:37 -04:00
465f332dfc Merge pull request #305 from PassiveLemon/ZeroTierFix
Fix: Build older version of ZeroTier
2024-09-15 23:43:49 +08:00
dfda3fe94b Fix: Build older version of ZeroTier
Anything from 1.12.0+ just doesn't work on Zoraxy
2024-09-14 01:23:17 -04:00
5c56da1180 Added basic oauth module structure (wip)
- Added struct for oauth
- Added interception handler for Zoraxy SSO
- Added user structure for SSO
2024-09-12 10:55:01 +08:00
3392013a5c Fixed #297
- Added UI to showcase ZeroSSL do not support DNS challenge
- Added test case for origin picker
- Updated zerotier struct info (wip)
2024-09-09 21:12:12 +08:00
8b4c601d50 Merge pull request #298 from Morethanevil/main
Update CHANGELOG.md
2024-09-05 08:18:09 +08:00
3a2eaf8766 Update CHANGELOG.md 2024-09-04 17:44:52 +02:00
a45092a449 Patched #274 2024-09-04 22:05:54 +08:00
d5315e5b8e Merge pull request #289 from tobychui/v3.1.1
v3.1.1 update
2024-09-04 21:35:21 +08:00
31cc1a69a1 Merge pull request #295 from PassiveLemon/zerotier
Add ZeroTier to Docker container
2024-09-01 23:01:20 +08:00
d348cbf48b Update Docker README 2024-08-30 09:47:02 -04:00
f6339868ac Refactor Dockerfile and bundle ZeroTier 2024-08-30 09:47:02 -04:00
af10f2a644 Fix typos and inconsistencies in README 2024-08-28 18:27:49 -04:00
3b247c31da Fixed typo in README 2024-08-27 10:18:08 +08:00
d74e8badb9 Fixed #287
- Removed unusded tab switch in quicksetup.js
- Changed Macedonia to North Macedonia
2024-08-25 13:12:07 +08:00
b40131d212 Updated geodb and merged PR from main 2024-08-23 17:52:36 +08:00
563a12c860 Merge pull request #286 from ahmadsyamim/patch-1
Fix typo remvoeClass to removeClass
2024-08-23 17:37:52 +08:00
8b2c3b7e03 Fix typo remvoeClass to removeClass 2024-08-23 09:51:34 +08:00
608cc0c523 Optimized upstream & loadbalancer
- Test and optimized load balancer origin picker
- Fixed no active origin cannot load proxy rule bug
- Implemented logger design in websocket proxy module
- Added more quickstart tours
- Fixed #270 (I guess)
- Fixed #90 (I guess)
2024-08-19 16:10:35 +08:00
b558bcbfcf Merge pull request #258 from bouroo/perf/upstreams-sortfunc
weighted random upstream
2024-08-19 15:39:22 +08:00
9ea3fa2542 Added tour for setup https 2024-08-16 22:28:21 +08:00
01f68c5ef5 Added tour for basic operations
- added static website setup tour
- added subdomain setup tour
2024-08-15 22:35:43 +08:00
a7f89086d4 Restructured log format in acme module
- Replaced all log.Println in acme module to system wide logger
- Fixed file manager path escape bug #274
2024-08-13 21:56:23 +08:00
a5ef6456c6 v3.1.1 init
- Fixed path traverse bug in web server file manager
- Merged docker container list from main
- Updated version code
- Merged network status fix from PR
- Removed unused comments in dpcore
-
2024-08-07 13:53:43 +08:00
87659b43bd Merge pull request #278 from JokerQyou/fix/network-io-chart-not-rendering
Fix network I/O chart not rendering.
2024-08-07 13:49:02 +08:00
ddbecf7b68 Merge pull request #280 from 7brend7/fix-added-containers-list
Fix existings containers list in docker popup
2024-08-07 13:40:24 +08:00
1b3a9de378 Fix existings containers list in docker popup 2024-08-04 00:25:13 +03:00
6dd62f509d Update network data instead of assigning new variables. 2024-08-02 22:00:51 +08:00
d5cc6a6859 Fix network I/O chart not rendering.
Close #200.
2024-08-02 00:07:12 +08:00
1d965da7d0 Merge pull request #277 from Morethanevil/main
Update CHANGELOG.md
2024-08-01 08:43:46 +08:00
3567c70bab Update CHANGELOG.md 2024-07-31 19:52:31 +02:00
8a8ec1cb0b 📝 randIndex for fallbackUpstreams random 2024-07-24 14:59:48 +07:00
e53c3cf3c4 ️ fallbackUpstreams with preserve index 2024-07-24 14:47:33 +07:00
d17de5c200 weighted random upstream 2024-07-23 08:50:10 +07:00
97ff48ee70 🔥 origins already checked before getRandomUpstreamByWeight 2024-07-23 08:31:59 +07:00
d64b1174af keep compatible with go 1.20 2024-07-23 08:31:59 +07:00
bec363abab ️ immediate return if single upstream 2024-07-23 08:31:59 +07:00
0dddd1f9e3 📝 discribe for upstream sort func 2024-07-23 08:31:59 +07:00
6bfcb2e1f5 ️ slices.SortFunc for upstreams 2024-07-23 08:31:59 +07:00
301 changed files with 233479 additions and 55816 deletions

View File

@ -28,12 +28,16 @@ If applicable, add screenshots to help explain your problem.
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Host Environment (please complete the following information):**
**Host Environment (please complete following information, DO NOT REMOVE ANY FIELD(S)):**
- Arch: [e.g. arm64]
- Device: [e.g. Bananapi R2 PRO]
- OS: [e.g. Armbian]
- Version [e.g. 23.02 Bullseye ]
- Docker Version (if you are running Zoraxy in docker): [e.g. 3.0.4]
- Are you using Docker? (yes / no)
- Docker Version (fill in "N/A" for native deployment): [e.g. 3.0.4]
**Supplementary links**
If your issue is related to a particular open source project, paste the link here.
**Additional context**
Add any other context about the problem here.

48
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: Build and push Docker image
on:
release:
types: [ published ]
jobs:
setup-build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Pull last image for layer reuse
run: |
docker pull docker.io/zoraxydocker/zoraxy:latest
- name: Setup building file structure
run: |
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
cp -lr $GITHUB_WORKSPACE/example/ $GITHUB_WORKSPACE/docker/
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: ./docker
push: true
platforms: linux/amd64,linux/arm64
tags: |
zoraxydocker/zoraxy:latest
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,40 +0,0 @@
name: Image Publisher
on:
release:
types: [ published ]
jobs:
setup-build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker & GHCR
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Setup building file structure
run: |
cp -r $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
- name: Build the image
run: |
cd $GITHUB_WORKSPACE/docker/
docker buildx create --name mainbuilder --driver docker-container --platform linux/amd64,linux/arm64 --use
docker buildx build --push \
--provenance=false \
--platform linux/amd64,linux/arm64 \
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
--tag zoraxydocker/zoraxy:latest \
.

14
.gitignore vendored
View File

@ -30,7 +30,7 @@ src/certs/*
src/rules/*
src/README.md
docker/ContainerTester.sh
docker/ImagePublisher.sh
docker/docker-compose.yaml
src/mod/acme/test/stackoverflow.pem
/tools/dns_challenge_update/code-gen/acmedns
/tools/dns_challenge_update/code-gen/lego
@ -39,4 +39,14 @@ src/tmp/localhost.pem
src/www/html/index.html
src/sys.uuid
src/zoraxy
src/log/
src/log/
# dev-tags
/Dockerfile
/Entrypoint.sh
# plugins
example/plugins/ztnc/ztnc.db
example/plugins/ztnc/authtoken.secret
example/plugins/ztnc/ztnc.db.lock

View File

@ -1,3 +1,119 @@
# v3.1.9 1 Mar 2025
+ Fixed netstat underflow bug
+ Fixed origin picker cookie bug [#550](https://github.com/tobychui/zoraxy/issues/550)
+ Added prototype plugin system
+ Added plugin examples
+ Added notice for build-in Zerotier network controller deprecation (and will be moved to plugins)
+ Added country code display for quickban list [#247](https://github.com/tobychui/zoraxy/issues/247)
+ Removed passive load balancer and default to active lb only [#554](https://github.com/tobychui/zoraxy/issues/554)
# v3.1.8 16 Feb 2025
+ Exposed timeout value from dpcore to UI
+ Added active load balancing (if uptime monitor is enabled on that rule)
+ Re-factorized io stats and remove dependencies over wmic by [eyerrock](https://github.com/eyerrock)
+ Removed SMTP input validation [#497](https://github.com/tobychui/zoraxy/issues/497)
+ Fixed sticky session bug
+ Fixed passive load balancer bug
+ Fixed dockerfile bug by [PassiveLemon](https://github.com/PassiveLemon)
# v3.1.7 08 Feb 2025
+ Merged and added new tagging system for HTTP Proxy rules [by @adoolaard](https://github.com/adoolaard)
+ Added inline editing for redirection rules [#510](https://github.com/tobychui/zoraxy/issues/510)
+ Added uptime monitor status dot detail info (now clickable) [#467](https://github.com/tobychui/zoraxy/issues/467)
+ Added close connection support to port 80 listener [#405](https://github.com/tobychui/zoraxy/issues/450)
+ Optimized port collision check on startup
+ Optimized dark theme color scheme (Free consultation by 3S Design studio)
+ Fixed capital letter rule unable to delete bug [#507](https://github.com/tobychui/zoraxy/issues/507)
+ Fixed docker statistic not save bug [by @PassiveLemon](https://github.com/PassiveLemon) [#505](https://github.com/tobychui/zoraxy/issues/505)
# v3.1.6 31 Dec 2024
+ Exposed log file, sys.uuid and static web server path to start flag (customizable conf and sys.db path is still wip)
+ Optimized connection close implementation
+ Added toggle for uptime monitor
+ Added optional copy HTTP custom headers to websocket connection [#444](https://github.com/tobychui/zoraxy/issues/444)
# v3.1.5 28 Dec 2024
+ Fixed hostname case sensitive bug [#435](https://github.com/tobychui/zoraxy/issues/435)
+ Fixed ACME table too wide css bug [#422](https://github.com/tobychui/zoraxy/issues/422)
+ Fixed HSTS toggle button bug [#415](https://github.com/tobychui/zoraxy/issues/415)
+ Fixed slow GeoIP resolve mode concurrent r/w bug [#401](https://github.com/tobychui/zoraxy/issues/401)
+ Added close connection as default site option [#430](https://github.com/tobychui/zoraxy/issues/430)
+ Added experimental authelia support [#384](https://github.com/tobychui/zoraxy/issues/384)
+ Added custom header support to websocket [#426](https://github.com/tobychui/zoraxy/issues/426)
+ Added levelDB as database implementation (not currently used)
+ Added external GeoIP db loading support
+ Restructured a lot of modules
# v3.1.4 24 Nov 2024
+ **Added Dark Theme Mode** [#390](https://github.com/tobychui/zoraxy/issues/390) [#82](https://github.com/tobychui/zoraxy/issues/82)
+ Added an auto sniffer for self-signed certificates
+ Added robots.txt to the project
+ Introduced an EU wrapper in the front-end for automatic registration of 26 countries [#378](https://github.com/tobychui/zoraxy/issues/378)
+ Moved all hard-coded values to a dedicated def.go file
+ Fixed a panic issue occurring on unsupported platform exits
+ Integrated fixes for SSH proxy and Docker snippet updates [#330](https://github.com/tobychui/zoraxy/issues/330) [#348](https://github.com/tobychui/zoraxy/issues/348)
+ **Changed the default listening port to 443 and enable TLS by default**
+ Optimized GeoIP database slow-search mode CPU usage
# v3.1.3 12 Nov 2024
+ Fixed a critical security bug [CVE-2024-52010](https://github.com/advisories/GHSA-7hpf-g48v-hw3j)
# v3.1.2 03 Nov 2024
+ Added auto start port 80 listener on acme certificate generator
+ Added polling interval and propagation timeout option in ACME module [#300](https://github.com/tobychui/zoraxy/issues/300)
+ Added support for custom header variables [#318](https://github.com/tobychui/zoraxy/issues/318)
+ Added support for X-Remote-User
+ Added port scanner [#342](https://github.com/tobychui/zoraxy/issues/342)
+ Optimized code base for stream proxy and config file storage [#320](https://github.com/tobychui/zoraxy/issues/320)
+ Removed sorting on cert list
+ Fixed request certificate button bug
+ Fixed cert auto renew logic [#316](https://github.com/tobychui/zoraxy/issues/316)
+ Fixed unable to remove new stream proxy bug
+ Fixed many other minor bugs [#328](https://github.com/tobychui/zoraxy/issues/328) [#297](https://github.com/tobychui/zoraxy/issues/297)
+ Added more code to SSO system (disabled in release)
# v3.1.1. 09 Sep 2024
+ Updated country name in access list [#287](https://github.com/tobychui/zoraxy/issues/287)
+ Added tour for basic operations
+ Updated acme log to system wide logger implementation
+ Fixed path traversal in file manager [#274](https://github.com/tobychui/zoraxy/issues/274)
+ Removed Proxmox debug code
+ Fixed trie tree implementations
**Thanks to all contributors**
+ Fix existing containers list in docker popup [7brend7](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3A7brend7)
+ Fix network I/O chart not rendering [JokerQyou](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3AJokerQyou)
+ Fix typo remvoeClass to removeClass [Aahmadsyamim](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Aahmadsyamim)
+ Updated weighted random upstream implementation [bouroo](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Abouroo)
# v3.1.0 31 Jul 2024
+ Updated log viewer with filter and auto refresh [#243](https://github.com/tobychui/zoraxy/issues/243)
+ Fixed csrf vulnerability [#267](https://github.com/tobychui/zoraxy/issues/267)
+ Fixed promox issue
+ Fixed status code bug in upstream log [#254](https://github.com/tobychui/zoraxy/issues/254)
+ Added host overwrite and hop-by-hop header remover
+ Added early renew days settings [#256](https://github.com/tobychui/zoraxy/issues/256)
+ Updated make file to force no CGO in cicd process
+ Fixed bug in updater
+ Fixed wildcard certificate renew bug [#249](https://github.com/tobychui/zoraxy/issues/249)
+ Added certificate download function [#227](https://github.com/tobychui/zoraxy/issues/227)
# v3.0.9 16 Jul 2024
+ Added certificate download [#227](https://github.com/tobychui/zoraxy/issues/227)

View File

@ -4,7 +4,6 @@
A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
### Features
- Simple to use interface with detail in-system instructions
@ -34,24 +33,26 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
- Basic single-admin management mode
- External permission management system for easy system integration
- SMTP config for password reset
- Dark Theme Mode
## Downloads
[Windows](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_windows_amd64.exe)
/[Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64)
/[Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64)
/ [Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64)
/ [Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64)
For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/)
For other systems or architectures, please see [Releases](https://github.com/tobychui/zoraxy/releases/latest/)
## Getting Started
[Installing Zoraxy Reverse Proxy: Your Gateway to Efficient Web Routing](https://geekscircuit.com/installing-zoraxy-reverse-proxy-your-gateway-to-efficient-web-routing/)
Thank you for the well written and easy to follow tutorial by Reddit users [itsvmn](https://www.reddit.com/user/itsvmn/)!
Thank you for the well written and easy to follow tutorial by Reddit user [itsvmn](https://www.reddit.com/user/itsvmn/)!
If you have no background in setting up reverse proxy or web routing, you should check this out before you start setting up your Zoraxy.
## Build from Source
Requires Go 1.22 or higher
Requires Go 1.23 or higher
```bash
git clone https://github.com/tobychui/zoraxy
@ -64,7 +65,7 @@ sudo ./zoraxy -port=:8000
## Usage
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructionss below for your desired deployment platform.
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructions below for your desired deployment platform.
### Standalone Mode
@ -92,7 +93,7 @@ The installation method is same as Linux. For other ARM SBCs, please refer to yo
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
### Start Paramters
### Start Parameters
```
Usage of zoraxy:
@ -100,30 +101,42 @@ Usage of zoraxy:
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
-cfgupgrade
Enable auto config upgrade if breaking change is detected (default true)
-db string
Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto")
-default_inbound_enabled
If web server is enabled by default (default true)
-default_inbound_port int
Default web server listening port (default 443)
-docker
Run Zoraxy in docker compatibility mode
-earlyrenew int
Number of days to early renew a soon expiring certificate (days) (default 30)
-fastgeoip
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
-log string
Log folder path (default "./log")
-mdns
Enable mDNS scanner and transponder (default true)
-mdnsname string
mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)
-noauth
Disable authentication for management interface
-plugin string
Plugin folder path (default "./plugins")
-port string
Management web interface listening port (default ":8000")
-sshlb
Allow loopback web ssh connection (DANGER)
-update_geoip
Download the latest GeoIP data and exit
-uuid string
sys.uuid file path (default "./sys.uuid")
-version
Show version of this server
-webfm
Enable web file manager for static web server root folder (default true)
-webroot string
Static web server root folder. Only allow chnage in start paramters (default "./www")
-ztauth string
ZeroTier authtoken for the local node
-ztport int
ZeroTier controller API port (default 9993)
Static web server root folder. Only allow change in start paramters (default "./www")
```
### External Permission Management Mode
@ -134,7 +147,8 @@ If you already have an upstream reverse proxy server in place with permission ma
./zoraxy -noauth=true
```
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
> [!WARNING]
> For security reasons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
## Screenshots
@ -154,7 +168,7 @@ This project also compatible with [ZeroTier](https://www.zerotier.com/). However
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::
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
@ -175,12 +189,22 @@ Web SSH currently only supports Linux based OSes. The following platforms are su
### Loopback Connection
Loopback web SSH connection, by default, is disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
Loopback web SSH connections, by default, are disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
```bash
./zoraxy -sshlb=true
```
## Community Maintained Sections
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)
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
Thank you so much for your contributions!
## Sponsor This Project
If you like the project and want to support us, please consider a donation. You can use the links below

View File

@ -1,11 +1,9 @@
FROM docker.io/golang:alpine AS build
## Build Zoraxy
FROM docker.io/golang:bookworm AS build-zoraxy
RUN mkdir -p /opt/zoraxy/source/ &&\
mkdir -p /opt/zoraxy/config/ &&\
mkdir -p /usr/local/bin/
RUN chmod -R 770 /opt/zoraxy/
# If you build it yourself, you will need to add the src directory into the docker directory.
COPY ./src/ /opt/zoraxy/source/
@ -13,36 +11,68 @@ WORKDIR /opt/zoraxy/source/
RUN go mod tidy &&\
go build -o /usr/local/bin/zoraxy &&\
rm -r /opt/zoraxy/source/
chmod 755 /usr/local/bin/zoraxy
RUN chmod 755 /usr/local/bin/zoraxy &&\
chmod +x /usr/local/bin/zoraxy
FROM docker.io/alpine:latest
## Build ZeroTier
FROM docker.io/golang:bookworm AS build-zerotier
RUN apk add --no-cache bash netcat-openbsd sudo
RUN mkdir -p /opt/zerotier/source/ &&\
mkdir -p /usr/local/bin/
COPY --from=build /usr/local/bin/zoraxy /usr/local/bin/zoraxy
COPY --from=build /opt/zoraxy/config/ /opt/zoraxy/config
WORKDIR /opt/zerotier/source/
VOLUME [ "/opt/zoraxy/config/" ]
RUN apt-get update -y &&\
apt-get install -y curl jq build-essential pkg-config clang cargo libssl-dev
RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
tar -xzvf ZeroTierOne.tar.gz &&\
cd ZeroTierOne-* &&\
make &&\
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
chmod 755 /usr/local/bin/zerotier-one
FROM docker.io/golang:bookworm
# If you build it yourself, you will need to add the example directory into the docker directory.
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
COPY --chmod=700 ./example/plugins/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
RUN apt-get update -y &&\
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates openssh-server
RUN mkdir -p /opt/zoraxy/plugin/
WORKDIR /opt/zoraxy/config/
ENV ZEROTIER="false"
ENV AUTORENEW="86400"
ENV CFGUPGRADE="true"
ENV DB="auto"
ENV DOCKER="true"
ENV EARLYRENEW="30"
ENV FASTGEOIP="false"
ENV MDNS="true"
ENV MDNSNAME="''"
ENV NOAUTH="false"
ENV PLUGIN="/opt/zoraxy/plugin/"
ENV PORT="8000"
ENV SSHLB="false"
ENV UPDATE_GEOIP="false"
ENV VERSION="false"
ENV WEBFM="true"
ENV WEBROOT="./www"
ENV ZTAUTH="''"
ENV ZTPORT="9993"
ENTRYPOINT "zoraxy" "-docker=true" "-autorenew=${AUTORENEW}" "-earlyrenew=${EARLYRENEW}" "-fastgeoip=${FASTGEOIP}" "-mdns=${MDNS}" "-mdnsname=${MDNSNAME}" "-noauth=${NOAUTH}" "-port=:${PORT}" "-sshlb=${SSHLB}" "-version=${VERSION}" "-webfm=${WEBFM}" "-webroot=${WEBROOT}" "-ztauth=${ZTAUTH}" "-ztport=${ZTPORT}"
VOLUME [ "/opt/zoraxy/config/" ]
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1

View File

@ -1,73 +1,115 @@
# [zoraxy](https://github.com/tobychui/zoraxy/) </br>
# Zoraxy Docker
[![Repo](https://img.shields.io/badge/Docker-Repo-007EC6?labelColor-555555&color-007EC6&logo=docker&logoColor=fff&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Version](https://img.shields.io/docker/v/zoraxydocker/zoraxy/latest?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Size](https://img.shields.io/docker/image-size/zoraxydocker/zoraxy/latest?sort=semver&labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Pulls](https://img.shields.io/docker/pulls/zoraxydocker/zoraxy?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
## Setup: </br>
Although not required, it is recommended to give Zoraxy a dedicated location on the host to mount the container. That way, the host/user can access them whenever needed. A volume will be created automatically within Docker if a location is not specified. </br>
## Usage
You may also need to portforward your 80/443 to allow http and https traffic. If you are accessing the interface from outside of the local network, you may also need to forward your management port. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. </br>
If you are attempting to access your service from outside your network, make sure to forward ports 80 and 443 to the Zoraxy host to allow web traffic. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. Read more about it from [whatismyip](https://www.whatismyip.com/port-forwarding/).
The examples below are not exactly how it should be set up, rather they give a general idea of usage.
In the examples below, make sure to update `/path/to/zoraxy/config/`. If a path is not provided, a Docker volume will be created at the location but it is recommended to store the data at a defined host location or a named Docker volume.
Once setup, access the webui at `http://<host-ip>:8000` to configure Zoraxy. Change the port in the URL if you changed the management port.
### Docker Run
### Using Docker run </br>
```
docker run -d --name (container name) -p 80:80 -p 443:443 -p (management external):(management internal) -v (path to storage directory):/opt/zoraxy/data/ -e (flag)="(value)" zoraxydocker/zoraxy:latest
docker run -d \
--name zoraxy \
--restart unless-stopped \
-p 80:80 \
-p 443:443 \
-p 8000:8000 \
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
-v /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/ \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /etc/localtime:/etc/localtime \
-e FASTGEOIP="true" \
zoraxydocker/zoraxy:latest
```
### Using Docker Compose </br>
### Docker Compose
```yml
services:
zoraxy-docker:
image: zoraxydocker/zoraxy:latest
container_name: (container name)
ports:
- 80:80
- 443:443
- (management external):(management internal)
volumes:
- (path to storage directory):/opt/zoraxy/config/
environment:
(flag): "(value)"
```
| Operator | Need | Details |
|:-|:-|:-|
| `-d` | Yes | will run the container in the background. |
| `--name (container name)` | No | Sets the name of the container to the following word. You can change this to whatever you want. |
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
| `-v (path to storage directory):/opt/zoraxy/config/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
| `-v /var/run/docker.sock:/var/run/docker.sock` | No | Used for autodiscovery. |
| `-v /etc/localtime:/etc/localtime` | No | Uses the hosts time in the container. |
| `-e (flag)="(value)"` | No | Arguments to run Zoraxy with. They are simply just capitalized Zoraxy flags. `-docker=true` is always set by default. See examples below. |
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that is published. |
> [!IMPORTANT]
> Docker usage of the port flag should not include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
## Examples: </br>
### Docker Run </br>
```
docker run -d --name zoraxy -p 80:80 -p 443:443 -p 8005:8005 -v /home/docker/Containers/Zoraxy:/opt/zoraxy/config/ -v /var/run/docker.sock:/var/run/docker.sock -v /etc/localtime:/etc/localtime -e PORT="8005" -e FASTGEOIP="true" zoraxydocker/zoraxy:latest
```
### Docker Compose </br>
```yml
services:
zoraxy-docker:
zoraxy:
image: zoraxydocker/zoraxy:latest
container_name: zoraxy
restart: unless-stopped
ports:
- 80:80
- 443:443
- 8005:8005
- 8000:8000
volumes:
- /home/docker/Containers/Zoraxy:/opt/zoraxy/config/
- /path/to/zoraxy/config/:/opt/zoraxy/config/
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
- /var/run/docker.sock:/var/run/docker.sock
- /etc/localtime:/etc/localtime
environment:
PORT: "8005"
FASTGEOIP: "true"
```
### Ports
| Port | Details |
|:-|:-|
| `80` | HTTP traffic. |
| `443` | HTTPS traffic. |
| `8000` | Management interface. Can be changed with the `PORT` env. |
### Volumes
| Volume | Details |
|:-|:-|
| `/opt/zoraxy/config/` | Zoraxy configuration. |
| `/opt/zoraxy/plugin/` | Zoraxy plugins. |
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
### Environment
Variables are the same as those in [Start Parameters](https://github.com/tobychui/zoraxy?tab=readme-ov-file#start-paramters).
| Variable | Default | Details |
|:-|:-|:-|
| `AUTORENEW` | `86400` (Integer) | ACME auto TLS/SSL certificate renew check interval. |
| `CFGUPGRADE` | `true` (Boolean) | Enable auto config upgrade if breaking change is detected. |
| `DB` | `auto` (String) | Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto"). |
| `DOCKER` | `true` (Boolean) | Run Zoraxy in docker compatibility mode. |
| `EARLYRENEW` | `30` (Integer) | Number of days to early renew a soon expiring certificate. |
| `FASTGEOIP` | `false` (Boolean) | Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices). |
| `MDNS` | `true` (Boolean) | Enable mDNS scanner and transponder. |
| `MDNSNAME` | `''` (String) | mDNS name, leave empty to use default (zoraxy_{node-uuid}.local). |
| `NOAUTH` | `false` (Boolean) | Disable authentication for management interface. |
| `PLUGIN` | `/opt/zoraxy/plugin/` (String) | Set the path for Zoraxy plugins. Only change this if you know what you are doing. |
| `PORT` | `8000` (Integer) | Management web interface listening port |
| `SSHLB` | `false` (Boolean) | Allow loopback web ssh connection (DANGER). |
| `UPDATE_GEOIP` | `false` (Boolean) | Download the latest GeoIP data and exit. |
| `VERSION` | `false` (Boolean) | Show version of this server. |
| `WEBFM` | `true` (Boolean) | Enable web file manager for static web server root folder. |
| `WEBROOT` | `./www` (String) | Static web server root folder. Only allow change in start parameters. |
| `ZEROTIER` | `false` (Boolean) | Enable ZeroTier functionality for GAN. |
> [!IMPORTANT]
> Contrary to the Zoraxy README, Docker usage of the port flag should NOT include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
### Plugins
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
Place your plugins inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location). Any plugins you have added will then be built and used on the next restart.
> [!IMPORTANT]
> Plugins are currently experimental.
### Building
To build the Docker image:
- Check out the repository/branch.
- Copy the Zoraxy `src/` and `example/` directory into the `docker/` (here) directory.
- Run the build command with `docker build -t zoraxy_build .`
- You can now use the image `zoraxy_build`
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.

19
docker/build_plugins.sh Normal file
View File

@ -0,0 +1,19 @@
#!/bin/bash
echo "Copying zoraxy_plugin to all mods..."
for dir in "$1"/*; do
if [ -d "$dir" ]; then
cp -r "/opt/zoraxy/zoraxy_plugin/" "$dir/mod/"
fi
done
echo "Running go mod tidy and go build for all directories..."
for dir in "$1"/*; do
if [ -d "$dir" ]; then
cd "$dir" || exit 1
go mod tidy
go build
cd "$1" || exit 1
fi
done

16
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,16 @@
services:
zoraxy:
image: zoraxydocker/zoraxy:latest
container_name: zoraxy
restart: unless-stopped
ports:
- 80:80
- 443:443
- 8000:8000
volumes:
- /path/to/zoraxy/config/:/opt/zoraxy/config/
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
- /var/run/docker.sock:/var/run/docker.sock
- /etc/localtime:/etc/localtime
environment:
FASTGEOIP: "true"

55
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env bash
cleanup() {
echo "Stop signal received. Shutting down..."
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
unlink /var/lib/zerotier-one/zerotier/
exit 0
}
trap cleanup SIGTERM SIGINT TERM INT
update-ca-certificates && echo "CA certificates updated."
zoraxy -update_geoip=true && echo "GeoIP data updated ."
echo "Building plugins..."
cd /opt/zoraxy/plugin/ || exit 1
build_plugins "$PWD"
echo "Plugins built."
cd /opt/zoraxy/config/ || exit 1
if [ "$ZEROTIER" = "true" ]; then
if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then
mkdir -p /opt/zoraxy/config/zerotier/
fi
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
zerotier-one -d &
zerotierpid=$!
echo "ZeroTier daemon started."
fi
echo "Starting Zoraxy..."
zoraxy \
-autorenew="$AUTORENEW" \
-cfgupgrade="$CFGUPGRADE" \
-db="$DB" \
-docker="$DOCKER" \
-earlyrenew="$EARLYRENEW" \
-fastgeoip="$FASTGEOIP" \
-mdns="$MDNS" \
-mdnsname="$MDNSNAME" \
-noauth="$NOAUTH" \
-plugin="$PLUGIN" \
-port=:"$PORT" \
-sshlb="$SSHLB" \
-update_geoip="$UPDATE_GEOIP" \
-version="$VERSION" \
-webfm="$WEBFM" \
-webroot="$WEBROOT" \
&
zoraxypid=$!
wait "$zoraxypid"
wait "$zerotierpid"

View File

@ -1 +1 @@
zoraxy.arozos.com
zoraxy.aroz.org

View File

@ -0,0 +1,451 @@
GNU Free Documentation License
Version 1.3, 3 November 2008
Copyright (C) 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
0. PREAMBLE
The purpose of this License is to make a manual, textbook, or other
functional and useful document "free" in the sense of freedom: to
assure everyone the effective freedom to copy and redistribute it,
with or without modifying it, either commercially or noncommercially.
Secondarily, this License preserves for the author and publisher a way
to get credit for their work, while not being considered responsible
for modifications made by others.
This License is a kind of "copyleft", which means that derivative
works of the document must themselves be free in the same sense. It
complements the GNU General Public License, which is a copyleft
license designed for free software.
We have designed this License in order to use it for manuals for free
software, because free software needs free documentation: a free
program should come with manuals providing the same freedoms that the
software does. But this License is not limited to software manuals;
it can be used for any textual work, regardless of subject matter or
whether it is published as a printed book. We recommend this License
principally for works whose purpose is instruction or reference.
1. APPLICABILITY AND DEFINITIONS
This License applies to any manual or other work, in any medium, that
contains a notice placed by the copyright holder saying it can be
distributed under the terms of this License. Such a notice grants a
world-wide, royalty-free license, unlimited in duration, to use that
work under the conditions stated herein. The "Document", below,
refers to any such manual or work. Any member of the public is a
licensee, and is addressed as "you". You accept the license if you
copy, modify or distribute the work in a way requiring permission
under copyright law.
A "Modified Version" of the Document means any work containing the
Document or a portion of it, either copied verbatim, or with
modifications and/or translated into another language.
A "Secondary Section" is a named appendix or a front-matter section of
the Document that deals exclusively with the relationship of the
publishers or authors of the Document to the Document's overall
subject (or to related matters) and contains nothing that could fall
directly within that overall subject. (Thus, if the Document is in
part a textbook of mathematics, a Secondary Section may not explain
any mathematics.) The relationship could be a matter of historical
connection with the subject or with related matters, or of legal,
commercial, philosophical, ethical or political position regarding
them.
The "Invariant Sections" are certain Secondary Sections whose titles
are designated, as being those of Invariant Sections, in the notice
that says that the Document is released under this License. If a
section does not fit the above definition of Secondary then it is not
allowed to be designated as Invariant. The Document may contain zero
Invariant Sections. If the Document does not identify any Invariant
Sections then there are none.
The "Cover Texts" are certain short passages of text that are listed,
as Front-Cover Texts or Back-Cover Texts, in the notice that says that
the Document is released under this License. A Front-Cover Text may
be at most 5 words, and a Back-Cover Text may be at most 25 words.
A "Transparent" copy of the Document means a machine-readable copy,
represented in a format whose specification is available to the
general public, that is suitable for revising the document
straightforwardly with generic text editors or (for images composed of
pixels) generic paint programs or (for drawings) some widely available
drawing editor, and that is suitable for input to text formatters or
for automatic translation to a variety of formats suitable for input
to text formatters. A copy made in an otherwise Transparent file
format whose markup, or absence of markup, has been arranged to thwart
or discourage subsequent modification by readers is not Transparent.
An image format is not Transparent if used for any substantial amount
of text. A copy that is not "Transparent" is called "Opaque".
Examples of suitable formats for Transparent copies include plain
ASCII without markup, Texinfo input format, LaTeX input format, SGML
or XML using a publicly available DTD, and standard-conforming simple
HTML, PostScript or PDF designed for human modification. Examples of
transparent image formats include PNG, XCF and JPG. Opaque formats
include proprietary formats that can be read and edited only by
proprietary word processors, SGML or XML for which the DTD and/or
processing tools are not generally available, and the
machine-generated HTML, PostScript or PDF produced by some word
processors for output purposes only.
The "Title Page" means, for a printed book, the title page itself,
plus such following pages as are needed to hold, legibly, the material
this License requires to appear in the title page. For works in
formats which do not have any title page as such, "Title Page" means
the text near the most prominent appearance of the work's title,
preceding the beginning of the body of the text.
The "publisher" means any person or entity that distributes copies of
the Document to the public.
A section "Entitled XYZ" means a named subunit of the Document whose
title either is precisely XYZ or contains XYZ in parentheses following
text that translates XYZ in another language. (Here XYZ stands for a
specific section name mentioned below, such as "Acknowledgements",
"Dedications", "Endorsements", or "History".) To "Preserve the Title"
of such a section when you modify the Document means that it remains a
section "Entitled XYZ" according to this definition.
The Document may include Warranty Disclaimers next to the notice which
states that this License applies to the Document. These Warranty
Disclaimers are considered to be included by reference in this
License, but only as regards disclaiming warranties: any other
implication that these Warranty Disclaimers may have is void and has
no effect on the meaning of this License.
2. VERBATIM COPYING
You may copy and distribute the Document in any medium, either
commercially or noncommercially, provided that this License, the
copyright notices, and the license notice saying this License applies
to the Document are reproduced in all copies, and that you add no
other conditions whatsoever to those of this License. You may not use
technical measures to obstruct or control the reading or further
copying of the copies you make or distribute. However, you may accept
compensation in exchange for copies. If you distribute a large enough
number of copies you must also follow the conditions in section 3.
You may also lend copies, under the same conditions stated above, and
you may publicly display copies.
3. COPYING IN QUANTITY
If you publish printed copies (or copies in media that commonly have
printed covers) of the Document, numbering more than 100, and the
Document's license notice requires Cover Texts, you must enclose the
copies in covers that carry, clearly and legibly, all these Cover
Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
the back cover. Both covers must also clearly and legibly identify
you as the publisher of these copies. The front cover must present
the full title with all words of the title equally prominent and
visible. You may add other material on the covers in addition.
Copying with changes limited to the covers, as long as they preserve
the title of the Document and satisfy these conditions, can be treated
as verbatim copying in other respects.
If the required texts for either cover are too voluminous to fit
legibly, you should put the first ones listed (as many as fit
reasonably) on the actual cover, and continue the rest onto adjacent
pages.
If you publish or distribute Opaque copies of the Document numbering
more than 100, you must either include a machine-readable Transparent
copy along with each Opaque copy, or state in or with each Opaque copy
a computer-network location from which the general network-using
public has access to download using public-standard network protocols
a complete Transparent copy of the Document, free of added material.
If you use the latter option, you must take reasonably prudent steps,
when you begin distribution of Opaque copies in quantity, to ensure
that this Transparent copy will remain thus accessible at the stated
location until at least one year after the last time you distribute an
Opaque copy (directly or through your agents or retailers) of that
edition to the public.
It is requested, but not required, that you contact the authors of the
Document well before redistributing any large number of copies, to
give them a chance to provide you with an updated version of the
Document.
4. MODIFICATIONS
You may copy and distribute a Modified Version of the Document under
the conditions of sections 2 and 3 above, provided that you release
the Modified Version under precisely this License, with the Modified
Version filling the role of the Document, thus licensing distribution
and modification of the Modified Version to whoever possesses a copy
of it. In addition, you must do these things in the Modified Version:
A. Use in the Title Page (and on the covers, if any) a title distinct
from that of the Document, and from those of previous versions
(which should, if there were any, be listed in the History section
of the Document). You may use the same title as a previous version
if the original publisher of that version gives permission.
B. List on the Title Page, as authors, one or more persons or entities
responsible for authorship of the modifications in the Modified
Version, together with at least five of the principal authors of the
Document (all of its principal authors, if it has fewer than five),
unless they release you from this requirement.
C. State on the Title page the name of the publisher of the
Modified Version, as the publisher.
D. Preserve all the copyright notices of the Document.
E. Add an appropriate copyright notice for your modifications
adjacent to the other copyright notices.
F. Include, immediately after the copyright notices, a license notice
giving the public permission to use the Modified Version under the
terms of this License, in the form shown in the Addendum below.
G. Preserve in that license notice the full lists of Invariant Sections
and required Cover Texts given in the Document's license notice.
H. Include an unaltered copy of this License.
I. Preserve the section Entitled "History", Preserve its Title, and add
to it an item stating at least the title, year, new authors, and
publisher of the Modified Version as given on the Title Page. If
there is no section Entitled "History" in the Document, create one
stating the title, year, authors, and publisher of the Document as
given on its Title Page, then add an item describing the Modified
Version as stated in the previous sentence.
J. Preserve the network location, if any, given in the Document for
public access to a Transparent copy of the Document, and likewise
the network locations given in the Document for previous versions
it was based on. These may be placed in the "History" section.
You may omit a network location for a work that was published at
least four years before the Document itself, or if the original
publisher of the version it refers to gives permission.
K. For any section Entitled "Acknowledgements" or "Dedications",
Preserve the Title of the section, and preserve in the section all
the substance and tone of each of the contributor acknowledgements
and/or dedications given therein.
L. Preserve all the Invariant Sections of the Document,
unaltered in their text and in their titles. Section numbers
or the equivalent are not considered part of the section titles.
M. Delete any section Entitled "Endorsements". Such a section
may not be included in the Modified Version.
N. Do not retitle any existing section to be Entitled "Endorsements"
or to conflict in title with any Invariant Section.
O. Preserve any Warranty Disclaimers.
If the Modified Version includes new front-matter sections or
appendices that qualify as Secondary Sections and contain no material
copied from the Document, you may at your option designate some or all
of these sections as invariant. To do this, add their titles to the
list of Invariant Sections in the Modified Version's license notice.
These titles must be distinct from any other section titles.
You may add a section Entitled "Endorsements", provided it contains
nothing but endorsements of your Modified Version by various
parties--for example, statements of peer review or that the text has
been approved by an organization as the authoritative definition of a
standard.
You may add a passage of up to five words as a Front-Cover Text, and a
passage of up to 25 words as a Back-Cover Text, to the end of the list
of Cover Texts in the Modified Version. Only one passage of
Front-Cover Text and one of Back-Cover Text may be added by (or
through arrangements made by) any one entity. If the Document already
includes a cover text for the same cover, previously added by you or
by arrangement made by the same entity you are acting on behalf of,
you may not add another; but you may replace the old one, on explicit
permission from the previous publisher that added the old one.
The author(s) and publisher(s) of the Document do not by this License
give permission to use their names for publicity for or to assert or
imply endorsement of any Modified Version.
5. COMBINING DOCUMENTS
You may combine the Document with other documents released under this
License, under the terms defined in section 4 above for modified
versions, provided that you include in the combination all of the
Invariant Sections of all of the original documents, unmodified, and
list them all as Invariant Sections of your combined work in its
license notice, and that you preserve all their Warranty Disclaimers.
The combined work need only contain one copy of this License, and
multiple identical Invariant Sections may be replaced with a single
copy. If there are multiple Invariant Sections with the same name but
different contents, make the title of each such section unique by
adding at the end of it, in parentheses, the name of the original
author or publisher of that section if known, or else a unique number.
Make the same adjustment to the section titles in the list of
Invariant Sections in the license notice of the combined work.
In the combination, you must combine any sections Entitled "History"
in the various original documents, forming one section Entitled
"History"; likewise combine any sections Entitled "Acknowledgements",
and any sections Entitled "Dedications". You must delete all sections
Entitled "Endorsements".
6. COLLECTIONS OF DOCUMENTS
You may make a collection consisting of the Document and other
documents released under this License, and replace the individual
copies of this License in the various documents with a single copy
that is included in the collection, provided that you follow the rules
of this License for verbatim copying of each of the documents in all
other respects.
You may extract a single document from such a collection, and
distribute it individually under this License, provided you insert a
copy of this License into the extracted document, and follow this
License in all other respects regarding verbatim copying of that
document.
7. AGGREGATION WITH INDEPENDENT WORKS
A compilation of the Document or its derivatives with other separate
and independent documents or works, in or on a volume of a storage or
distribution medium, is called an "aggregate" if the copyright
resulting from the compilation is not used to limit the legal rights
of the compilation's users beyond what the individual works permit.
When the Document is included in an aggregate, this License does not
apply to the other works in the aggregate which are not themselves
derivative works of the Document.
If the Cover Text requirement of section 3 is applicable to these
copies of the Document, then if the Document is less than one half of
the entire aggregate, the Document's Cover Texts may be placed on
covers that bracket the Document within the aggregate, or the
electronic equivalent of covers if the Document is in electronic form.
Otherwise they must appear on printed covers that bracket the whole
aggregate.
8. TRANSLATION
Translation is considered a kind of modification, so you may
distribute translations of the Document under the terms of section 4.
Replacing Invariant Sections with translations requires special
permission from their copyright holders, but you may include
translations of some or all Invariant Sections in addition to the
original versions of these Invariant Sections. You may include a
translation of this License, and all the license notices in the
Document, and any Warranty Disclaimers, provided that you also include
the original English version of this License and the original versions
of those notices and disclaimers. In case of a disagreement between
the translation and the original version of this License or a notice
or disclaimer, the original version will prevail.
If a section in the Document is Entitled "Acknowledgements",
"Dedications", or "History", the requirement (section 4) to Preserve
its Title (section 1) will typically require changing the actual
title.
9. TERMINATION
You may not copy, modify, sublicense, or distribute the Document
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense, or distribute it is void, and
will automatically terminate your rights under this License.
However, if you cease all violation of this License, then your license
from a particular copyright holder is reinstated (a) provisionally,
unless and until the copyright holder explicitly and finally
terminates your license, and (b) permanently, if the copyright holder
fails to notify you of the violation by some reasonable means prior to
60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, receipt of a copy of some or all of the same material does
not give you any rights to use it.
10. FUTURE REVISIONS OF THIS LICENSE
The Free Software Foundation may publish new, revised versions of the
GNU Free Documentation License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in
detail to address new problems or concerns. See
https://www.gnu.org/licenses/.
Each version of the License is given a distinguishing version number.
If the Document specifies that a particular numbered version of this
License "or any later version" applies to it, you have the option of
following the terms and conditions either of that specified version or
of any later version that has been published (not as a draft) by the
Free Software Foundation. If the Document does not specify a version
number of this License, you may choose any version ever published (not
as a draft) by the Free Software Foundation. If the Document
specifies that a proxy can decide which future versions of this
License can be used, that proxy's public statement of acceptance of a
version permanently authorizes you to choose that version for the
Document.
11. RELICENSING
"Massive Multiauthor Collaboration Site" (or "MMC Site") means any
World Wide Web server that publishes copyrightable works and also
provides prominent facilities for anybody to edit those works. A
public wiki that anybody can edit is an example of such a server. A
"Massive Multiauthor Collaboration" (or "MMC") contained in the site
means any set of copyrightable works thus published on the MMC site.
"CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0
license published by Creative Commons Corporation, a not-for-profit
corporation with a principal place of business in San Francisco,
California, as well as future copyleft versions of that license
published by that same organization.
"Incorporate" means to publish or republish a Document, in whole or in
part, as part of another Document.
An MMC is "eligible for relicensing" if it is licensed under this
License, and if all works that were first published under this License
somewhere other than this MMC, and subsequently incorporated in whole or
in part into the MMC, (1) had no cover texts or invariant sections, and
(2) were thus incorporated prior to November 1, 2008.
The operator of an MMC Site may republish an MMC contained in the site
under CC-BY-SA on the same site at any time before August 1, 2009,
provided the MMC is eligible for relicensing.
ADDENDUM: How to use this License for your documents
To use this License in a document you have written, include a copy of
the License in the document and put the following copyright and
license notices just after the title page:
Copyright (c) YEAR YOUR NAME.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
replace the "with...Texts." line with this:
with the Invariant Sections being LIST THEIR TITLES, with the
Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
If you have Invariant Sections without Cover Texts, or some other
combination of the three, merge those two alternatives to suit the
situation.
If your document contains nontrivial examples of program code, we
recommend releasing these examples in parallel under your choice of
free software license, such as the GNU General Public License,
to permit their use in free software.

1
docs/dom-i18n.min.js vendored Normal file
View File

@ -0,0 +1 @@
!function(a,b){"use strict";"function"==typeof define&&define.amd?define([],function(){return a.domI18n=b()}):"object"==typeof exports?module.exports=b():a.domI18n=b()}(this,function(){"use strict";return function(a){function b(a){return a||(a=window.navigator.languages?window.navigator.languages[0]:window.navigator.language||window.navigator.userLanguage),-1===q.indexOf(a)&&(r&&console.warn(a+" is not available on the list of languages provided"),a=a.indexOf("-")?a.split("-")[0]:a),-1===q.indexOf(a)&&(r&&console.error(a+" is not compatible with any language provided"),a=p),a}function c(a){v=b(a),l()}function d(){u={}}function e(a){var b=a.getAttribute("data-dom-i18n-id");return b&&u&&u[b]}function f(a,b){var c="i18n"+Date.now()+1e3*Math.random();a.setAttribute("data-dom-i18n-id",c),u[c]=b}function g(a){return u&&u[a.getAttribute("data-dom-i18n-id")]}function h(a,b){var c={},d=a.firstElementChild,e=!d&&a[b].split(o);return q.forEach(function(b,f){var g;d?(g=a.children[f],g&&g.cloneNode&&(c[b]=g.cloneNode(!0))):(g=e[f],g&&(c[b]=String(g)))}),c}function i(a){var b,c,d=a.getAttribute(t),i=null!==a.getAttribute(s),k=d?d:"textContent";!i&&e(a)?b=g(a):(b=h(a,k),i||f(a,b)),c=b[v],"string"==typeof c?a[k]=c:"object"==typeof c&&j(a,c)}function j(a,b){k(a),a.appendChild(b)}function k(a){for(;a.lastChild;)a.removeChild(a.lastChild)}function l(){for(var a="string"==typeof n||n instanceof String?m.querySelectorAll(n):n,b=0;b<a.length;++b)i(a[b])}a=a||{};var m=a.rootElement||window.document,n=a.selector||"[data-translatable]",o=a.separator||" // ",p=a.defaultLanguage||"en",q=a.languages||["en"],r=void 0!==a.enableLog?a.enableLog:!0,s="data-no-cache",t="data-translatable-attr",u={},v=b(a.currentLanguage);return l(n),{changeLanguage:c,clearCachedElements:d}}});

BIN
docs/img/logo_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
docs/img/preview-mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
docs/img/preview-mobile.psd Normal file

Binary file not shown.

BIN
docs/img/preview-pc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@ -1,128 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="Reverse Proxy, Cluster, Gateway, Go, Homelab, Network Tools" name="keywords">
<meta content="A reverse proxy server and cluster network gateway for noobs" name="description">
<meta content="Reverse Proxy, Open Source, Aroz, Go, OS, NAS, Cloud" name="keywords">
<meta content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox" name="description">
<meta name="author" content="tobychui">
<!-- HTML Meta Tags -->
<title>Reverse Proxy Server | Zoraxy</title>
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
<title>Homelab Gateway | Zoraxy</title>
<meta name="description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
<!-- Facebook Meta Tags -->
<meta property="og:url" content="https://zoraxy.arozos.com/">
<meta property="og:url" content="https://zoraxy.aroz.org/">
<meta property="og:type" content="website">
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta property="og:image" content="https://zoraxy.arozos.com/img/og.png">
<meta property="og:title" content="Hello Zoraxy">
<meta property="og:description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="arozos.com">
<meta property="twitter:url" content="https://zoraxy.arozos.com/">
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta name="twitter:image" content="https://zoraxy.arozos.com/img/og.png">
<meta property="twitter:domain" content="os.aroz.org">
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
<meta name="twitter:title" content="Hello Zoraxy">
<meta name="twitter:description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- JavaScript Libs-->
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="dom-i18n.min.js"></script>
<link href="main.css" rel="stylesheet">
<!-- Css stuffs-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js" integrity="sha512-gnoBksrDbaMnlE0rhhkcx3iwzvgBGz6mOEj4/Y5ZY09n55dYddx6+WYc72A55qEesV8VX2iMomteIwobeGK1BQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.css" integrity="sha512-3quBdRGJyLy79hzhDDcBzANW+mVqPctrGCfIPosHQtMKb3rKsCxfyslzwlz2wj1dT8A7UX+sEvDjaUv+WExQrA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Favicons -->
<link href="favicon.png" rel="icon">
<link href="img/apple-touch-icon.png" rel="apple-touch-icon">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@100;300;400;600;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=M+PLUS+1p&family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100;300;400&display=swap" rel="stylesheet">
<!-- Main Stylesheet File -->
<link href="style.css" rel="stylesheet">
<script
src="https://code.jquery.com/jquery-3.7.0.min.js"
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js" integrity="sha512-5cguXwRllb+6bcc2pogwIeQmQPXEzn2ddsqAexIBhh7FO1z5Hkek1J9mrK2+rmZCTU6b6pERxI7acnp1MpAg4Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css" integrity="sha512-n//BDM4vMPvyca4bJjZPDh7hlqsQ7hqbP9RH18GF2hTXBY5amBwM2501M0GPiwCU/v9Tor2m13GOTFjk00tkQA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
p,a,div,span,h1,h2,h3,h4,h5,h6{
font-family: 'Source Sans Pro', sans-serif !important;
color: #404040;
}
</style>
<!-- AOS.js-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js" integrity="sha512-A7AYk1fGKX6S2SsHywmPkrnzTZHrgiVT7GcQkLGDe2ev0aWb8zejytzS8wjo7PGEXKqJOrjQ4oORtnimIRZBtw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css" integrity="sha512-1cK78a1o+ht2JcaW6g8OXYwqpev9+6GqOkz9xmBN9iUUhIndKtxwILGWYOSibOKjLsEdjyjZvYDq/cZwNeak0w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>
<div class="main section">
<div class="left-menu">
<div class="iconWrapper">
<a href="index.html"><img class="ui fluid image" src="img/icon.png"></a>
</div>
<a href="#home" class="menu-item active" align="center">
<img src="img/icons/home.svg">
</a>
<a href="#features" class="menu-item" align="center">
<img src="img/icons/awesome.svg">
</a>
<a href="#screenshots" class="menu-item" align="center">
<img src="img/icons/screenshots.svg">
</a>
<a href="#plugins" class="menu-item" align="center">
<img src="img/icons/plugin.svg">
</a>
<a href="#source" class="menu-item" align="center">
<img src="img/icons/code.svg">
</a>
</div>
<div class="right-content">
<!-- Hero Banner Section -->
<div class="headbanner"></div>
<div id="home" class="herotext">
<div class="ui basic segment">
<div class="bannerHeaderWrapper">
<h1 class="bannerHeader">Zoraxy</h1>
<div class="ui divider"></div><br>
<p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
<div id="backToTopBtn" class="ui big icon button" onclick="backToTop();">
<i class="ui arrow up icon"></i>
</div>
<button id="rwdmenubtn" class="ui black big icon button"><i class="ui bars icon"></i></button>
<div id="mainmenu" class="ui segment">
<div class="ui container">
<div class="ui small stackable secondary menu">
<div class="item">
<img class="ui tiny image" src="img/logo.png">
</div>
<a class="item" href="#mainmenu" i18n>
Home // 主頁 // Startseite
</a>
<a class="item" href="#about" i18n>
About Zoraxy // 關於 Zoraxy // Über Zoraxy
</a>
<a class="item" href="#features" i18n>
Screenshots // 系統截圖 // Bildschirmfotos
</a>
<a class="item" href="#techspec" i18n>
Videos // 介紹影片 // Videos
</a>
<a class="item" href="#download" i18n>
Download // 下載 // Herunterladen
</a>
<a class="item" href="#learnmore" i18n>
Learn More // 了解更多 // Mehr erfahren
</a>
<a class="right floated item">
<div class="ui small selection dropdown">
<input type="hidden" id="language">
<div class="default text" style="color: #6cacff;"><i class="language icon"></i> Default</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" data-value="en">English</div>
<div class="item" data-value="zh">中文(正體)</div>
<div class="item" data-value="de">Deutsch</div>
</div>
</div>
</a>
</div>
<br><br>
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
<br><br>
<table class="ui very basic collapsing unstackable celled table">
<thead>
<tr><th colspan="2">Quick Access</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<h4 class="ui image header">
<i class="ui download icon"></i>
<div class="content">
Download
<div class="sub header">Prebuild Binary
</div>
</div>
</h4></td>
<td>
<a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Open <i class="ui external icon"></i></a>
</td>
</tr>
<tr>
<td>
<h4 class="ui image header">
<i class="ui github icon"></i>
<div class="content">
Github
<div class="sub header">Source Code
</div>
</div>
</h4></td>
<td>
<a href="https://github.com/tobychui/zoraxy" target="_blank">Open <i class="ui external icon"></i></a>
</td>
</tr>
</table>
</div>
<div id="wavesWrapper">
</div>
</div>
<div class="messageBanner">
<div class="ui text container">
<p i18n>This site is currently under development. Some information might not be ready.
// 本網站目前仍在開發中,部分資訊可能尚未準備好。
// Diese Seite ist in Entwicklung. Einige Informationen sind möglicherweise nicht verfügbar.
</p>
</div>
</div>
<div id="slideshowBanner">
<div class="title">
<h1 i18n>Zoraxy</h1>
<div class="ui divider" style="border-top: 1px solid rgba(255,255,255,0.5); "></div>
<p i18n>The ultimate homelab networking toolbox for self-hosted services
// 簡化自家伺服器部署之事,初學者居家網絡必備良器
// Das ultimative Homelab-Netzwerk-Toolbox für selbstgehostete Dienste
</p>
<a href="https://github.com/tobychui/zoraxy/releases" class="ui basic white button" target="_blank"><i style="color:white;" class="ui download icon"></i><span i18n>Download // 立即下載 // Herunterladen </span></a>
<a href="https://github.com/tobychui/zoraxy" class="ui basic white button" target="_blank"><i style="color: white;" class="ui code icon"></i><span i18n>Source Code // 查看原始碼 // Quellcode</span></a>
</div>
<div id="wavesWrapper">
<!-- CSS waves-->
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
@ -137,250 +130,470 @@
</g>
</svg>
</div>
</div>
<!-- Features -->
<div id="features" class="section">
<div class="ui container">
<div class="ui basic segment">
<h1 class="ui header">
<img class="ui small image" src="img/icons/awesome.svg">
<div class="content">
Features
<div class="sub header">Highlighting a few important features of Zoraxy</div>
</div>
</h1>
<br>
<div class="ui stackable grid featureList">
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/proxy.svg">
<div class="content">
Reverse Proxy
</div>
</h3>
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/redirect.svg">
<div class="content">
Redirection
</div>
</h3>
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/blacklist.svg">
<div class="content">
Geo-IP & Blacklist
</div>
</h3>
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/gan.svg">
<div class="content">
Global Area Network
</div>
</h3>
<p>ZeroTier controller integrated GAN. Enable unlimited nodes in your network with a few clicks.</p>
</div>
<!-- Row 2-->
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/terminal.svg">
<div class="content">
Web SSH
</div>
</h3>
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/stats.svg">
<div class="content">
Real Time Statistics
</div>
</h3>
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/scan.svg">
<div class="content">
Scanner & Utilities
</div>
</h3>
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/code.svg">
<div class="content">
Open Source
</div>
</h3>
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
</div>
</div>
</div>
<br><br><br><br>
<!-- About ArozOS-->
<div id="about" class="ui text container">
<div class="ui stackable grid" data-aos="fade-up">
<div class="six wide column" align="right">
<img class="ui medium image" src="img/preview-pc.png">
</div>
</div>
</div>
<!-- Screenshots -->
<div id="screenshots" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/screenshots.svg">
<div class="content">
Screenshots
<div class="sub header">A quick overview of the UI designs</div>
</div>
</h1>
<div class="ui three column stackable grid">
<div class="column">
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
</div>
</div>
</div>
</div>
<!-- Plugin Developments -->
<div id="plugins" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/plugin.svg">
<div class="content">
Plugins
<div class="sub header">Add custom routing rules via simple scripts</div>
</div>
</h1>
<div style="width: 100%; text-align: center;">
<br>
<p>Documentation work in progress</p>
</div>
</div>
</div>
<!-- Source code -->
<div id="source" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/code.svg">
<div class="content">
Source Code
<div class="sub header">Feel free to give us a ⭐ star ⭐.</div>
</div>
</h1>
<br>
<div class="ui two column stackable grid">
<div class="column">
<h3 class="ui header">
<i class="grey github icon"></i>
<div class="content" style="text-align: left;">
<a href="https://github.com/tobychui/zoraxy">
Github
<div class="sub header">https://github.com/tobychui/zoraxy</div>
</a>
<div class="ten wide column">
<div class="about-text-wrapper">
<p class="about-title"><b i18n>Reverse Proxy // 反向代理 // Reverse-Proxy</b></p>
<p><span i18n>Easy setups with dynamic updates // 讓你想不到般簡單易用、迅速設定、動態更新 // Einfache Einrichtung mit dynamischen Updates</span></p>
<p i18n>Access your reverse proxy and self-hosted services from any computer with a browser, anytime, anywhere.
// 透過瀏覽器,隨時隨地在任何裝置上存取您的反向代理及自家伺服器服務。
// Greifen Sie jederzeit und überall von jedem Gerät aus auf Ihren Reverse-Proxy und selbst gehostete
</p>
<div class="ui list">
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Simple setups with web UI
// 透過網頁介面簡單設定即可使用
// Einfache Einrichtung mit Web-UI
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Change settings on the fly without restarting
// 即時更改設定,無需重新啟動
// Einstellungen ohne Neustart ändern
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
One of the best reverse proxy manager for beginners
// 可能是最適合初學者的反向代理管理器之一
// Einer der besten Reverse-Proxy-Manager für Anfänger
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Easily install plugins and edit configurations
// 輕鬆安裝插件並編輯設定
// Plugins einfach installieren und Konfigurationen bearbeiten
</div>
</div>
</div>
</h3>
</div>
<div class="column">
<h3 class="ui header">
<i class="blue mail icon"></i>
<div class="content" style="text-align: left;">
<a href="mailto:toby@imuslab.com">
Email Contact
<div class="sub header">toby@imuslab.com</div>
</a>
</div>
</h3>
</div>
</div>
</div>
</div>
</div>
<br><br>
<div class="ui container">
<p style="color: #3a3a3a">CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
<div class="ui stackable grid" data-aos="fade-up">
<div class="six wide column" align="right">
<img class="ui medium image" src="img/preview-mobile.png">
</div>
<div class="ten wide column">
<div class="about-text-wrapper">
<p class="about-title"><b i18n>Real-time Analytics // 即時流量分析 // Echtzeit-Analysen</b></p>
<p><span i18n>Dynamic statistic and access control // 動態流量數據、權限與路由設定 // Dynamische Statistik und Zugriffskontrolle</span></p>
<p i18n>Provide real time statistical overview, take advantage of the real time traffic and situations to make better decisions.
// 提供即時統計概覽,利用即時流量和情況做出更好的決策。
// Bietet eine Echtzeit-Übersicht über die Statistiken, um bessere Entscheidungen zu treffen.
</p>
<div class="ui list">
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Real time visitor statistic
// 即時訪客統計概覽
// Echtzeit-Besucherstatistik
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
Instant network utilitization overview
// 即時網路使用率概覽
// Sofortige Netzwerkübersicht
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
No-reload access control and settings
// 即時生效存取控制和設定
// Zugriffskontrolle und Einstellungen ohne Neustart
</div>
</div>
<div class="item">
<i class="caret right icon"></i>
<div class="content" i18n>
One-click setting change with no downtime
// 一鍵設定更改,無需停機
// Einstellungsänderung mit einem Klick ohne Ausfallzeiten
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<br><br><br><br>
<!-- Features -->
<div class="ui divider"></div>
<div id="features" class="ui container">
<div class="centered title">
<h1 i18n>Screenshots
// 系統截圖
// Bildschirmfotos
</h1>
</div>
<div class="ui three column stackable grid">
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/1.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/2.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/3.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/4.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/5.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/6.png">
</div>
<!-- <div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/7.png">
</div> -->
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/8.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/9.png">
</div>
<div class="column">
<img class="ui fluid image screenshot" src="img/screenshots/10.png">
</div>
</div>
<br><br><br>
</div>
</div>
<br>
<!-- Spec -->
<div id="techspec" class="blackbanner">
<br><br>
<div class="centered title">
<h1 style="font-weight: 600;" i18n>
Review Videos
// 介紹影片
// Videos
</h1>
</div>
<div>
<div class="videoScrollBar">
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/5-lps8DC6_Y?si=rkfePn9kiYKCvYUZ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/49xQYLpmedE?si=fgba2iK55s1760Xr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/I_F97he5F2A?si=qKEXwDcjkX1nPejq" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/FNU08-ufByM?si=I2hq9vsapeXB2Oqb" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
</div>
</div>
<br><br><br>
</div>
<!-- Download -->
<div id="download" class="ui text container">
<br><br>
<div class="centered title">
<h1 i18n>
Download
// 下載
// Herunterladen
</h1>
</div>
<div class="downloadTabWrapper">
<div class="ui top attached fluid stackable tabular menu">
<a class="active item" data-tab="linux"><i class="grey linux icon"></i> Linux</a>
<a class="item" data-tab="windows"><i class="blue windows icon"></i> Windows</a>
<a class="item" data-tab="rpi"><i class="red raspberry pi icon"></i><span i18n>SBCs // ARM 開發板 // SBCs</span></a>
<a class="item" data-tab="build"><i class="code icon"></i> <span i18n>Build from source // 從原始碼建置 // Aus dem Quellcode erstellen</span> </a>
</div>
<div class="ui bottom attached active tab segment" data-tab="linux">
<p i18n>
Install with command line
// 使用 CLI 下載並執行發行版本
// Installieren Sie mit der Befehlszeile
</p>
<div class="ui black message">
<code>
wget https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64<br>
chmod +x ./zoraxy_linux_amd64<br>
sudo ./zoraxy_linux_amd64
</code>
</div>
<br>
<p i18n>
Install with precompiled binary
// 下載發行版本
// Installieren Sie mit vorkompilierten Binärdateien
</p>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_amd64");'>
<i class="black linux icon"></i>
<span i18n>Download x64
// 下載 64位元 執行檔
// Herunterladen x64
</span>
</button>
<span style="font-size: 1.2em; font-weight: 600; margin-right: 0.4em">OR</span>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_386");'>
<i class="black linux icon"></i>
<span i18n>Download x32
// 下載 32位元 執行檔
// Herunterladen x32
</span>
</button>
</div>
<div class="ui bottom attached tab segment" data-tab="windows">
<p i18n>
Install with precompiled binary
// 下載發行版本
// Installieren Sie mit vorkompilierten Binärdateien
</p>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_windows_amd64.exe");'>
<i class="blue windows icon"></i>
<span i18n>
Download Zoraxy for Windows
// 下載 Windows 版 Zoraxy
// Zoraxy für Windows herunterladen
</span>
</button>
<br><br>
</div>
<div class="ui bottom attached tab segment" data-tab="rpi">
<p i18n>Install with command line (armv6-7, arm64, x86)
// 使用 CLI 下載並執行 armv6-7, arm64, x86
// Installieren Sie mit der Befehlszeile (armv6-7, arm64, x86)
</p>
<div class="ui black message">
<code>
# Check your CPU architecture<br>
uname -m <br>
<br>
# For arm64 (aarch64) CPU<br>
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64<br>
<br>
# For armv6 (armv6l) / armv7 (armv7l) CPU<br>
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm<br>
<br>
# For RISC-V (riscv64) CPU<br>
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_riscv64<br>
<br>
chmod +x ./zoraxy<br>
sudo ./zoraxy <br>
</code>
</div>
<br>
<p i18n>Install with precompiled binary
// 下載發行版本
// Installieren Sie mit vorkompilierten Binärdateien
</p>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_arm");'><i class="black download icon"></i> <span i18n></span>arm (v6, v7)</button>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_arm64");'><i class="black download icon"></i> <span i18n></span>arm64</button>
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_riscv64");'><i class="grey download icon"></i> <span i18n></span>riscv64</button>
</div>
<div class="ui bottom attached tab segment" data-tab="build">
<p i18n>Require Go (Golang) compiler. Details build from source instruction can be found on Zoraxy Github README file.
// 需要 Go (Go 語言)編譯器。建置詳情可以在 Zoraxy Github README 檔案中找到。
// Erfordert den Go (Golang) Compiler. Detaillierte Anweisungen zum Erstellen aus dem Quellcode finden Sie in der Zoraxy Github README-Datei.
</p>
<div class="ui black message">
<code>
git clone https://github.com/tobychui/zoraxy<br>
cd ./zoraxy/src/<br>
go mod tidy<br>
go build<br>
sudo ./zoraxy <br>
</code>
</div>
</div>
</div>
<p>
<span i18n>After Zoraxy is started, navigate to
// 當 Zoraxy 執行檔 / 服務啟動後,使用瀏覽器開啟
// Nachdem Zoraxy gestartet wurde, navigieren Sie zu
</span>
<a href="http://localhost:8000" target="_blank">http://localhost:8000</a>
<span i18n>to continue account and system setup.
// 以繼續帳戶和系統設定。
// um die Konto- und Systemeinrichtung fortzusetzen.
</span>
</p>
<br><br>
</div>
<!-- Learn More -->
<div class="ui divider"></div>
<div id="learnmore" class="ui text container">
<div class="centered title" style="margin-bottom: 0px;">
<h1 i18n>Learn More
// 了解更多
// Mehr erfahren
</h1>
<p i18n>If you like this project, please feel free to give us a ⭐ star ⭐.
// 如果您喜歡這個開源專案,歡迎來給我們一顆 ⭐星星⭐ 喔!!
// Wenn Ihnen dieses Projekt gefällt, geben Sie uns bitte einen ⭐ Stern ⭐.
</p>
</div>
<div class="ui basic segment linkicons">
<div class="ui big breadcrumb">
<a class="section externallink" href="https://github.com/tobychui/zoraxy" target="_blank">
<div class="ui icon header">
<i class="black github icon"></i>
<div class="content" i18n>
Github
// 源碼
// Quellcode
</div>
</div>
</a>
<i class="divider"> </i>
<a class="section externallink" href="" target="_blank">
<div class="ui icon header">
<i class="green code icon"></i>
<div class="content" i18n>
Plugin Devs
// 插件開發
// Plugin-Entwickler
</div>
</div>
</a>
<i class=" divider"> </i>
<a class="section externallink" href="mailto:toby@imuslab.com" target="_blank">
<div class="ui icon header">
<i class="yellow mail icon"></i>
<div class="content" i18n>
Email
// 電子郵件
// E-Mail
</div>
</div>
</a>
<i class=" divider"> </i>
<a class="section externallink" href="https://t.me/ArOZBeta" target="_blank">
<div class="ui icon header">
<i class="blue telegram icon"></i>
<div class="content">
Telegram
</div>
</div>
</a>
</div>
</div>
<br><br><br><br>
</div>
<!-- Footer -->
<div id="footer">
<div class="ui container">
<br><br>
<div class="ui stackable grid" style="height: 100%;">
<div class="six wide column" style="height: 100%;">
<a href="https://zoraxy.aroz.org"><img src="img/logo_white.png" class="ui small image"></a>
<p><span style="font-weight: 300;">The Zoraxy Project</span><br>
© Toby Chui</p>
<div class="bottom-attach">
<br><br>
<div class="ui breadcrumb" style="margin-top: 0.4em;">
<div class="section" i18n><a style="color: white;" href="https://zoraxy.aroz.org" target="_blank">zoraxy.aroz.org</a></div>
<div class="divider"> / </div>
<div class="section">2018 - <span class="year">now</span></div>
</div>
</div>
</div>
<div class="three wide column">
<div class="ui list">
<div class="item title" i18n>Developer Tools
// 開發者工具
// Entwicklerwerkzeuge
</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>
</div>
<div class="three wide column">
<div class="ui list">
<div class="item title" i18n>Project Spin-offs
// 衍生開源計劃
// Projekt-Ableger
</div>
<div class="item"><a href="https://os.aroz.org" target="_blank">ArozOS</a></div>
</div>
</div>
<div class="three wide column">
<div class="ui list">
<div class="item title" i18n>Related Links
// 相關連接
// Verwandte Links
</div>
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki/Getting-Started" target="_blank" i18n>Getting Started</a></div>
<div class="item"><a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Zoraxy Release</a></div>
<div class="item"><a href="https://hub.docker.com/r/zoraxydocker/zoraxy" target="_blank">Zoraxy Docker</a></div>
<div class="item"><a href="https://imuslab.com" target="_blank">imuslab</a></div>
</div>
</div>
</div>
</div>
<br><br><br><br>
</div>
<script>
$(".year").text(new Date().getFullYear() );
AOS.init();
$(".year").text(new Date().getFullYear());
$(".menu-item").on("click", function(){
$(".menu-item.active").removeClass("active");
$(this).addClass("active");
});
// Function to open the modal with the clicked image using jQuery
function openModal(src) {
// Remove the old modal if it exists
$('#imageModal').remove();
$(".right-content").on("scroll", function() {
var scrollPos = $(".right-content").scrollTop();
if (scrollPos < 10){
//Reaching the top
$('.menu-item.active').removeClass("active");
$($('.menu-item')[0]).addClass('active');
return;
}else if ($(".right-content")[0].scrollHeight == $(".right-content").scrollTop() + window.innerHeight ){
//Reaching the bottom
$('.menu-item.active').removeClass("active");
$($('.menu-item').get().reverse()[0]).addClass('active');
return
const modal = $('<div style="display:none;">', { id: 'imageModal' }).css({
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: '1000'
});
const img = $('<img>', { src: src }).css({
maxWidth: '80%',
maxHeight: '80%',
boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)'
});
modal.append(img);
$("body").css("overflow", "hidden");
modal.on('click', function() {
modal.remove();
$("body").css("overflow", "auto");
});
$('body').append(modal);
}
$('.menu-item').each(function() {
var currLink = $(this);
var refElement = $(currLink.attr("href"));
if (refElement.offset().top <= (window.innerHeight / 2)) {
$('.menu-item.active').removeClass("active");
currLink.addClass("active");
console.log(currLink.attr("href"));
}
});
});
// Add click event listener to all screenshot images using jQuery
$('.screenshot').on('click', function() {
openModal($(this).attr('src'));
});
</script>
<!-- Locales -->
<script src="main.js" defer></script>
</body>
</html>
</html>

386
docs/index_legacy.html Normal file
View File

@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="Reverse Proxy, Cluster, Gateway, Go, Homelab, Network Tools" name="keywords">
<meta content="A reverse proxy server and cluster network gateway for noobs" name="description">
<meta name="author" content="tobychui">
<!-- HTML Meta Tags -->
<title>Reverse Proxy Server | Zoraxy</title>
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
<!-- Facebook Meta Tags -->
<meta property="og:url" content="https://zoraxy.aroz.org/">
<meta property="og:type" content="website">
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="aroz.org">
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- Favicons -->
<link href="favicon.png" rel="icon">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@100;300;400;600;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
<!-- Main Stylesheet File -->
<link href="style.css" rel="stylesheet">
<script
src="https://code.jquery.com/jquery-3.7.0.min.js"
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js" integrity="sha512-5cguXwRllb+6bcc2pogwIeQmQPXEzn2ddsqAexIBhh7FO1z5Hkek1J9mrK2+rmZCTU6b6pERxI7acnp1MpAg4Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css" integrity="sha512-n//BDM4vMPvyca4bJjZPDh7hlqsQ7hqbP9RH18GF2hTXBY5amBwM2501M0GPiwCU/v9Tor2m13GOTFjk00tkQA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
p,a,div,span,h1,h2,h3,h4,h5,h6{
font-family: 'Source Sans Pro', sans-serif !important;
color: #404040;
}
</style>
</head>
<body>
<div class="main section">
<div class="left-menu">
<div class="iconWrapper">
<a href="index.html"><img class="ui fluid image" src="img/icon.png"></a>
</div>
<a href="#home" class="menu-item active" align="center">
<img src="img/icons/home.svg">
</a>
<a href="#features" class="menu-item" align="center">
<img src="img/icons/awesome.svg">
</a>
<a href="#screenshots" class="menu-item" align="center">
<img src="img/icons/screenshots.svg">
</a>
<a href="#plugins" class="menu-item" align="center">
<img src="img/icons/plugin.svg">
</a>
<a href="#source" class="menu-item" align="center">
<img src="img/icons/code.svg">
</a>
</div>
<div class="right-content">
<!-- Hero Banner Section -->
<div class="headbanner"></div>
<div id="home" class="herotext">
<div class="ui basic segment">
<div class="bannerHeaderWrapper">
<h1 class="bannerHeader">Zoraxy</h1>
<div class="ui divider"></div><br>
<p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
</div>
<br><br>
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
<br><br>
<table class="ui very basic collapsing unstackable celled table">
<thead>
<tr><th colspan="2">Quick Access</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<h4 class="ui image header">
<i class="ui download icon"></i>
<div class="content">
Download
<div class="sub header">Prebuild Binary
</div>
</div>
</h4></td>
<td>
<a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Open <i class="ui external icon"></i></a>
</td>
</tr>
<tr>
<td>
<h4 class="ui image header">
<i class="ui github icon"></i>
<div class="content">
Github
<div class="sub header">Source Code
</div>
</div>
</h4></td>
<td>
<a href="https://github.com/tobychui/zoraxy" target="_blank">Open <i class="ui external icon"></i></a>
</td>
</tr>
</table>
</div>
<div id="wavesWrapper">
<!-- CSS waves-->
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
</g>
</svg>
</div>
</div>
<!-- Features -->
<div id="features" class="section">
<div class="ui container">
<div class="ui basic segment">
<h1 class="ui header">
<img class="ui small image" src="img/icons/awesome.svg">
<div class="content">
Features
<div class="sub header">Highlighting a few important features of Zoraxy</div>
</div>
</h1>
<br>
<div class="ui stackable grid featureList">
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/proxy.svg">
<div class="content">
Reverse Proxy
</div>
</h3>
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/redirect.svg">
<div class="content">
Redirection
</div>
</h3>
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/blacklist.svg">
<div class="content">
Geo-IP & Blacklist
</div>
</h3>
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/gan.svg">
<div class="content">
Global Area Network
</div>
</h3>
<p>ZeroTier controller integrated GAN. Enable unlimited nodes in your network with a few clicks.</p>
</div>
<!-- Row 2-->
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/terminal.svg">
<div class="content">
Web SSH
</div>
</h3>
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/stats.svg">
<div class="content">
Real Time Statistics
</div>
</h3>
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/scan.svg">
<div class="content">
Scanner & Utilities
</div>
</h3>
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
</div>
<div class="four wide column featureItem">
<h3 class="ui header featureHeader">
<img class="ui image" src="img/icons/code.svg">
<div class="content">
Open Source
</div>
</h3>
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
</div>
</div>
</div>
</div>
</div>
<!-- Screenshots -->
<div id="screenshots" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/screenshots.svg">
<div class="content">
Screenshots
<div class="sub header">A quick overview of the UI designs</div>
</div>
</h1>
<div class="ui three column stackable grid">
<div class="column">
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
</div>
</div>
</div>
</div>
<!-- Plugin Developments -->
<div id="plugins" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/plugin.svg">
<div class="content">
Plugins
<div class="sub header">Add custom routing rules via simple scripts</div>
</div>
</h1>
<div style="width: 100%; text-align: center;">
<br>
<p>Documentation work in progress</p>
</div>
</div>
</div>
<!-- Source code -->
<div id="source" class="ui container">
<div class="ui basic segment">
<br>
<h1 class="ui header">
<img class="ui small image" src="img/icons/code.svg">
<div class="content">
Source Code
<div class="sub header">Feel free to give us a ⭐ star ⭐.</div>
</div>
</h1>
<br>
<div class="ui two column stackable grid">
<div class="column">
<h3 class="ui header">
<i class="grey github icon"></i>
<div class="content" style="text-align: left;">
<a href="https://github.com/tobychui/zoraxy">
Github
<div class="sub header">https://github.com/tobychui/zoraxy</div>
</a>
</div>
</h3>
</div>
<div class="column">
<h3 class="ui header">
<i class="blue mail icon"></i>
<div class="content" style="text-align: left;">
<a href="mailto:toby@imuslab.com">
Email Contact
<div class="sub header">toby@imuslab.com</div>
</a>
</div>
</h3>
</div>
</div>
</div>
</div>
<br><br>
<div class="ui container">
<p style="color: #3a3a3a">CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
</div>
<br><br><br>
</div>
</div>
<br>
<script>
$(".year").text(new Date().getFullYear() );
$(".menu-item").on("click", function(){
$(".menu-item.active").removeClass("active");
$(this).addClass("active");
});
$(".right-content").on("scroll", function() {
var scrollPos = $(".right-content").scrollTop();
if (scrollPos < 10){
//Reaching the top
$('.menu-item.active').removeClass("active");
$($('.menu-item')[0]).addClass('active');
return;
}else if ($(".right-content")[0].scrollHeight == $(".right-content").scrollTop() + window.innerHeight ){
//Reaching the bottom
$('.menu-item.active').removeClass("active");
$($('.menu-item').get().reverse()[0]).addClass('active');
return
}
$('.menu-item').each(function() {
var currLink = $(this);
var refElement = $(currLink.attr("href"));
if (refElement.offset().top <= (window.innerHeight / 2)) {
$('.menu-item.active').removeClass("active");
currLink.addClass("active");
console.log(currLink.attr("href"));
}
});
});
</script>
</body>
</html>

462
docs/main.css Normal file
View File

@ -0,0 +1,462 @@
/* Global */
p,a,div,span,h1,h2,h3,h4,h5,h6{
font-family: 'Source Sans Pro', sans-serif;
}
body.en *:not(i){
font-family: 'Source Sans Pro', 'Noto Sans TC',sans-serif !important;
}
body.zh *:not(i){
font-family: 'Noto Sans TC',sans-serif !important;
}
body.jp *:not(i){
font-family: "Noto Sans JP", sans-serif !important;
}
body.zh-cn *:not(i){
font-family: 'Noto Sans SC',sans-serif !important;
}
.centered.title{
padding: 2em;
margin-bottom: 2em;
text-align: center;
}
.centered.title h1{
font-weight: 300 !important;
}
.messageBanner{
width: 100%;
background: #6cacff;
text-align: center;
color: white;
padding: 10px;
}
.messageBanner .header{
font-weight: 500;
}
#backToTopBtn{
position: fixed;
bottom: 1em;
right: 1em;
display:none;
z-index: 999;
border: 1px solid white;
background: #6cacff;
}
#backToTopBtn:hover{
opacity: 0.8;
}
#backToTopBtn i{
color: white;
}
/* Main Menu */
#mainmenu{
padding-top: 0.4em;
padding-bottom: 0.4em;
border-radius: 0;
margin-bottom: 0;
margin-top: 0;
background: transparent !important;
}
#slideshowBanner .ui.basic.white.button{
color: white;
box-shadow: 0 0 0 1px rgb(231, 231, 231) inset;
border-radius: 0.4em;
background: none !important;
}
#slideshowBanner .ui.basic.white.button:hover{
background-color: rgba(255, 255, 255, 0.3) !important;
}
#slideshowBanner .ui.basic.white.button:active{
background: rgba(255, 255, 255, 0.5) !important;
}
#rwdmenubtn{
display:none;
position: absolute;
background: white;
border: 1px solid #6cacff;
color: #6cacff;
}
#mainmenu .ui.secondary.inverted.menu .link.item:not(.disabled), .ui.secondary.inverted.menu a.item:not(.disabled){
font-size: 1.1em;
font-weight: 500;
border-bottom: 1px solid transparent;
transition: border-bottom ease-in-out 0.1s;
color: white !important;
border-radius: 0;
}
#mainmenu #mainmenu .ui.secondary.inverted.menu .link.item:not(.disabled), .ui.secondary.inverted.menu a.item:not(.disabled):hover{
background-color: transparent;
border-bottom: 1px solid #82adfc;
color: #82adfc !important;
}
/* Image Sldiers */
#slideshowBanner{
background: rgb(108,172,255);
background: linear-gradient(48deg, rgba(108,172,255,1) 8%, rgba(141,235,255,1) 65%);
position: relative;
height: 80vh;
}
.slideshow {
width: 100%;
overflow: hidden;
border-radius: 0;
max-height: 500px;
}
.slideshow .slides {
display: flex;
transition: transform 1s ease-in-out;
opacity: 0.6;
filter: blur(2px);
pointer-events: none;
user-select: none;
}
.slideshow .slide {
min-width: 100%;
box-sizing: border-box;
}
.slideshow .slide img {
width: 100%;
display: block;
}
.slideshow .dots{
text-align: center;
position: absolute;
bottom: 15px;
width: 100%;
}
.slideshow .dot {
display: inline-block;
width: 10px;
height: 10px;
margin: 0 5px;
background-color: #bebebe;
border-radius: 50%;
cursor: pointer;
transition: background-color 0.6s ease;
}
.dot.active {
background-color: #ffffff;
}
#slideshowBanner .title{
display: inline-block;
width: 100%;
max-width: 500px;
text-align: left;
position: absolute;
top: 50%;
margin-left: 10%;
transform: translateX(0%) translateY(-50%);
color: white;
}
#slideshowBanner .title .scrolldownTips{
display: none;
}
#slideshowBanner .title h1{
font-size: 4em;
font-weight: 600;
margin-bottom: 0;
}
#slideshowBanner .title p{
font-size: 1.2em;
}
/* About Zoraxy */
.about-text-wrapper{
margin-top: 3em;
}
.about-text-wrapper p, .about-text-wrapper .list .item{
font-weight: 300;
}
.about-title{
font-size: 2.4em;
font-weight: 300;
margin-bottom: 0em;
}
.about-title b{
font-weight: 800;
}
.about-text-wrapper .ui.list .item{
margin-bottom: 0.6em;
}
.about-text-wrapper .ui.list .item .icon{
padding-top: 0.15em;
}
/* Screenshots */
#features{
margin-bottom: 3em;
}
#features .screenshot{
transition: opacity 0.1s ease-in-out;
cursor: pointer;
}
#features .screenshot:hover{
opacity: 0.5;
}
/* Videos */
#techspec .centered.title{
color: white;
}
#techspec p {
color: white;
}
#techspec .videoScrollBar{
overflow-x: scroll;
display: block;
white-space: nowrap;
scrollbar-color: #e7e7e7 rgba(0, 0, 0, 0.1);
padding-top: 2em;
padding-bottom: 3em;
}
.introvideo{
display: inline-block !important;
}
.blackbanner{
width: 100%;
background: rgb(108,172,255);
background: linear-gradient(48deg, rgba(108,172,255,1) 8%, rgba(141,235,255,1) 65%);
min-height: 300px;
}
/* Download */
.downloadButton {
margin-top: 0.4em !important;
}
.downloadTabWrapper{
width: 100%;
overflow-x: hidden;
}
#download .ui.black.message{
word-break: break-all;
}
/* Learn More */
#learnmore .linkicons{
text-align: center;
width: 100%;
}
#learnmore .linkicons .divider{
margin-left: 1em;
margin-right: 1em;
}
#learnmore .linkicons .externallink{
margin-bottom: 0.6em;
transition: opacity 0.1s ease-in-out;
}
#learnmore .linkicons .externallink i{
/* color: #1b1c1d; */
font-weight: 300;
font-size: 1.5em;
}
#learnmore .linkicons .externallink:hover{
opacity: 0.8;
}
#learnmore .linkicons .externallink .content{
color: #1b1c1d;
font-weight: 500;
font-size: 0.6em;
}
/* Footer */
#footer{
background: rgb(85,131,238);
background: linear-gradient(48deg, rgba(85,131,238,1) 21%, rgba(108,172,255,1) 73%);
color: rgb(255, 255, 255);
}
#footer a {
color: rgb(209, 224, 255);
}
#footer a:hover{
color: rgb(255, 255, 255);
}
#footer .bottom-attach .divider{
color: rgb(212, 212, 212);
}
#footer .ui.list .title{
margin-bottom: 0.6em;
}
/* RWD Rules */
@media (max-width:960px) {
/* Main menu */
#mainmenu{
display:none;
z-index: 99;
position: absolute;
top: 0;
left: 0;
width: 100%;
background: #fdfdfd !important;
}
#rwdmenubtn{
display: block;
position: absolute;
top: 0.4em;
right: 0.4em;
z-index: 100;
}
/* Slideshows */
.slideshow {
min-height: 100vh;
}
.slideshow .slide{
height: 100% !important;
min-width: none;
}
.slideshow .slide img{
height: 100%;
width: auto;
}
#slideshowBanner .title{
padding: 1em;
margin-left: 0;
}
#slideshowBanner .title .scrolldownTips{
margin-top: 2em;
display: block;
}
#slideshowBanner .title .scrolldownTips img{
left: 50%;
transform: translateX(-50%);
}
#techspec .videoScrollBar{
overflow-x: auto;
display: block;
scrollbar-color: #e7e7e7 rgba(0, 0, 0, 0.1);
padding-top: 2em;
padding-bottom: 3em;
}
.introvideo {
display: block !important;
width: 100%;
margin-bottom: 1em;
}
.introvideo iframe{
width: 100%;
}
#download .stackable.tabular.menu .active.item{
background-color: rgb(243, 243, 243);
border-width: 0;
border-radius: 0.4em !important;
}
}
/*
Waves CSS
*/
#wavesWrapper{
position: absolute;
bottom: 0;
width: 100%;
left: 0;
}
.waves {
position:relative;
width: 100%;
height:15vh;
margin-bottom:-7px; /*Fix for safari gap*/
min-height:100px;
max-height:150px;
}
.parallax > use {
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
}
.parallax > use:nth-child(1) {
animation-delay: -8s;
animation-duration: 28s;
}
.parallax > use:nth-child(2) {
animation-delay: -12s;
animation-duration: 40s;
}
.parallax > use:nth-child(3) {
animation-delay: -16s;
animation-duration: 52s;
}
.parallax > use:nth-child(4) {
animation-delay: -20s;
animation-duration: 80s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px,0,0);
}
100% {
transform: translate3d(85px,0,0);
}
}
/*Shrinking for mobile*/
@media (max-width: 768px) {
.waves {
height:40px;
min-height:40px;
}
}

84
docs/main.js Normal file
View File

@ -0,0 +1,84 @@
/*
Localization
To add more locales, add to the html file with // (translated text)
after each DOM elements with attr i18n
And then add the language ISO key to the list below.
*/
let languages = ['en', 'zh', 'de'];
//Bind language change dropdown events
$(".dropdown").dropdown();
$("#language").on("change",function(){
let newLang = $("#language").parent().dropdown("get value");
i18n.changeLanguage(newLang);
$("body").attr("class", newLang);
});
//Initialize the i18n dom library
var i18n = domI18n({
selector: '[i18n]',
separator: ' // ',
languages: languages,
defaultLanguage: 'en'
});
$(document).ready(function(){
let userLang = navigator.language || navigator.userLanguage;
console.log("User language: " + userLang);
userLang = userLang.split("-")[0];
if (!languages.includes(userLang)) {
userLang = 'en';
}
i18n.changeLanguage(userLang);
$("body").attr("class", userLang);
});
/* Main Menu */
$("#rwdmenubtn").on("click", function(){
$("#mainmenu").slideToggle("fast");
})
//Handle resize
$(window).on("resize", function(){
if (window.innerWidth > 960){
$("#mainmenu").show();
}else{
$("#mainmenu").hide();
}
})
/*
Download
*/
$('.menu .item').tab();
//Download webpack and binary at the same time
function handleDownload(releasename){
let binaryURL = "https://github.com/tobychui/zoraxy/releases/latest/download/" + releasename;
window.open(binaryURL);
}
/* RWD */
window.addEventListener('scroll', function() {
var scrollPosition = window.scrollY || window.pageYOffset;
var windowHeight = window.innerHeight;
var hiddenDiv = document.querySelector('#backToTopBtn');
if (scrollPosition > windowHeight / 2) {
hiddenDiv.style.display = 'block';
} else {
hiddenDiv.style.display = 'none';
}
});
function backToTop(){
$('html, body').animate({scrollTop : 0},800, function(){
window.location.hash = "";
});
}

View File

@ -0,0 +1,32 @@
#!/bin/bash
# This script builds all the plugins in the current directory
echo "Copying zoraxy_plugin to all mods"
for dir in ./*; do
if [ -d "$dir" ]; then
cp -r ../mod/plugins/zoraxy_plugin "$dir/mod"
fi
done
# Iterate over all directories in the current directory
echo "Running go mod tidy and go build for all directories"
for dir in */; do
if [ -d "$dir" ]; then
echo "Processing directory: $dir"
cd "$dir"
# Execute go mod tidy
echo "Running go mod tidy in $dir"
go mod tidy
# Execute go build
echo "Running go build in $dir"
go build
# Return to the parent directory
cd ..
fi
done
echo "Build process completed for all directories."

View File

@ -0,0 +1,3 @@
module aroz.org/zoraxy/debugger
go 1.23.6

View File

@ -0,0 +1,140 @@
package main
import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
plugin "aroz.org/zoraxy/debugger/mod/zoraxy_plugin"
)
const (
PLUGIN_ID = "org.aroz.zoraxy.debugger"
UI_PATH = "/debug"
STATIC_CAPTURE_INGRESS = "/s_capture"
)
func main() {
// Serve the plugin intro spect
// This will print the plugin intro spect and exit if the -introspect flag is provided
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
ID: "org.aroz.zoraxy.debugger",
Name: "Plugin Debugger",
Author: "aroz.org",
AuthorContact: "https://aroz.org",
Description: "A debugger for Zoraxy <-> plugin communication pipeline",
URL: "https://zoraxy.aroz.org",
Type: plugin.PluginType_Router,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
StaticCapturePaths: []plugin.StaticCaptureRule{
{
CapturePath: "/test_a",
},
{
CapturePath: "/test_b",
},
},
StaticCaptureIngress: "/s_capture",
DynamicCaptureSniff: "/d_sniff",
DynamicCaptureIngress: "/d_capture",
UIPath: UI_PATH,
/*
SubscriptionPath: "/subept",
SubscriptionsEvents: []plugin.SubscriptionEvent{
*/
})
if err != nil {
//Terminate or enter standalone mode here
panic(err)
}
// Setup the path router
pathRouter := plugin.NewPathRouter()
pathRouter.SetDebugPrintMode(true)
/*
Static Routers
*/
pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//In theory this should never be called
//but just in case the request is not captured by the path handlers
//this will be the fallback handler
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("This request is captured by the default handler!<br>Request URI: " + r.URL.String()))
}))
pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
/*
Dynamic Captures
*/
pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
//fmt.Println("Dynamic Capture Sniffed Request:")
//fmt.Println("Request URI: " + dsfr.RequestURI)
//In this example, we want to capture all URI
//that start with /test_ and forward it to the dynamic capture handler
if strings.HasPrefix(dsfr.RequestURI, "/test_") {
reqUUID := dsfr.GetRequestUUID()
fmt.Println("Accepting request with UUID: " + reqUUID)
return plugin.SniffResultAccpet
}
return plugin.SniffResultSkip
})
pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
// This is the dynamic capture handler where it actually captures and handle the request
w.WriteHeader(http.StatusOK)
w.Write([]byte("Welcome to the dynamic capture handler!"))
// Print all the request info to the response writer
w.Write([]byte("\n\nRequest Info:\n"))
w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
w.Write([]byte("Request Method: " + r.Method + "\n"))
w.Write([]byte("Request Headers:\n"))
headers := make([]string, 0, len(r.Header))
for key := range r.Header {
headers = append(headers, key)
}
sort.Strings(headers)
for _, key := range headers {
for _, value := range r.Header[key] {
w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
}
}
})
http.HandleFunc(UI_PATH+"/", RenderDebugUI)
fmt.Println("Debugger started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
}
// Handle the captured request
func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
/*for key, values := range r.Header {
for _, value := range values {
fmt.Printf("%s: %s\n", key, value)
}
}*/
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("This request is captured by A handler!<br>Request URI: " + r.URL.String()))
}
func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
/*for key, values := range r.Header {
for _, value := range values {
fmt.Printf("%s: %s\n", key, value)
}
}*/
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("This request is captured by the B handler!<br>Request URI: " + r.URL.String()))
}

View File

@ -0,0 +1,19 @@
# Zoraxy Plugin
## Overview
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
## Instructions
1. **Copy the Module:**
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
2. **Include the Structure:**
- Ensure that you maintain the directory structure and file organization as provided in this module.
3. **Modify as Needed:**
- Customize the copied module to implement the desired functionality for your plugin.
## Directory Structure
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages

View File

@ -0,0 +1,145 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"os"
"strings"
"time"
)
type PluginUiDebugRouter struct {
PluginID string //The ID of the plugin
TargetDir string //The directory where the UI files are stored
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
// The targetDir is the directory where the UI files are stored (e.g. ./www)
// The handlerPrefix is the prefix of the handler used to route this router
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
// All prefix should not end with a slash
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
//Make sure all prefix are in /prefix format
if !strings.HasPrefix(handlerPrefix, "/") {
handlerPrefix = "/" + handlerPrefix
}
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
//Return the PluginUiRouter
return &PluginUiDebugRouter{
PluginID: pluginID,
TargetDir: targetDir,
HandlerPrefix: handlerPrefix,
}
}
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
//Get the CSRF token from header
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
if csrfToken == "" {
csrfToken = "missing-csrf-token"
}
//Return the middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the request is for an HTML file
if strings.HasSuffix(r.URL.Path, ".html") {
//Read the target file from file system
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
//Check if the request is for a directory
//Check if the directory has an index.html file
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
if _, err := os.Stat(targetFilePath); err == nil {
//Serve the index.html file
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
}
}
//Call the next handler
fsHandler.ServeHTTP(w, r)
})
}
// GetHttpHandler returns the http.Handler for the PluginUiRouter
func (p *PluginUiDebugRouter) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Remove the plugin UI handler path prefix
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL.Path = rewrittenURL
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//Serve the file from the file system
fsHandler := http.FileServer(http.Dir(p.TargetDir))
// Replace {{csrf_token}} with the actual CSRF token and serve the file
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// Attach the file system UI handler to the target http.ServeMux
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@ -0,0 +1,162 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
/*
Dynamic Path Handler
*/
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
/*
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
You can decide to accept or skip the request based on the request header and paths
*/
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
if !strings.HasSuffix(sniff_ingress, "/") {
sniff_ingress = sniff_ingress + "/"
}
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
}
// Decode the request payload
jsonBytes, err := io.ReadAll(r.Body)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error reading request body:", err)
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
payload, err := DecodeForwardRequestPayload(jsonBytes)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error decoding request payload:", err)
fmt.Print("Payload: ")
fmt.Println(string(jsonBytes))
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Get the forwarded request UUID
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
payload.requestUUID = forwardUUID
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("SKIP"))
}
}))
}
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
if rewrittenURL == "" {
rewrittenURL = "/"
}
if !strings.HasPrefix(rewrittenURL, "/") {
rewrittenURL = "/" + rewrittenURL
}
r.RequestURI = rewrittenURL
handlefunc(w, r)
}))
}
/*
Sniffing and forwarding
The following functions are here to help with
sniffing and forwarding requests to the dynamic
router.
*/
// A custom request object to be used in the dynamic sniffing
type DynamicSniffForwardRequest struct {
Method string `json:"method"`
Hostname string `json:"hostname"`
URL string `json:"url"`
Header map[string][]string `json:"header"`
RemoteAddr string `json:"remote_addr"`
Host string `json:"host"`
RequestURI string `json:"request_uri"`
Proto string `json:"proto"`
ProtoMajor int `json:"proto_major"`
ProtoMinor int `json:"proto_minor"`
/* Internal use */
rawRequest *http.Request `json:"-"`
requestUUID string `json:"-"`
}
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
return DynamicSniffForwardRequest{
Method: r.Method,
Hostname: r.Host,
URL: r.URL.String(),
Header: r.Header,
RemoteAddr: r.RemoteAddr,
Host: r.Host,
RequestURI: r.RequestURI,
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
rawRequest: r,
}
}
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
var payload DynamicSniffForwardRequest
err := json.Unmarshal(jsonBytes, &payload)
if err != nil {
return DynamicSniffForwardRequest{}, err
}
return payload, nil
}
// GetRequest returns the original http.Request object, for debugging purposes
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
return dsfr.rawRequest
}
// GetRequestUUID returns the request UUID
// if this UUID is empty string, that might indicate the request
// is not coming from the dynamic router
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
return dsfr.requestUUID
}

View File

@ -0,0 +1,156 @@
package zoraxy_plugin
import (
"embed"
"fmt"
"io/fs"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type PluginUiRouter struct {
PluginID string //The ID of the plugin
TargetFs *embed.FS //The embed.FS where the UI files are stored
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored
// The targetFsPrefix should be relative to the root of the embed.FS
// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS
// The handlerPrefix is the prefix of the handler used to route this router
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
// All prefix should not end with a slash
func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
//Make sure all prefix are in /prefix format
if !strings.HasPrefix(targetFsPrefix, "/") {
targetFsPrefix = "/" + targetFsPrefix
}
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
if !strings.HasPrefix(handlerPrefix, "/") {
handlerPrefix = "/" + handlerPrefix
}
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
//Return the PluginUiRouter
return &PluginUiRouter{
PluginID: pluginID,
TargetFs: targetFs,
TargetFsPrefix: targetFsPrefix,
HandlerPrefix: handlerPrefix,
}
}
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
//Get the CSRF token from header
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
if csrfToken == "" {
csrfToken = "missing-csrf-token"
}
//Return the middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the request is for an HTML file
if strings.HasSuffix(r.URL.Path, ".html") {
//Read the target file from embed.FS
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
// Check if the directory has an index.html file
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
if err == nil {
body := string(indexFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
}
}
//Call the next handler
fsHandler.ServeHTTP(w, r)
})
}
// GetHttpHandler returns the http.Handler for the PluginUiRouter
func (p *PluginUiRouter) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Remove the plugin UI handler path prefix
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL, _ = url.Parse(rewrittenURL)
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//Serve the file from the embed.FS
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
if err != nil {
fmt.Println(err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Replace {{csrf_token}} with the actual CSRF token and serve the file
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// Attach the embed UI handler to the target http.ServeMux
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@ -0,0 +1,105 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"sort"
"strings"
)
type PathRouter struct {
enableDebugPrint bool
pathHandlers map[string]http.Handler
defaultHandler http.Handler
}
// NewPathRouter creates a new PathRouter
func NewPathRouter() *PathRouter {
return &PathRouter{
enableDebugPrint: false,
pathHandlers: make(map[string]http.Handler),
}
}
// RegisterPathHandler registers a handler for a path
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
path = strings.TrimSuffix(path, "/")
p.pathHandlers[path] = handler
}
// RemovePathHandler removes a handler for a path
func (p *PathRouter) RemovePathHandler(path string) {
delete(p.pathHandlers, path)
}
// SetDefaultHandler sets the default handler for the router
// This handler will be called if no path handler is found
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
p.defaultHandler = handler
}
// SetDebugPrintMode sets the debug print mode
func (p *PathRouter) SetDebugPrintMode(enable bool) {
p.enableDebugPrint = enable
}
// StartStaticCapture starts the static capture ingress
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p.staticCaptureServeHTTP(w, r)
}))
}
// staticCaptureServeHTTP serves the static capture path using user defined handler
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
capturePath := r.Header.Get("X-Zoraxy-Capture")
if capturePath != "" {
if p.enableDebugPrint {
fmt.Printf("Using capture path: %s\n", capturePath)
}
originalURI := r.Header.Get("X-Zoraxy-Uri")
r.URL.Path = originalURI
if handler, ok := p.pathHandlers[capturePath]; ok {
handler.ServeHTTP(w, r)
return
}
}
p.defaultHandler.ServeHTTP(w, r)
}
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
if p.enableDebugPrint {
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
keys := make([]string, 0, len(r.Header))
for key := range r.Header {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
for _, value := range r.Header[key] {
fmt.Printf("%s: %s\n", key, value)
}
}
fmt.Printf("\n\n**Request Details**\n\n")
fmt.Printf("Method: %s\n", r.Method)
fmt.Printf("URL: %s\n", r.URL.String())
fmt.Printf("Proto: %s\n", r.Proto)
fmt.Printf("Host: %s\n", r.Host)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
fmt.Printf("ContentLength: %d\n", r.ContentLength)
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
fmt.Printf("Close: %v\n", r.Close)
fmt.Printf("Form: %v\n", r.Form)
fmt.Printf("PostForm: %v\n", r.PostForm)
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
fmt.Printf("Trailer: %v\n", r.Trailer)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
}
}

View File

@ -0,0 +1,176 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"os"
"strings"
)
/*
Plugins Includes.go
This file is copied from Zoraxy source code
You can always find the latest version under mod/plugins/includes.go
Usually this file are backward compatible
*/
type PluginType int
const (
PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic
PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore
)
type StaticCaptureRule struct {
CapturePath string `json:"capture_path"`
//To be expanded
}
type ControlStatusCode int
const (
ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic
ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic
ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error
)
type SubscriptionEvent struct {
EventName string `json:"event_name"`
EventSource string `json:"event_source"`
Payload string `json:"payload"` //Payload of the event, can be empty
}
type RuntimeConstantValue struct {
ZoraxyVersion string `json:"zoraxy_version"`
ZoraxyUUID string `json:"zoraxy_uuid"`
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
/*
IntroSpect Payload
When the plugin is initialized with -introspect flag,
the plugin shell return this payload as JSON and exit
*/
type IntroSpect struct {
/* Plugin metadata */
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
Name string `json:"name"` //Name of your plugin
Author string `json:"author"` //Author name of your plugin
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
Description string `json:"description"` //Description of your plugin
URL string `json:"url"` //URL of your plugin
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
VersionMajor int `json:"version_major"` //Major version of your plugin
VersionMinor int `json:"version_minor"` //Minor version of your plugin
VersionPatch int `json:"version_patch"` //Patch version of your plugin
/*
Endpoint Settings
*/
/*
Static Capture Settings
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
This is faster than dynamic capture, but less flexible
*/
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
/*
Dynamic Capture Settings
Once plugin is enabled, these rules will be captured and forward to plugin sniff
if the plugin sniff returns 280, the traffic will be captured
otherwise, the traffic will be forwarded to the next plugin
This is slower than static capture, but more flexible
*/
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
/* UI Path for your plugin */
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
/* Subscriptions Settings */
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
}
/*
ServeIntroSpect Function
This function will check if the plugin is initialized with -introspect flag,
if so, it will print the intro spect and exit
Place this function at the beginning of your plugin main function
*/
func ServeIntroSpect(pluginSpect *IntroSpect) {
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
//Print the intro spect and exit
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
fmt.Println(string(jsonData))
os.Exit(0)
}
}
/*
ConfigureSpec Payload
Zoraxy will start your plugin with -configure flag,
the plugin shell read this payload as JSON and configure itself
by the supplied values like starting a web server at given port
that listens to 127.0.0.1:port
*/
type ConfigureSpec struct {
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
//To be expanded
}
/*
RecvExecuteConfigureSpec Function
This function will read the configure spec from Zoraxy
and return the ConfigureSpec object
Place this function after ServeIntroSpect function in your plugin main function
*/
func RecvConfigureSpec() (*ConfigureSpec, error) {
for i, arg := range os.Args {
if strings.HasPrefix(arg, "-configure=") {
var configSpec ConfigureSpec
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
return nil, err
}
return &configSpec, nil
} else if arg == "-configure" {
var configSpec ConfigureSpec
var nextArg string
if len(os.Args) > i+1 {
nextArg = os.Args[i+1]
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("No port specified after -configure flag")
}
return &configSpec, nil
}
}
return nil, fmt.Errorf("No -configure flag found")
}
/*
ServeAndRecvSpec Function
This function will serve the intro spect and return the configure spec
See the ServeIntroSpect and RecvConfigureSpec for more details
*/
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
ServeIntroSpect(pluginSpect)
return RecvConfigureSpec()
}

View File

@ -0,0 +1,26 @@
package main
import (
_ "embed"
"fmt"
"net/http"
"sort"
)
// Render the debug UI
func RenderDebugUI(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "**Plugin UI Debug Interface**\n\n[Recv Headers] \n")
headerKeys := make([]string, 0, len(r.Header))
for name := range r.Header {
headerKeys = append(headerKeys, name)
}
sort.Strings(headerKeys)
for _, name := range headerKeys {
values := r.Header[name]
for _, value := range values {
fmt.Fprintf(w, "%s: %s\n", name, value)
}
}
w.Header().Set("Content-Type", "text/html")
}

View File

@ -0,0 +1,3 @@
module example.com/zoraxy/helloworld
go 1.23.6

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,63 @@
package main
import (
"embed"
_ "embed"
"fmt"
"net/http"
"strconv"
plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
)
const (
PLUGIN_ID = "com.example.helloworld"
UI_PATH = "/"
WEB_ROOT = "/www"
)
//go:embed www/*
var content embed.FS
func main() {
// Serve the plugin intro spect
// This will print the plugin intro spect and exit if the -introspect flag is provided
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
ID: "com.example.helloworld",
Name: "Hello World Plugin",
Author: "foobar",
AuthorContact: "admin@example.com",
Description: "A simple hello world plugin",
URL: "https://example.com",
Type: plugin.PluginType_Utilities,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
// As this is a utility plugin, we don't need to capture any traffic
// but only serve the UI, so we set the UI (relative to the plugin path) to "/"
UIPath: UI_PATH,
})
if err != nil {
//Terminate or enter standalone mode here
panic(err)
}
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
// The router will also help to handle the termination of the plugin when
// a user wants to stop the plugin via Zoraxy Web UI
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
embedWebRouter.RegisterTerminateHandler(func() {
// Do cleanup here if needed
fmt.Println("Hello World Plugin Exited")
}, nil)
// Serve the hello world page in the www folder
http.Handle(UI_PATH, embedWebRouter.Handler())
fmt.Println("Hello World started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,19 @@
# Zoraxy Plugin
## Overview
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
## Instructions
1. **Copy the Module:**
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
2. **Include the Structure:**
- Ensure that you maintain the directory structure and file organization as provided in this module.
3. **Modify as Needed:**
- Customize the copied module to implement the desired functionality for your plugin.
## Directory Structure
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages

View File

@ -0,0 +1,145 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"os"
"strings"
"time"
)
type PluginUiDebugRouter struct {
PluginID string //The ID of the plugin
TargetDir string //The directory where the UI files are stored
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
// The targetDir is the directory where the UI files are stored (e.g. ./www)
// The handlerPrefix is the prefix of the handler used to route this router
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
// All prefix should not end with a slash
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
//Make sure all prefix are in /prefix format
if !strings.HasPrefix(handlerPrefix, "/") {
handlerPrefix = "/" + handlerPrefix
}
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
//Return the PluginUiRouter
return &PluginUiDebugRouter{
PluginID: pluginID,
TargetDir: targetDir,
HandlerPrefix: handlerPrefix,
}
}
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
//Get the CSRF token from header
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
if csrfToken == "" {
csrfToken = "missing-csrf-token"
}
//Return the middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the request is for an HTML file
if strings.HasSuffix(r.URL.Path, ".html") {
//Read the target file from file system
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
//Check if the request is for a directory
//Check if the directory has an index.html file
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
if _, err := os.Stat(targetFilePath); err == nil {
//Serve the index.html file
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
}
}
//Call the next handler
fsHandler.ServeHTTP(w, r)
})
}
// GetHttpHandler returns the http.Handler for the PluginUiRouter
func (p *PluginUiDebugRouter) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Remove the plugin UI handler path prefix
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL.Path = rewrittenURL
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//Serve the file from the file system
fsHandler := http.FileServer(http.Dir(p.TargetDir))
// Replace {{csrf_token}} with the actual CSRF token and serve the file
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// Attach the file system UI handler to the target http.ServeMux
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@ -0,0 +1,162 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
/*
Dynamic Path Handler
*/
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
/*
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
You can decide to accept or skip the request based on the request header and paths
*/
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
if !strings.HasSuffix(sniff_ingress, "/") {
sniff_ingress = sniff_ingress + "/"
}
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
}
// Decode the request payload
jsonBytes, err := io.ReadAll(r.Body)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error reading request body:", err)
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
payload, err := DecodeForwardRequestPayload(jsonBytes)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error decoding request payload:", err)
fmt.Print("Payload: ")
fmt.Println(string(jsonBytes))
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Get the forwarded request UUID
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
payload.requestUUID = forwardUUID
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("SKIP"))
}
}))
}
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
if rewrittenURL == "" {
rewrittenURL = "/"
}
if !strings.HasPrefix(rewrittenURL, "/") {
rewrittenURL = "/" + rewrittenURL
}
r.RequestURI = rewrittenURL
handlefunc(w, r)
}))
}
/*
Sniffing and forwarding
The following functions are here to help with
sniffing and forwarding requests to the dynamic
router.
*/
// A custom request object to be used in the dynamic sniffing
type DynamicSniffForwardRequest struct {
Method string `json:"method"`
Hostname string `json:"hostname"`
URL string `json:"url"`
Header map[string][]string `json:"header"`
RemoteAddr string `json:"remote_addr"`
Host string `json:"host"`
RequestURI string `json:"request_uri"`
Proto string `json:"proto"`
ProtoMajor int `json:"proto_major"`
ProtoMinor int `json:"proto_minor"`
/* Internal use */
rawRequest *http.Request `json:"-"`
requestUUID string `json:"-"`
}
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
return DynamicSniffForwardRequest{
Method: r.Method,
Hostname: r.Host,
URL: r.URL.String(),
Header: r.Header,
RemoteAddr: r.RemoteAddr,
Host: r.Host,
RequestURI: r.RequestURI,
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
rawRequest: r,
}
}
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
var payload DynamicSniffForwardRequest
err := json.Unmarshal(jsonBytes, &payload)
if err != nil {
return DynamicSniffForwardRequest{}, err
}
return payload, nil
}
// GetRequest returns the original http.Request object, for debugging purposes
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
return dsfr.rawRequest
}
// GetRequestUUID returns the request UUID
// if this UUID is empty string, that might indicate the request
// is not coming from the dynamic router
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
return dsfr.requestUUID
}

View File

@ -0,0 +1,156 @@
package zoraxy_plugin
import (
"embed"
"fmt"
"io/fs"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type PluginUiRouter struct {
PluginID string //The ID of the plugin
TargetFs *embed.FS //The embed.FS where the UI files are stored
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored
// The targetFsPrefix should be relative to the root of the embed.FS
// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS
// The handlerPrefix is the prefix of the handler used to route this router
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
// All prefix should not end with a slash
func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
//Make sure all prefix are in /prefix format
if !strings.HasPrefix(targetFsPrefix, "/") {
targetFsPrefix = "/" + targetFsPrefix
}
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
if !strings.HasPrefix(handlerPrefix, "/") {
handlerPrefix = "/" + handlerPrefix
}
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
//Return the PluginUiRouter
return &PluginUiRouter{
PluginID: pluginID,
TargetFs: targetFs,
TargetFsPrefix: targetFsPrefix,
HandlerPrefix: handlerPrefix,
}
}
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
//Get the CSRF token from header
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
if csrfToken == "" {
csrfToken = "missing-csrf-token"
}
//Return the middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the request is for an HTML file
if strings.HasSuffix(r.URL.Path, ".html") {
//Read the target file from embed.FS
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
// Check if the directory has an index.html file
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
if err == nil {
body := string(indexFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
}
}
//Call the next handler
fsHandler.ServeHTTP(w, r)
})
}
// GetHttpHandler returns the http.Handler for the PluginUiRouter
func (p *PluginUiRouter) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Remove the plugin UI handler path prefix
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL, _ = url.Parse(rewrittenURL)
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//Serve the file from the embed.FS
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
if err != nil {
fmt.Println(err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Replace {{csrf_token}} with the actual CSRF token and serve the file
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// Attach the embed UI handler to the target http.ServeMux
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@ -0,0 +1,105 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"sort"
"strings"
)
type PathRouter struct {
enableDebugPrint bool
pathHandlers map[string]http.Handler
defaultHandler http.Handler
}
// NewPathRouter creates a new PathRouter
func NewPathRouter() *PathRouter {
return &PathRouter{
enableDebugPrint: false,
pathHandlers: make(map[string]http.Handler),
}
}
// RegisterPathHandler registers a handler for a path
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
path = strings.TrimSuffix(path, "/")
p.pathHandlers[path] = handler
}
// RemovePathHandler removes a handler for a path
func (p *PathRouter) RemovePathHandler(path string) {
delete(p.pathHandlers, path)
}
// SetDefaultHandler sets the default handler for the router
// This handler will be called if no path handler is found
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
p.defaultHandler = handler
}
// SetDebugPrintMode sets the debug print mode
func (p *PathRouter) SetDebugPrintMode(enable bool) {
p.enableDebugPrint = enable
}
// StartStaticCapture starts the static capture ingress
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p.staticCaptureServeHTTP(w, r)
}))
}
// staticCaptureServeHTTP serves the static capture path using user defined handler
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
capturePath := r.Header.Get("X-Zoraxy-Capture")
if capturePath != "" {
if p.enableDebugPrint {
fmt.Printf("Using capture path: %s\n", capturePath)
}
originalURI := r.Header.Get("X-Zoraxy-Uri")
r.URL.Path = originalURI
if handler, ok := p.pathHandlers[capturePath]; ok {
handler.ServeHTTP(w, r)
return
}
}
p.defaultHandler.ServeHTTP(w, r)
}
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
if p.enableDebugPrint {
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
keys := make([]string, 0, len(r.Header))
for key := range r.Header {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
for _, value := range r.Header[key] {
fmt.Printf("%s: %s\n", key, value)
}
}
fmt.Printf("\n\n**Request Details**\n\n")
fmt.Printf("Method: %s\n", r.Method)
fmt.Printf("URL: %s\n", r.URL.String())
fmt.Printf("Proto: %s\n", r.Proto)
fmt.Printf("Host: %s\n", r.Host)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
fmt.Printf("ContentLength: %d\n", r.ContentLength)
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
fmt.Printf("Close: %v\n", r.Close)
fmt.Printf("Form: %v\n", r.Form)
fmt.Printf("PostForm: %v\n", r.PostForm)
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
fmt.Printf("Trailer: %v\n", r.Trailer)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
}
}

View File

@ -0,0 +1,176 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"os"
"strings"
)
/*
Plugins Includes.go
This file is copied from Zoraxy source code
You can always find the latest version under mod/plugins/includes.go
Usually this file are backward compatible
*/
type PluginType int
const (
PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic
PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore
)
type StaticCaptureRule struct {
CapturePath string `json:"capture_path"`
//To be expanded
}
type ControlStatusCode int
const (
ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic
ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic
ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error
)
type SubscriptionEvent struct {
EventName string `json:"event_name"`
EventSource string `json:"event_source"`
Payload string `json:"payload"` //Payload of the event, can be empty
}
type RuntimeConstantValue struct {
ZoraxyVersion string `json:"zoraxy_version"`
ZoraxyUUID string `json:"zoraxy_uuid"`
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
/*
IntroSpect Payload
When the plugin is initialized with -introspect flag,
the plugin shell return this payload as JSON and exit
*/
type IntroSpect struct {
/* Plugin metadata */
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
Name string `json:"name"` //Name of your plugin
Author string `json:"author"` //Author name of your plugin
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
Description string `json:"description"` //Description of your plugin
URL string `json:"url"` //URL of your plugin
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
VersionMajor int `json:"version_major"` //Major version of your plugin
VersionMinor int `json:"version_minor"` //Minor version of your plugin
VersionPatch int `json:"version_patch"` //Patch version of your plugin
/*
Endpoint Settings
*/
/*
Static Capture Settings
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
This is faster than dynamic capture, but less flexible
*/
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
/*
Dynamic Capture Settings
Once plugin is enabled, these rules will be captured and forward to plugin sniff
if the plugin sniff returns 280, the traffic will be captured
otherwise, the traffic will be forwarded to the next plugin
This is slower than static capture, but more flexible
*/
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
/* UI Path for your plugin */
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
/* Subscriptions Settings */
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
}
/*
ServeIntroSpect Function
This function will check if the plugin is initialized with -introspect flag,
if so, it will print the intro spect and exit
Place this function at the beginning of your plugin main function
*/
func ServeIntroSpect(pluginSpect *IntroSpect) {
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
//Print the intro spect and exit
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
fmt.Println(string(jsonData))
os.Exit(0)
}
}
/*
ConfigureSpec Payload
Zoraxy will start your plugin with -configure flag,
the plugin shell read this payload as JSON and configure itself
by the supplied values like starting a web server at given port
that listens to 127.0.0.1:port
*/
type ConfigureSpec struct {
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
//To be expanded
}
/*
RecvExecuteConfigureSpec Function
This function will read the configure spec from Zoraxy
and return the ConfigureSpec object
Place this function after ServeIntroSpect function in your plugin main function
*/
func RecvConfigureSpec() (*ConfigureSpec, error) {
for i, arg := range os.Args {
if strings.HasPrefix(arg, "-configure=") {
var configSpec ConfigureSpec
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
return nil, err
}
return &configSpec, nil
} else if arg == "-configure" {
var configSpec ConfigureSpec
var nextArg string
if len(os.Args) > i+1 {
nextArg = os.Args[i+1]
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("No port specified after -configure flag")
}
return &configSpec, nil
}
}
return nil, fmt.Errorf("No -configure flag found")
}
/*
ServeAndRecvSpec Function
This function will serve the intro spect and return the configure spec
See the ServeIntroSpect and RecvConfigureSpec for more details
*/
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
ServeIntroSpect(pluginSpect)
return RecvConfigureSpec()
}

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- CSRF token, if your plugin need to make POST request to backend -->
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="stylesheet" href="/script/semantic/semantic.min.css">
<script src="/script/jquery-3.6.0.min.js"></script>
<script src="/script/semantic/semantic.min.js"></script>
<script src="/script/utils.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/main.css">
<title>Hello World</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
background:none;
}
</style>
</head>
<body>
<!-- Dark theme script must be included after body tag-->
<link rel="stylesheet" href="/darktheme.css">
<script src="/script/darktheme.js"></script>
<div style="text-align: center;">
<h1>Hello World</h1>
<p>Welcome to your first Zoraxy plugin</p>
</div>
</body>
</html>

327
example/plugins/upnp/api.go Normal file
View File

@ -0,0 +1,327 @@
package main
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
)
/*
API Handlers
*/
func handleUsableState(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
js, _ := json.Marshal(upnpRouterExists)
SendJSONResponse(w, string(js))
} else if r.Method == "POST" {
//Try to probe the UPnP router again
TryStartUPnPClient()
if upnpRouterExists {
SendOK(w)
} else {
SendErrorResponse(w, "UPnP router not found")
}
}
}
// Get or set the enable state of the plugin
func handleEnableState(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
js, _ := json.Marshal(upnpRuntimeConfig.Enabled)
SendJSONResponse(w, string(js))
} else if r.Method == "POST" {
enable, err := PostBool(r, "enable")
if err != nil {
SendErrorResponse(w, err.Error())
return
}
if !enable {
//Close all the port forwards if UPnP client is available
if upnpClient != nil {
for _, record := range upnpRuntimeConfig.ForwardRules {
err = upnpClient.ClosePort(record.PortNumber)
if err != nil {
SendErrorResponse(w, err.Error())
return
}
}
}
} else {
if upnpClient == nil {
SendErrorResponse(w, "No UPnP router in network")
return
}
//Forward all the ports if UPnP client is available
if upnpClient != nil {
for _, record := range upnpRuntimeConfig.ForwardRules {
err = upnpClient.ForwardPort(record.PortNumber, record.RuleName)
if err != nil {
SendErrorResponse(w, err.Error())
return
}
}
}
}
upnpRuntimeConfig.Enabled = enable
SaveRuntimeConfig()
}
}
func handleForwardPortEdit(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
port, err := PostInt(r, "port")
if err != nil {
SendErrorResponse(w, err.Error())
return
}
oldPort, err := PostInt(r, "oldPort")
if err != nil {
SendErrorResponse(w, err.Error())
return
}
name, err := PostPara(r, "name")
if err != nil {
SendErrorResponse(w, err.Error())
return
}
if port < 1 || port > 65535 {
SendErrorResponse(w, "invalid port number")
return
}
//Check if the old port exists
found := false
for _, record := range upnpRuntimeConfig.ForwardRules {
if record.PortNumber == oldPort {
found = true
break
}
}
if !found {
SendErrorResponse(w, "editing forward rule not found")
return
}
//Delete the old port forward
if oldPort != port && upnpClient != nil {
//Remove the port forward if UPnP client is available
err = upnpClient.ClosePort(oldPort)
if err != nil {
SendErrorResponse(w, err.Error())
return
}
}
//Remove from runtime config
for i, record := range upnpRuntimeConfig.ForwardRules {
if record.PortNumber == oldPort {
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules[:i], upnpRuntimeConfig.ForwardRules[i+1:]...)
break
}
}
//Create the new forward rule
if upnpClient != nil {
//Forward the port if UPnP client is available
err = upnpClient.ForwardPort(port, name)
if err != nil {
SendErrorResponse(w, err.Error())
return
}
}
//Add to runtime config
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules, &PortForwardRecord{
RuleName: name,
PortNumber: port,
})
//Save the runtime config
SaveRuntimeConfig()
SendOK(w)
}
}
// Remove a port forward
func handleForwardPortRemove(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
port, err := PostInt(r, "port")
if err != nil {
SendErrorResponse(w, err.Error())
return
}
if upnpClient != nil {
//Remove the port forward if UPnP client is available
err = upnpClient.ClosePort(port)
if err != nil {
SendErrorResponse(w, err.Error())
return
}
}
//Remove from runtime config
for i, record := range upnpRuntimeConfig.ForwardRules {
if record.PortNumber == port {
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules[:i], upnpRuntimeConfig.ForwardRules[i+1:]...)
break
}
}
//Save the runtime config
SaveRuntimeConfig()
SendOK(w)
}
}
// Handle the port forward operations
func handleForwardPort(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
// List all the forwarded ports
js, _ := json.Marshal(upnpRuntimeConfig.ForwardRules)
SendJSONResponse(w, string(js))
} else if r.Method == "POST" {
//Add a new port forward
port, err := PostInt(r, "port")
if err != nil {
SendErrorResponse(w, err.Error())
return
}
name, err := PostPara(r, "name")
if err != nil {
SendErrorResponse(w, err.Error())
return
}
if port < 1 || port > 65535 {
SendErrorResponse(w, "invalid port number")
return
}
if upnpClient != nil {
//Forward the port if UPnP client is available
err = upnpClient.ForwardPort(port, name)
if err != nil {
SendErrorResponse(w, err.Error())
return
}
}
//Add to runtime config
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules, &PortForwardRecord{
RuleName: name,
PortNumber: port,
})
//Save the runtime config
SaveRuntimeConfig()
SendOK(w)
}
}
/*
Network Utilities
*/
// Send JSON response, with an extra json header
func SendJSONResponse(w http.ResponseWriter, json string) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(json))
}
func SendErrorResponse(w http.ResponseWriter, errMsg string) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
}
func SendOK(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("\"OK\""))
}
// Get GET parameter
func GetPara(r *http.Request, key string) (string, error) {
// Get first value from the URL query
value := r.URL.Query().Get(key)
if len(value) == 0 {
return "", errors.New("invalid " + key + " given")
}
return value, nil
}
// Get GET paramter as boolean, accept 1 or true
func GetBool(r *http.Request, key string) (bool, error) {
x, err := GetPara(r, key)
if err != nil {
return false, err
}
// Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
case "1", "true", "on":
return true, nil
case "0", "false", "off":
return false, nil
}
return false, errors.New("invalid boolean given")
}
// Get POST parameter
func PostPara(r *http.Request, key string) (string, error) {
// Try to parse the form
if err := r.ParseForm(); err != nil {
return "", err
}
// Get first value from the form
x := r.Form.Get(key)
if len(x) == 0 {
return "", errors.New("invalid " + key + " given")
}
return x, nil
}
// Get POST paramter as boolean, accept 1 or true
func PostBool(r *http.Request, key string) (bool, error) {
x, err := PostPara(r, key)
if err != nil {
return false, err
}
// Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
case "1", "true", "on":
return true, nil
case "0", "false", "off":
return false, nil
}
return false, errors.New("invalid boolean given")
}
// Get POST paramter as int
func PostInt(r *http.Request, key string) (int, error) {
x, err := PostPara(r, key)
if err != nil {
return 0, err
}
x = strings.TrimSpace(x)
rx, err := strconv.Atoi(x)
if err != nil {
return 0, err
}
return rx, nil
}

View File

@ -0,0 +1,13 @@
module plugins.zoraxy.aroz.org/zoraxy/upnp
go 1.23.6
require gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6
require (
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // indirect
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect
golang.org/x/text v0.3.6 // indirect
)

View File

@ -0,0 +1,17 @@
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 h1:dizWJqTWjwyD8KGcMOwgrkqu1JIkofYgKkmDeNE7oAs=
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA=
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6 h1:WKij6HF8ECp9E7K0E44dew9NrRDGiNR5u4EFsXnJUx4=
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6/go.mod h1:vhrHTGDh4YR7wK8Z+kRJ+x8SF/6RUM3Vb64Si5FD0L8=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

View File

@ -0,0 +1,194 @@
package main
import (
"embed"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"time"
"plugins.zoraxy.aroz.org/zoraxy/upnp/mod/upnpc"
plugin "plugins.zoraxy.aroz.org/zoraxy/upnp/mod/zoraxy_plugin"
)
const (
PLUGIN_ID = "org.aroz.zoraxy.plugins.upnp"
UI_PATH = "/ui"
WEB_ROOT = "/www"
CONFIG_FILE = "upnp.json"
AUTO_RENEW_INTERVAL = 12 * 60 * 60 // 12 hours
)
type PortForwardRecord struct {
RuleName string
PortNumber int
}
type UPnPConfig struct {
ForwardRules []*PortForwardRecord
Enabled bool
}
//go:embed www/*
var content embed.FS
// Runtime variables
var (
upnpRouterExists bool = false
upnpRuntimeConfig *UPnPConfig = &UPnPConfig{
ForwardRules: []*PortForwardRecord{},
Enabled: false,
}
upnpClient *upnpc.UPnPClient = nil
renewTickerStop chan bool
)
func main() {
//Handle introspect
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
ID: PLUGIN_ID,
Name: "UPnP Forwarder",
Author: "aroz.org",
AuthorContact: "https://github.com/aroz-online",
Description: "A UPnP Port Forwarder Plugin for Zoraxy",
URL: "https://github.com/aroz-online",
Type: plugin.PluginType_Utilities,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
UIPath: UI_PATH,
})
if err != nil {
//Terminate or enter standalone mode here
fmt.Println("This is a plugin for Zoraxy and should not be run standalone\n Visit zoraxy.aroz.org to download Zoraxy.")
panic(err)
}
//Read the configuration from file
if _, err := os.Stat(CONFIG_FILE); os.IsNotExist(err) {
err = os.WriteFile(CONFIG_FILE, []byte("{}"), 0644)
if err != nil {
panic(err)
}
}
cfgBytes, err := os.ReadFile(CONFIG_FILE)
if err != nil {
panic(err)
}
//Load the configuration
err = json.Unmarshal(cfgBytes, &upnpRuntimeConfig)
if err != nil {
panic(err)
}
//Start upnp client and auto-renew ticker
go func() {
TryStartUPnPClient()
}()
//Serve the plugin UI
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
// For debugging, use the following line instead
//embedWebRouter := plugin.NewPluginFileSystemUIRouter(PLUGIN_ID, "."+WEB_ROOT, UI_PATH)
//embedWebRouter.EnableDebug = true
embedWebRouter.RegisterTerminateHandler(func() {
if renewTickerStop != nil {
renewTickerStop <- true
}
// Do cleanup here if needed
upnpClient.Close()
}, nil)
embedWebRouter.AttachHandlerToMux(nil)
//Serve the API
RegisterAPIs()
//Start the IO server
fmt.Println("UPnP Forwarder started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
if err != nil {
panic(err)
}
}
// RegisterAPIs registers the APIs for the plugin
func RegisterAPIs() {
http.HandleFunc(UI_PATH+"/api/usable", handleUsableState)
http.HandleFunc(UI_PATH+"/api/enable", handleEnableState)
http.HandleFunc(UI_PATH+"/api/forward", handleForwardPort)
http.HandleFunc(UI_PATH+"/api/edit", handleForwardPortEdit)
http.HandleFunc(UI_PATH+"/api/remove", handleForwardPortRemove)
}
// TryStartUPnPClient tries to start the UPnP client
func TryStartUPnPClient() {
if renewTickerStop != nil {
renewTickerStop <- true
}
// Create UPnP client
upnpClient, err := upnpc.NewUPNPClient()
if err != nil {
upnpRouterExists = false
upnpRuntimeConfig.Enabled = false
fmt.Println("UPnP router not found")
SaveRuntimeConfig()
return
}
upnpRouterExists = true
//Check if the client is enabled by default
if upnpRuntimeConfig.Enabled {
// Forward all the ports
for _, rule := range upnpRuntimeConfig.ForwardRules {
err = upnpClient.ForwardPort(rule.PortNumber, rule.RuleName)
if err != nil {
fmt.Println("Unable to forward port", rule.PortNumber, ":", err)
return
}
}
}
// Start the auto-renew ticker
_, renewTickerStop = SetupAutoRenewTicker()
}
// SetupAutoRenewTicker sets up a ticker for auto-renewing the port forwarding rules
func SetupAutoRenewTicker() (*time.Ticker, chan bool) {
ticker := time.NewTicker(AUTO_RENEW_INTERVAL * time.Second)
closeChan := make(chan bool)
go func() {
for {
select {
case <-closeChan:
ticker.Stop()
return
case <-ticker.C:
if upnpClient != nil {
upnpClient.RenewForwardRules()
}
}
}
}()
return ticker, closeChan
}
// SaveRuntimeConfig saves the runtime configuration to file
func SaveRuntimeConfig() error {
cfgBytes, err := json.Marshal(upnpRuntimeConfig)
if err != nil {
return err
}
err = os.WriteFile(CONFIG_FILE, cfgBytes, 0644)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,135 @@
package upnpc
import (
"errors"
"fmt"
"sync"
"time"
"gitlab.com/NebulousLabs/go-upnp"
)
/*
uPNP Module
This module handles uPNP Connections to the gateway router and create a port forward entry
for the host system at the given port (set with -port paramter)
*/
type UPnPClient struct {
Connection *upnp.IGD //UPnP conenction object
ExternalIP string //Storage of external IP address
RequiredPorts []int //All the required ports will be recored
PolicyNames sync.Map //Name for the required port nubmer
}
// NewUPNPClient creates a new UPnPClient object
func NewUPNPClient() (*UPnPClient, error) {
//Create uPNP forwarding in the NAT router
fmt.Println("Discovering UPnP router in Local Area Network...")
d, err := upnp.Discover()
if err != nil {
return &UPnPClient{}, err
}
// discover external IP
ip, err := d.ExternalIP()
if err != nil {
return &UPnPClient{}, err
}
//Create the final obejcts
newUPnPObject := &UPnPClient{
Connection: d,
ExternalIP: ip,
RequiredPorts: []int{},
}
return newUPnPObject, nil
}
// ForwardPort forwards a port to the host
func (u *UPnPClient) ForwardPort(portNumber int, ruleName string) error {
fmt.Println("UPnP forwarding new port: ", portNumber, "for "+ruleName+" service")
//Check if port already forwarded
_, ok := u.PolicyNames.Load(portNumber)
if ok {
//Port already forward. Ignore this request
return errors.New("port already forwarded")
}
// forward a port
err := u.Connection.Forward(uint16(portNumber), ruleName)
if err != nil {
return err
}
u.RequiredPorts = append(u.RequiredPorts, portNumber)
u.PolicyNames.Store(portNumber, ruleName)
return nil
}
// ClosePort closes the port forwarding
func (u *UPnPClient) ClosePort(portNumber int) error {
//Check if port is opened
portOpened := false
newRequiredPort := []int{}
for _, thisPort := range u.RequiredPorts {
if thisPort != portNumber {
newRequiredPort = append(newRequiredPort, thisPort)
} else {
portOpened = true
}
}
if portOpened {
//Update the port list
u.RequiredPorts = newRequiredPort
// Close the port
fmt.Println("Closing UPnP Port Forward: ", portNumber)
err := u.Connection.Clear(uint16(portNumber))
//Delete the name registry
u.PolicyNames.Delete(portNumber)
if err != nil {
fmt.Println(err)
return err
}
}
return nil
}
// Renew forward rules, prevent router lease time from flushing the Upnp config
func (u *UPnPClient) RenewForwardRules() {
if u.Connection == nil {
//UPnP router gone
return
}
portsToRenew := u.RequiredPorts
for _, thisPort := range portsToRenew {
ruleName, ok := u.PolicyNames.Load(thisPort)
if !ok {
continue
}
u.ClosePort(thisPort)
time.Sleep(100 * time.Millisecond)
u.ForwardPort(thisPort, ruleName.(string))
}
fmt.Println("UPnP Port Forward rule renew completed")
}
func (u *UPnPClient) Close() error {
//Shutdown the default UPnP Object
if u != nil {
for _, portNumber := range u.RequiredPorts {
err := u.Connection.Clear(uint16(portNumber))
if err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,19 @@
# Zoraxy Plugin
## Overview
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
## Instructions
1. **Copy the Module:**
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
2. **Include the Structure:**
- Ensure that you maintain the directory structure and file organization as provided in this module.
3. **Modify as Needed:**
- Customize the copied module to implement the desired functionality for your plugin.
## Directory Structure
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages

View File

@ -0,0 +1,145 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"os"
"strings"
"time"
)
type PluginUiDebugRouter struct {
PluginID string //The ID of the plugin
TargetDir string //The directory where the UI files are stored
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
// The targetDir is the directory where the UI files are stored (e.g. ./www)
// The handlerPrefix is the prefix of the handler used to route this router
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
// All prefix should not end with a slash
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
//Make sure all prefix are in /prefix format
if !strings.HasPrefix(handlerPrefix, "/") {
handlerPrefix = "/" + handlerPrefix
}
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
//Return the PluginUiRouter
return &PluginUiDebugRouter{
PluginID: pluginID,
TargetDir: targetDir,
HandlerPrefix: handlerPrefix,
}
}
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
//Get the CSRF token from header
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
if csrfToken == "" {
csrfToken = "missing-csrf-token"
}
//Return the middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the request is for an HTML file
if strings.HasSuffix(r.URL.Path, ".html") {
//Read the target file from file system
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
//Check if the request is for a directory
//Check if the directory has an index.html file
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
if _, err := os.Stat(targetFilePath); err == nil {
//Serve the index.html file
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
}
}
//Call the next handler
fsHandler.ServeHTTP(w, r)
})
}
// GetHttpHandler returns the http.Handler for the PluginUiRouter
func (p *PluginUiDebugRouter) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Remove the plugin UI handler path prefix
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL.Path = rewrittenURL
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//Serve the file from the file system
fsHandler := http.FileServer(http.Dir(p.TargetDir))
// Replace {{csrf_token}} with the actual CSRF token and serve the file
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// Attach the file system UI handler to the target http.ServeMux
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@ -0,0 +1,162 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
/*
Dynamic Path Handler
*/
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
/*
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
You can decide to accept or skip the request based on the request header and paths
*/
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
if !strings.HasSuffix(sniff_ingress, "/") {
sniff_ingress = sniff_ingress + "/"
}
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
}
// Decode the request payload
jsonBytes, err := io.ReadAll(r.Body)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error reading request body:", err)
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
payload, err := DecodeForwardRequestPayload(jsonBytes)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error decoding request payload:", err)
fmt.Print("Payload: ")
fmt.Println(string(jsonBytes))
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Get the forwarded request UUID
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
payload.requestUUID = forwardUUID
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("SKIP"))
}
}))
}
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
if rewrittenURL == "" {
rewrittenURL = "/"
}
if !strings.HasPrefix(rewrittenURL, "/") {
rewrittenURL = "/" + rewrittenURL
}
r.RequestURI = rewrittenURL
handlefunc(w, r)
}))
}
/*
Sniffing and forwarding
The following functions are here to help with
sniffing and forwarding requests to the dynamic
router.
*/
// A custom request object to be used in the dynamic sniffing
type DynamicSniffForwardRequest struct {
Method string `json:"method"`
Hostname string `json:"hostname"`
URL string `json:"url"`
Header map[string][]string `json:"header"`
RemoteAddr string `json:"remote_addr"`
Host string `json:"host"`
RequestURI string `json:"request_uri"`
Proto string `json:"proto"`
ProtoMajor int `json:"proto_major"`
ProtoMinor int `json:"proto_minor"`
/* Internal use */
rawRequest *http.Request `json:"-"`
requestUUID string `json:"-"`
}
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
return DynamicSniffForwardRequest{
Method: r.Method,
Hostname: r.Host,
URL: r.URL.String(),
Header: r.Header,
RemoteAddr: r.RemoteAddr,
Host: r.Host,
RequestURI: r.RequestURI,
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
rawRequest: r,
}
}
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
var payload DynamicSniffForwardRequest
err := json.Unmarshal(jsonBytes, &payload)
if err != nil {
return DynamicSniffForwardRequest{}, err
}
return payload, nil
}
// GetRequest returns the original http.Request object, for debugging purposes
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
return dsfr.rawRequest
}
// GetRequestUUID returns the request UUID
// if this UUID is empty string, that might indicate the request
// is not coming from the dynamic router
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
return dsfr.requestUUID
}

View File

@ -0,0 +1,156 @@
package zoraxy_plugin
import (
"embed"
"fmt"
"io/fs"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type PluginUiRouter struct {
PluginID string //The ID of the plugin
TargetFs *embed.FS //The embed.FS where the UI files are stored
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored
// The targetFsPrefix should be relative to the root of the embed.FS
// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS
// The handlerPrefix is the prefix of the handler used to route this router
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
// All prefix should not end with a slash
func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
//Make sure all prefix are in /prefix format
if !strings.HasPrefix(targetFsPrefix, "/") {
targetFsPrefix = "/" + targetFsPrefix
}
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
if !strings.HasPrefix(handlerPrefix, "/") {
handlerPrefix = "/" + handlerPrefix
}
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
//Return the PluginUiRouter
return &PluginUiRouter{
PluginID: pluginID,
TargetFs: targetFs,
TargetFsPrefix: targetFsPrefix,
HandlerPrefix: handlerPrefix,
}
}
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
//Get the CSRF token from header
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
if csrfToken == "" {
csrfToken = "missing-csrf-token"
}
//Return the middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the request is for an HTML file
if strings.HasSuffix(r.URL.Path, ".html") {
//Read the target file from embed.FS
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
// Check if the directory has an index.html file
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
if err == nil {
body := string(indexFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
}
}
//Call the next handler
fsHandler.ServeHTTP(w, r)
})
}
// GetHttpHandler returns the http.Handler for the PluginUiRouter
func (p *PluginUiRouter) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Remove the plugin UI handler path prefix
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL, _ = url.Parse(rewrittenURL)
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//Serve the file from the embed.FS
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
if err != nil {
fmt.Println(err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Replace {{csrf_token}} with the actual CSRF token and serve the file
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// Attach the embed UI handler to the target http.ServeMux
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@ -0,0 +1,105 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"sort"
"strings"
)
type PathRouter struct {
enableDebugPrint bool
pathHandlers map[string]http.Handler
defaultHandler http.Handler
}
// NewPathRouter creates a new PathRouter
func NewPathRouter() *PathRouter {
return &PathRouter{
enableDebugPrint: false,
pathHandlers: make(map[string]http.Handler),
}
}
// RegisterPathHandler registers a handler for a path
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
path = strings.TrimSuffix(path, "/")
p.pathHandlers[path] = handler
}
// RemovePathHandler removes a handler for a path
func (p *PathRouter) RemovePathHandler(path string) {
delete(p.pathHandlers, path)
}
// SetDefaultHandler sets the default handler for the router
// This handler will be called if no path handler is found
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
p.defaultHandler = handler
}
// SetDebugPrintMode sets the debug print mode
func (p *PathRouter) SetDebugPrintMode(enable bool) {
p.enableDebugPrint = enable
}
// StartStaticCapture starts the static capture ingress
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p.staticCaptureServeHTTP(w, r)
}))
}
// staticCaptureServeHTTP serves the static capture path using user defined handler
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
capturePath := r.Header.Get("X-Zoraxy-Capture")
if capturePath != "" {
if p.enableDebugPrint {
fmt.Printf("Using capture path: %s\n", capturePath)
}
originalURI := r.Header.Get("X-Zoraxy-Uri")
r.URL.Path = originalURI
if handler, ok := p.pathHandlers[capturePath]; ok {
handler.ServeHTTP(w, r)
return
}
}
p.defaultHandler.ServeHTTP(w, r)
}
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
if p.enableDebugPrint {
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
keys := make([]string, 0, len(r.Header))
for key := range r.Header {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
for _, value := range r.Header[key] {
fmt.Printf("%s: %s\n", key, value)
}
}
fmt.Printf("\n\n**Request Details**\n\n")
fmt.Printf("Method: %s\n", r.Method)
fmt.Printf("URL: %s\n", r.URL.String())
fmt.Printf("Proto: %s\n", r.Proto)
fmt.Printf("Host: %s\n", r.Host)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
fmt.Printf("ContentLength: %d\n", r.ContentLength)
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
fmt.Printf("Close: %v\n", r.Close)
fmt.Printf("Form: %v\n", r.Form)
fmt.Printf("PostForm: %v\n", r.PostForm)
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
fmt.Printf("Trailer: %v\n", r.Trailer)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
}
}

View File

@ -0,0 +1,176 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"os"
"strings"
)
/*
Plugins Includes.go
This file is copied from Zoraxy source code
You can always find the latest version under mod/plugins/includes.go
Usually this file are backward compatible
*/
type PluginType int
const (
PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic
PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore
)
type StaticCaptureRule struct {
CapturePath string `json:"capture_path"`
//To be expanded
}
type ControlStatusCode int
const (
ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic
ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic
ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error
)
type SubscriptionEvent struct {
EventName string `json:"event_name"`
EventSource string `json:"event_source"`
Payload string `json:"payload"` //Payload of the event, can be empty
}
type RuntimeConstantValue struct {
ZoraxyVersion string `json:"zoraxy_version"`
ZoraxyUUID string `json:"zoraxy_uuid"`
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
/*
IntroSpect Payload
When the plugin is initialized with -introspect flag,
the plugin shell return this payload as JSON and exit
*/
type IntroSpect struct {
/* Plugin metadata */
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
Name string `json:"name"` //Name of your plugin
Author string `json:"author"` //Author name of your plugin
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
Description string `json:"description"` //Description of your plugin
URL string `json:"url"` //URL of your plugin
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
VersionMajor int `json:"version_major"` //Major version of your plugin
VersionMinor int `json:"version_minor"` //Minor version of your plugin
VersionPatch int `json:"version_patch"` //Patch version of your plugin
/*
Endpoint Settings
*/
/*
Static Capture Settings
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
This is faster than dynamic capture, but less flexible
*/
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
/*
Dynamic Capture Settings
Once plugin is enabled, these rules will be captured and forward to plugin sniff
if the plugin sniff returns 280, the traffic will be captured
otherwise, the traffic will be forwarded to the next plugin
This is slower than static capture, but more flexible
*/
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
/* UI Path for your plugin */
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
/* Subscriptions Settings */
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
}
/*
ServeIntroSpect Function
This function will check if the plugin is initialized with -introspect flag,
if so, it will print the intro spect and exit
Place this function at the beginning of your plugin main function
*/
func ServeIntroSpect(pluginSpect *IntroSpect) {
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
//Print the intro spect and exit
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
fmt.Println(string(jsonData))
os.Exit(0)
}
}
/*
ConfigureSpec Payload
Zoraxy will start your plugin with -configure flag,
the plugin shell read this payload as JSON and configure itself
by the supplied values like starting a web server at given port
that listens to 127.0.0.1:port
*/
type ConfigureSpec struct {
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
//To be expanded
}
/*
RecvExecuteConfigureSpec Function
This function will read the configure spec from Zoraxy
and return the ConfigureSpec object
Place this function after ServeIntroSpect function in your plugin main function
*/
func RecvConfigureSpec() (*ConfigureSpec, error) {
for i, arg := range os.Args {
if strings.HasPrefix(arg, "-configure=") {
var configSpec ConfigureSpec
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
return nil, err
}
return &configSpec, nil
} else if arg == "-configure" {
var configSpec ConfigureSpec
var nextArg string
if len(os.Args) > i+1 {
nextArg = os.Args[i+1]
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("No port specified after -configure flag")
}
return &configSpec, nil
}
}
return nil, fmt.Errorf("No -configure flag found")
}
/*
ServeAndRecvSpec Function
This function will serve the intro spect and return the configure spec
See the ServeIntroSpect and RecvConfigureSpec for more details
*/
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
ServeIntroSpect(pluginSpect)
return RecvConfigureSpec()
}

View File

@ -0,0 +1 @@
{"ForwardRules":[],"Enabled":false}

View File

@ -0,0 +1,302 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- CSRF token, if your plugin need to make POST request to backend -->
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<title>UPnP Port Forwarder | Zoraxy</title>
<link rel="stylesheet" href="/script/semantic/semantic.min.css">
<script src="/script/jquery-3.6.0.min.js"></script>
<script src="/script/semantic/semantic.min.js"></script>
<script src="/script/utils.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/main.css">
<style>
body {
background:none;
}
</style>
</head>
<body>
<!-- Dark theme script must be included after body tag-->
<link rel="stylesheet" href="/darktheme.css">
<script src="/script/darktheme.js"></script>
<div id="upnpForwarder" class="standardContainer">
<div id="upnpRouterNotFoundWarning" class="ui basic segment" style="display: none;">
<h2>UPnP Port Forwarder</h2>
<p>Port forward using UPnP protocol, only works with some of the supported gateway routers</p>
</div>
<div class="ui basic segment">
<div class="ui message">
<div class="header"><i class="yellow exclamation triangle icon"></i> UPnP Gateway Not Found</div>
<p>No UPnP router found in network. Please ensure your router supports UPnP and is enabled.</p>
<button id="retryBtn" onclick="searchUpnpRouter();" class="ui basic small button"><i class="green refresh icon"></i> Search again</button>
</div>
<div class="ui basic segment">
<h3 class="ui header">UPnP Port Forwarder</h3>
<div class="ui toggle checkbox">
<input type="checkbox" id="upnpToggle" onchange="toggleUpnpState();">
<label>Enable UPnP Forwarding</label>
</div>
</div>
<div class="ui divider"></div>
<table class="ui celled table">
<thead>
<tr>
<th>Rule Name</th>
<th>Forwarded Port</th>
<th>Action</th>
</tr>
</thead>
<tbody id="forwardList">
<tr>
<td>Example Rule</td>
<td>8080</td>
<td>
<button class="ui button">Edit</button>
<button class="ui button">Remove</button>
</td>
</tr>
</tbody>
</table>
<div class="ui divider"></div>
<div>
<h4>Port Forward Rules</h4>
<form class="ui form" id="addRuleForm">
<div class="field">
<label>Rule Name</label>
<input type="text" name="ruleName" placeholder="Rule Name">
</div>
<div class="field">
<label>Port</label>
<input type="number" name="port" placeholder="Port" min="1" max="65535">
</div>
<button onclick="handleAddForward(event);" class="ui small basic button"><i class="ui blue add icon"></i> Add Rule</button>
</form>
</div>
</div>
</div>
<script>
function toggleUpnpState() {
let isChecked = $("#upnpToggle").prop("checked");
$.cjax({
url: './api/toggle',
method: "POST",
data: {
enable: isChecked
},
success: function(data) {
if (data.error != undefined) {
// Error
parent.msgbox(data.error, false);
} else {
// Success
parent.msgbox("UPnP Forwarding " + (isChecked ? "enabled" : "disabled"), true);
}
}
});
}
function initUPnPEnableState(){
$.cjax({
url: './api/enable',
success: function(data) {
if (data == true){
//Upnp forwarding enabled
$("#upnpToggle").prop("checked", true);
}else{
//Upnp forwarding disabled
$("#upnpToggle").prop("checked", false);
}
}
});
}
initUPnPEnableState();
function searchUpnpRouter(){
$("#retryBtn").addClass("loading");
parent.msgbox("Searching for UPnP router (will take a few minutes)...", true);
$.cjax({
url: './api/usable',
method: "POST",
success: function(data) {
if (data.error != undefined){
//Not found
parent.msgbox("UPnP router not found", false);
}else{
//Found
parent.msgbox("UPnP router discovered", true);
}
initUpnpUsableState();
$("#retryBtn").removeClass("loading");
}
});
}
//Check if UPnP is usable
function initUpnpUsableState(){
$.cjax({
url: './api/usable',
success: function(data) {
if (data == true){
//Upnp router found in network, enable the page
$('#upnpRouterNotFoundWarning').hide();
}else{
//No upnp router found in network, disable the page
$('#upnpForwarder').addClass('disabled');
$('#upnpRouterNotFoundWarning').show();
}
}
});
}
function handleAddForward(event){
event.preventDefault();
let ruleName = $("#addRuleForm input[name='ruleName']").val();
let port = $("#addRuleForm input[name='port']").val();
if (ruleName == "" || port == ""){
parent.msgbox("Please fill in all fields", false);
return;
}
$.cjax({
url: './api/forward',
method: "POST",
data: {
name: ruleName,
port: port
},
success: function(data) {
if (data.error != undefined){
//Error
parent.msgbox(data.error, false);
}else{
//Success
parent.msgbox("Rule added successfully", true);
initForwardList();
}
$("#addRuleForm input[name='ruleName']").val('');
$("#addRuleForm input[name='port']").val('');
}
});
}
function handleRemoveForward(portNo){
$.cjax({
url: './api/remove',
method: "POST",
data: {
port: portNo
},
success: function(data) {
if (data.error != undefined){
//Error
parent.msgbox(data.error, false);
}else{
//Success
parent.msgbox("Rule removed successfully", true);
initForwardList();
}
}
});
}
function editForwardRule(row){
let ruleName = $(row).closest('tr').find('td:eq(0)').text();
let portNumber = $(row).closest('tr').find('td:eq(1)').text();
$(row).closest('tr').html(`
<td>
<div class="ui fluid input">
<input type="text" value="${ruleName}" onkeypress="if(event.key === 'Enter') $(this).closest('tr').find('td:eq(1) input').focus();">
</div>
</td>
<td>
<div class="ui fluid input">
<input type="number" value="${portNumber}" class="ui input" min="1" max="65535" onkeypress="if(event.key === 'Enter') saveForwardRule(this, '${portNumber}');">
</div>
</td>
<td>
<button onclick="saveForwardRule(this, '${portNumber}');" class="ui basic small circular icon button"><i class="ui green save icon"></i></button>
<button onclick="cancelEditForwardRule(this, '${ruleName}', '${portNumber}');" class="ui basic small circular icon button"><i class="ui cancel icon"></i></button>
</td>
`);
}
function cancelEditForwardRule(){
initForwardList();
}
function saveForwardRule(row, portNo){
let ruleName = $(row).closest('tr').find('td:eq(0) input').val();
let newPortNo = $(row).closest('tr').find('td:eq(1) input').val();
if (ruleName == "" || newPortNo == ""){
parent.msgbox("Please fill in all fields", false);
return;
}
$.cjax({
url: './api/edit',
method: "POST",
data: {
name: ruleName,
port: newPortNo,
oldPort: portNo
},
success: function(data) {
if (data.error != undefined){
//Error
parent.msgbox(data.error, false);
}else{
//Success
parent.msgbox("Rule updated successfully", true);
initForwardList();
}
}
});
}
//Load forward list
function initForwardList(){
$("#forwardList").html('<tr><td colspan="3"> <i class="ui loading spinner icon"></i> Loading...</tr>');
$.cjax({
url: './api/forward',
success: function(data) {
if (data.error != undefined){
//Error
parent.msgbox(data.error, false);
}else{
//Success
$("#forwardList").empty();
let rows = '';
data.forEach(row => {
$("#forwardList").append(`
<tr>
<td>${row.RuleName}</td>
<td>${row.PortNumber}</td>
<td>
<button onclick="editForwardRule(this);" class="ui basic small circular icon button"><i class="ui edit icon"></i></button>
<button onclick="handleRemoveForward(${row.PortNumber});" class="ui basic small red circular icon button"><i class="ui red trash icon"></i></button>
</td>
</tr>
`);
});
if (data == null || data.length == 0){
//No rules
$("#forwardList").append(`
<tr>
<td colspan="3"><i class="ui green check circle icon"></i> No running port forward rules</td>
</tr>`);
}
}
}
});
}
initUpnpUsableState();
initForwardList();
</script>
</body>
</html>

View File

@ -0,0 +1,11 @@
## Global Area Network Plugin
This plugin implements a user interface for ZeroTier Network Controller in Zoraxy
## License
AGPL

View File

@ -0,0 +1,11 @@
module aroz.org/zoraxy/ztnc
go 1.23.6
require (
github.com/boltdb/bolt v1.3.1
github.com/syndtr/goleveldb v1.0.0
golang.org/x/sys v0.30.0
)
require github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect

View File

@ -0,0 +1,30 @@
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

View File

@ -0,0 +1,83 @@
package main
import (
"fmt"
"net/http"
"strconv"
"embed"
"aroz.org/zoraxy/ztnc/mod/database"
"aroz.org/zoraxy/ztnc/mod/ganserv"
plugin "aroz.org/zoraxy/ztnc/mod/zoraxy_plugin"
)
const (
PLUGIN_ID = "org.aroz.zoraxy.ztnc"
UI_RELPATH = "/ui"
EMBED_FS_ROOT = "/web"
DB_FILE_PATH = "ztnc.db"
AUTH_TOKEN_PATH = "./authtoken.secret"
)
//go:embed web/*
var content embed.FS
var (
sysdb *database.Database
ganManager *ganserv.NetworkManager
)
func main() {
// Serve the plugin intro spect
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
ID: PLUGIN_ID,
Name: "ztnc",
Author: "aroz.org",
AuthorContact: "zoraxy.aroz.org",
Description: "UI for ZeroTier Network Controller",
URL: "https://zoraxy.aroz.org",
Type: plugin.PluginType_Utilities,
VersionMajor: 1,
VersionMinor: 0,
VersionPatch: 0,
// As this is a utility plugin, we don't need to capture any traffic
// but only serve the UI, so we set the UI (relative to the plugin path) to "/ui/" to match the HTTP Handler
UIPath: UI_RELPATH,
})
if err != nil {
//Terminate or enter standalone mode here
panic(err)
}
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
uiRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, EMBED_FS_ROOT, UI_RELPATH)
uiRouter.EnableDebug = true
// Register the shutdown handler
uiRouter.RegisterTerminateHandler(func() {
// Do cleanup here if needed
if sysdb != nil {
sysdb.Close()
}
fmt.Println("ztnc Exited")
}, nil)
// This will serve the index.html file embedded in the binary
targetHandler := uiRouter.Handler()
http.Handle(UI_RELPATH+"/", targetHandler)
// Start the GAN Network Controller
err = startGanNetworkController()
if err != nil {
panic(err)
}
// Initiate the API endpoints
initApiEndpoints()
// Start the HTTP server, only listen to loopback interface
fmt.Println("Plugin UI server started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port) + UI_RELPATH)
http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
}

View File

@ -0,0 +1,146 @@
package database
/*
ArOZ Online Database Access Module
author: tobychui
This is an improved Object oriented base solution to the original
aroz online database script.
*/
import (
"log"
"runtime"
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
)
type Database struct {
Db interface{} //This will be nil on openwrt, leveldb.DB on x64 platforms or bolt.DB on other platforms
BackendType dbinc.BackendType
Backend dbinc.Backend
}
func NewDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
if runtime.GOARCH == "riscv64" {
log.Println("RISCV hardware detected, ignoring the backend type and using FS emulated database")
}
return newDatabase(dbfile, backendType)
}
// Get the recommended backend type for the current system
func GetRecommendedBackendType() dbinc.BackendType {
//Check if the system is running on RISCV hardware
if runtime.GOARCH == "riscv64" {
//RISCV hardware, currently only support FS emulated database
return dbinc.BackendFSOnly
} else if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
//Powerful hardware
return dbinc.BackendBoltDB
//return dbinc.BackendLevelDB
}
//Default to BoltDB, the safest option
return dbinc.BackendBoltDB
}
/*
Create / Drop a table
Usage:
err := sysdb.NewTable("MyTable")
err := sysdb.DropTable("MyTable")
*/
// Create a new table
func (d *Database) NewTable(tableName string) error {
return d.newTable(tableName)
}
// Check is table exists
func (d *Database) TableExists(tableName string) bool {
return d.tableExists(tableName)
}
// Drop the given table
func (d *Database) DropTable(tableName string) error {
return d.dropTable(tableName)
}
/*
Write to database with given tablename and key. Example Usage:
type demo struct{
content string
}
thisDemo := demo{
content: "Hello World",
}
err := sysdb.Write("MyTable", "username/message",thisDemo);
*/
func (d *Database) Write(tableName string, key string, value interface{}) error {
return d.write(tableName, key, value)
}
/*
Read from database and assign the content to a given datatype. Example Usage:
type demo struct{
content string
}
thisDemo := new(demo)
err := sysdb.Read("MyTable", "username/message",&thisDemo);
*/
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
return d.read(tableName, key, assignee)
}
/*
Check if a key exists in the database table given tablename and key
if sysdb.KeyExists("MyTable", "username/message"){
log.Println("Key exists")
}
*/
func (d *Database) KeyExists(tableName string, key string) bool {
return d.keyExists(tableName, key)
}
/*
Delete a value from the database table given tablename and key
err := sysdb.Delete("MyTable", "username/message");
*/
func (d *Database) Delete(tableName string, key string) error {
return d.delete(tableName, key)
}
/*
//List table example usage
//Assume the value is stored as a struct named "groupstruct"
entries, err := sysdb.ListTable("test")
if err != nil {
panic(err)
}
for _, keypairs := range entries{
log.Println(string(keypairs[0]))
group := new(groupstruct)
json.Unmarshal(keypairs[1], &group)
log.Println(group);
}
*/
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
return d.listTable(tableName)
}
/*
Close the database connection
*/
func (d *Database) Close() {
d.close()
}

View File

@ -0,0 +1,70 @@
//go:build !mipsle && !riscv64
// +build !mipsle,!riscv64
package database
import (
"errors"
"aroz.org/zoraxy/ztnc/mod/database/dbbolt"
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
"aroz.org/zoraxy/ztnc/mod/database/dbleveldb"
)
func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
if backendType == dbinc.BackendFSOnly {
return nil, errors.New("Unsupported backend type for this platform")
}
if backendType == dbinc.BackendLevelDB {
db, err := dbleveldb.NewDB(dbfile)
return &Database{
Db: nil,
BackendType: backendType,
Backend: db,
}, err
}
db, err := dbbolt.NewBoltDatabase(dbfile)
return &Database{
Db: nil,
BackendType: backendType,
Backend: db,
}, err
}
func (d *Database) newTable(tableName string) error {
return d.Backend.NewTable(tableName)
}
func (d *Database) tableExists(tableName string) bool {
return d.Backend.TableExists(tableName)
}
func (d *Database) dropTable(tableName string) error {
return d.Backend.DropTable(tableName)
}
func (d *Database) write(tableName string, key string, value interface{}) error {
return d.Backend.Write(tableName, key, value)
}
func (d *Database) read(tableName string, key string, assignee interface{}) error {
return d.Backend.Read(tableName, key, assignee)
}
func (d *Database) keyExists(tableName string, key string) bool {
return d.Backend.KeyExists(tableName, key)
}
func (d *Database) delete(tableName string, key string) error {
return d.Backend.Delete(tableName, key)
}
func (d *Database) listTable(tableName string) ([][][]byte, error) {
return d.Backend.ListTable(tableName)
}
func (d *Database) close() {
d.Backend.Close()
}

View File

@ -0,0 +1,196 @@
//go:build mipsle || riscv64
// +build mipsle riscv64
package database
import (
"encoding/json"
"errors"
"log"
"os"
"path/filepath"
"strings"
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
)
/*
OpenWRT or RISCV backend
For OpenWRT or RISCV platform, we will use the filesystem as the database backend
as boltdb or leveldb is not supported on these platforms, including boltDB and LevelDB
in conditional compilation will create a build error on these platforms
*/
func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
dbRootPath = "fsdb/" + dbRootPath
err := os.MkdirAll(dbRootPath, 0755)
if err != nil {
return nil, err
}
log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
return &Database{
Db: dbRootPath,
BackendType: dbinc.BackendFSOnly,
Backend: nil,
}, nil
}
func (d *Database) dump(filename string) ([]string, error) {
//Get all file objects from root
rootfiles, err := filepath.Glob(filepath.Join(d.Db.(string), "/*"))
if err != nil {
return []string{}, err
}
//Filter out the folders
rootFolders := []string{}
for _, file := range rootfiles {
if !isDirectory(file) {
rootFolders = append(rootFolders, filepath.Base(file))
}
}
return rootFolders, nil
}
func (d *Database) newTable(tableName string) error {
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if !fileExists(tablePath) {
return os.MkdirAll(tablePath, 0755)
}
return nil
}
func (d *Database) tableExists(tableName string) bool {
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if _, err := os.Stat(tablePath); errors.Is(err, os.ErrNotExist) {
return false
}
if !isDirectory(tablePath) {
return false
}
return true
}
func (d *Database) dropTable(tableName string) error {
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if d.tableExists(tableName) {
return os.RemoveAll(tablePath)
} else {
return errors.New("table not exists")
}
}
func (d *Database) write(tableName string, key string, value interface{}) error {
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
js, err := json.Marshal(value)
if err != nil {
return err
}
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
return os.WriteFile(filepath.Join(tablePath, key+".entry"), js, 0755)
}
func (d *Database) read(tableName string, key string, assignee interface{}) error {
if !d.keyExists(tableName, key) {
return errors.New("key not exists")
}
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
entryPath := filepath.Join(tablePath, key+".entry")
content, err := os.ReadFile(entryPath)
if err != nil {
return err
}
err = json.Unmarshal(content, &assignee)
return err
}
func (d *Database) keyExists(tableName string, key string) bool {
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
entryPath := filepath.Join(tablePath, key+".entry")
return fileExists(entryPath)
}
func (d *Database) delete(tableName string, key string) error {
if !d.keyExists(tableName, key) {
return errors.New("key not exists")
}
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
entryPath := filepath.Join(tablePath, key+".entry")
return os.Remove(entryPath)
}
func (d *Database) listTable(tableName string) ([][][]byte, error) {
if !d.tableExists(tableName) {
return [][][]byte{}, errors.New("table not exists")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
entries, err := filepath.Glob(filepath.Join(tablePath, "/*.entry"))
if err != nil {
return [][][]byte{}, err
}
var results [][][]byte = [][][]byte{}
for _, entry := range entries {
if !isDirectory(entry) {
//Read it
key := filepath.Base(entry)
key = strings.TrimSuffix(key, filepath.Ext(key))
key = strings.ReplaceAll(key, "-SLASH_SIGN-", "/")
bkey := []byte(key)
bval := []byte("")
c, err := os.ReadFile(entry)
if err != nil {
break
}
bval = c
results = append(results, [][]byte{bkey, bval})
}
}
return results, nil
}
func (d *Database) close() {
//Nothing to close as it is file system
}
func isDirectory(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
return false
}
return fileInfo.IsDir()
}
func fileExists(name string) bool {
_, err := os.Stat(name)
if err == nil {
return true
}
if errors.Is(err, os.ErrNotExist) {
return false
}
return false
}

View File

@ -0,0 +1,141 @@
package dbbolt
import (
"encoding/json"
"errors"
"github.com/boltdb/bolt"
)
type Database struct {
Db interface{} //This is the bolt database object
}
func NewBoltDatabase(dbfile string) (*Database, error) {
db, err := bolt.Open(dbfile, 0600, nil)
if err != nil {
return nil, err
}
return &Database{
Db: db,
}, err
}
// Create a new table
func (d *Database) NewTable(tableName string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
}
// Check is table exists
func (d *Database) TableExists(tableName string) bool {
return d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
if b == nil {
return errors.New("table not exists")
}
return nil
}) == nil
}
// Drop the given table
func (d *Database) DropTable(tableName string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
}
// Write to table
func (d *Database) Write(tableName string, key string, value interface{}) error {
jsonString, err := json.Marshal(value)
if err != nil {
return err
}
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
b := tx.Bucket([]byte(tableName))
err = b.Put([]byte(key), jsonString)
return err
})
return err
}
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
json.Unmarshal(v, &assignee)
return nil
})
return err
}
func (d *Database) KeyExists(tableName string, key string) bool {
resultIsNil := false
if !d.TableExists(tableName) {
//Table not exists. Do not proceed accessing key
//log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
return false
}
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
if v == nil {
resultIsNil = true
}
return nil
})
if err != nil {
return false
} else {
if resultIsNil {
return false
} else {
return true
}
}
}
func (d *Database) Delete(tableName string, key string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte(tableName)).Delete([]byte(key))
return nil
})
return err
}
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
var results [][][]byte
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
results = append(results, [][]byte{k, v})
}
return nil
})
return results, err
}
func (d *Database) Close() {
d.Db.(*bolt.DB).Close()
}

View File

@ -0,0 +1,67 @@
package dbbolt_test
import (
"os"
"testing"
"aroz.org/zoraxy/ztnc/mod/database/dbbolt"
)
func TestNewBoltDatabase(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
if db.Db == nil {
t.Fatalf("Expected non-nil database object")
}
}
func TestNewTable(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
err = db.NewTable("testTable")
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
}
func TestTableExists(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
tableName := "testTable"
err = db.NewTable(tableName)
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
exists := db.TableExists(tableName)
if !exists {
t.Fatalf("Expected table %s to exist", tableName)
}
nonExistentTable := "nonExistentTable"
exists = db.TableExists(nonExistentTable)
if exists {
t.Fatalf("Expected table %s to not exist", nonExistentTable)
}
}

View File

@ -0,0 +1,39 @@
package dbinc
/*
dbinc is the interface for all database backend
*/
type BackendType int
const (
BackendBoltDB BackendType = iota //Default backend
BackendFSOnly //OpenWRT or RISCV backend
BackendLevelDB //LevelDB backend
BackEndAuto = BackendBoltDB
)
type Backend interface {
NewTable(tableName string) error
TableExists(tableName string) bool
DropTable(tableName string) error
Write(tableName string, key string, value interface{}) error
Read(tableName string, key string, assignee interface{}) error
KeyExists(tableName string, key string) bool
Delete(tableName string, key string) error
ListTable(tableName string) ([][][]byte, error)
Close()
}
func (b BackendType) String() string {
switch b {
case BackendBoltDB:
return "BoltDB"
case BackendFSOnly:
return "File System Emulated Key-Value Store"
case BackendLevelDB:
return "LevelDB"
default:
return "Unknown"
}
}

View File

@ -0,0 +1,152 @@
package dbleveldb
import (
"encoding/json"
"log"
"path/filepath"
"strings"
"sync"
"time"
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
// Ensure the DB struct implements the Backend interface
var _ dbinc.Backend = (*DB)(nil)
type DB struct {
db *leveldb.DB
Table sync.Map //For emulating table creation
batch leveldb.Batch //Batch write
writeFlushTicker *time.Ticker //Ticker for flushing data into disk
writeFlushStop chan bool //Stop channel for write flush ticker
}
func NewDB(path string) (*DB, error) {
//If the path is not a directory (e.g. /tmp/dbfile.db), convert the filename to directory
if filepath.Ext(path) != "" {
path = strings.ReplaceAll(path, ".", "_")
}
db, err := leveldb.OpenFile(path, nil)
if err != nil {
return nil, err
}
thisDB := &DB{
db: db,
Table: sync.Map{},
batch: leveldb.Batch{},
}
//Create a ticker to flush data into disk every 1 seconds
writeFlushTicker := time.NewTicker(1 * time.Second)
writeFlushStop := make(chan bool)
go func() {
for {
select {
case <-writeFlushTicker.C:
if thisDB.batch.Len() == 0 {
//No flushing needed
continue
}
err = db.Write(&thisDB.batch, nil)
if err != nil {
log.Println("[LevelDB] Failed to flush data into disk: ", err)
}
thisDB.batch.Reset()
case <-writeFlushStop:
return
}
}
}()
thisDB.writeFlushTicker = writeFlushTicker
thisDB.writeFlushStop = writeFlushStop
return thisDB, nil
}
func (d *DB) NewTable(tableName string) error {
//Create a table entry in the sync.Map
d.Table.Store(tableName, true)
return nil
}
func (d *DB) TableExists(tableName string) bool {
_, ok := d.Table.Load(tableName)
return ok
}
func (d *DB) DropTable(tableName string) error {
d.Table.Delete(tableName)
iter := d.db.NewIterator(nil, nil)
defer iter.Release()
for iter.Next() {
key := iter.Key()
if filepath.Dir(string(key)) == tableName {
err := d.db.Delete(key, nil)
if err != nil {
return err
}
}
}
return nil
}
func (d *DB) Write(tableName string, key string, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
d.batch.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data)
return nil
}
func (d *DB) Read(tableName string, key string, assignee interface{}) error {
data, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
if err != nil {
return err
}
return json.Unmarshal(data, assignee)
}
func (d *DB) KeyExists(tableName string, key string) bool {
_, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
return err == nil
}
func (d *DB) Delete(tableName string, key string) error {
return d.db.Delete([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
}
func (d *DB) ListTable(tableName string) ([][][]byte, error) {
iter := d.db.NewIterator(util.BytesPrefix([]byte(tableName+"/")), nil)
defer iter.Release()
var result [][][]byte
for iter.Next() {
key := iter.Key()
//The key contains the table name as prefix. Trim it before returning
value := iter.Value()
result = append(result, [][]byte{[]byte(strings.TrimPrefix(string(key), tableName+"/")), value})
}
err := iter.Error()
if err != nil {
return nil, err
}
return result, nil
}
func (d *DB) Close() {
//Write the remaining data in batch back into disk
d.writeFlushStop <- true
d.writeFlushTicker.Stop()
d.db.Write(&d.batch, nil)
d.db.Close()
}

View File

@ -0,0 +1,141 @@
package dbleveldb_test
import (
"os"
"testing"
"aroz.org/zoraxy/ztnc/mod/database/dbleveldb"
)
func TestNewDB(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
}
func TestNewTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
err = db.NewTable("testTable")
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
}
func TestTableExists(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
if !db.TableExists("testTable") {
t.Fatalf("Table should exist")
}
}
func TestDropTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.DropTable("testTable")
if err != nil {
t.Fatalf("Failed to drop table: %v", err)
}
if db.TableExists("testTable") {
t.Fatalf("Table should not exist")
}
}
func TestWriteAndRead(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.Write("testTable", "testKey", "testValue")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
var value string
err = db.Read("testTable", "testKey", &value)
if err != nil {
t.Fatalf("Failed to read from table: %v", err)
}
if value != "testValue" {
t.Fatalf("Expected 'testValue', got '%v'", value)
}
}
func TestListTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.Write("testTable", "testKey1", "testValue1")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
err = db.Write("testTable", "testKey2", "testValue2")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
result, err := db.ListTable("testTable")
if err != nil {
t.Fatalf("Failed to list table: %v", err)
}
if len(result) != 2 {
t.Fatalf("Expected 2 entries, got %v", len(result))
}
expected := map[string]string{
"testTable/testKey1": "\"testValue1\"",
"testTable/testKey2": "\"testValue2\"",
}
for _, entry := range result {
key := string(entry[0])
value := string(entry[1])
if expected[key] != value {
t.Fatalf("Expected value '%v' for key '%v', got '%v'", expected[key], key, value)
}
}
}

View File

@ -9,7 +9,7 @@ import (
"os/user"
"strings"
"imuslab.com/zoraxy/mod/utils"
"aroz.org/zoraxy/ztnc/mod/utils"
)
func readAuthTokenAsAdmin() (string, error) {

View File

@ -5,15 +5,13 @@ package ganserv
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"aroz.org/zoraxy/ztnc/mod/utils"
"golang.org/x/sys/windows"
"imuslab.com/zoraxy/mod/utils"
)
// Use admin permission to read auth token on Windows
@ -46,15 +44,6 @@ func readAuthTokenAsAdmin() (string, error) {
return "", err
}
log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData")
retry := 0
time.Sleep(3 * time.Second)
for !utils.FileExists("./conf/authtoken.secret") && retry < 10 {
time.Sleep(3 * time.Second)
log.Println("Waiting for ZeroTier authtoken extraction...")
retry++
}
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err != nil {
return "", err

View File

@ -1,9 +1,10 @@
package ganserv
import (
"log"
"net"
"imuslab.com/zoraxy/mod/database"
"aroz.org/zoraxy/ztnc/mod/database"
)
/*
@ -85,6 +86,7 @@ func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
//Get controller info
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
if err != nil {
log.Println("ZeroTier connection failed: ", err.Error())
return &NetworkManager{
authToken: option.AuthToken,
apiPort: option.ApiPort,

View File

@ -7,7 +7,7 @@ import (
"regexp"
"strings"
"imuslab.com/zoraxy/mod/utils"
"aroz.org/zoraxy/ztnc/mod/utils"
)
func (m *NetworkManager) HandleGetNodeID(w http.ResponseWriter, r *http.Request) {

View File

@ -6,7 +6,7 @@ import (
"strconv"
"testing"
"imuslab.com/zoraxy/mod/ganserv"
"aroz.org/zoraxy/ztnc/mod/ganserv"
)
func TestGetRandomFreeIP(t *testing.T) {

View File

@ -28,11 +28,17 @@ type NodeInfo struct {
Clock int64 `json:"clock"`
Config struct {
Settings struct {
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay"`
PortMappingEnabled bool `json:"portMappingEnabled"`
PrimaryPort int `json:"primaryPort"`
SoftwareUpdate string `json:"softwareUpdate"`
SoftwareUpdateChannel string `json:"softwareUpdateChannel"`
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
HomeDir string `json:"homeDir,omitempty"`
ListeningOn []string `json:"listeningOn,omitempty"`
PortMappingEnabled bool `json:"portMappingEnabled,omitempty"`
PrimaryPort int `json:"primaryPort,omitempty"`
SecondaryPort int `json:"secondaryPort,omitempty"`
SoftwareUpdate string `json:"softwareUpdate,omitempty"`
SoftwareUpdateChannel string `json:"softwareUpdateChannel,omitempty"`
SurfaceAddresses []string `json:"surfaceAddresses,omitempty"`
TertiaryPort int `json:"tertiaryPort,omitempty"`
} `json:"settings"`
} `json:"config"`
Online bool `json:"online"`
@ -46,7 +52,6 @@ type NodeInfo struct {
VersionMinor int `json:"versionMinor"`
VersionRev int `json:"versionRev"`
}
type ErrResp struct {
Message string `json:"message"`
}

View File

@ -0,0 +1,105 @@
package utils
import (
"archive/zip"
"io"
"os"
"path/filepath"
"strconv"
"strings"
)
func StringToInt64(number string) (int64, error) {
i, err := strconv.ParseInt(number, 10, 64)
if err != nil {
return -1, err
}
return i, nil
}
func Int64ToString(number int64) string {
convedNumber := strconv.FormatInt(number, 10)
return convedNumber
}
func ReplaceSpecialCharacters(filename string) string {
replacements := map[string]string{
"#": "%pound%",
"&": "%amp%",
"{": "%left_cur%",
"}": "%right_cur%",
"\\": "%backslash%",
"<": "%left_ang%",
">": "%right_ang%",
"*": "%aster%",
"?": "%quest%",
" ": "%space%",
"$": "%dollar%",
"!": "%exclan%",
"'": "%sin_q%",
"\"": "%dou_q%",
":": "%colon%",
"@": "%at%",
"+": "%plus%",
"`": "%backtick%",
"|": "%pipe%",
"=": "%equal%",
".": "_",
"/": "-",
}
for char, replacement := range replacements {
filename = strings.ReplaceAll(filename, char, replacement)
}
return filename
}
/* Zip File Handler */
// zipFiles compresses multiple files into a single zip archive file
func ZipFiles(filename string, files ...string) error {
newZipFile, err := os.Create(filename)
if err != nil {
return err
}
defer newZipFile.Close()
zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close()
for _, file := range files {
if err := addFileToZip(zipWriter, file); err != nil {
return err
}
}
return nil
}
// addFileToZip adds an individual file to a zip archive
func addFileToZip(zipWriter *zip.Writer, filename string) error {
fileToZip, err := os.Open(filename)
if err != nil {
return err
}
defer fileToZip.Close()
info, err := fileToZip.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = filepath.Base(filename)
header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(writer, fileToZip)
return err
}

View File

@ -0,0 +1,19 @@
package utils
import (
"net/http"
)
/*
Web Template Generator
This is the main system core module that perform function similar to what PHP did.
To replace part of the content of any file, use {{paramter}} to replace it.
*/
func SendHTMLResponse(w http.ResponseWriter, msg string) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(msg))
}

View File

@ -0,0 +1,202 @@
package utils
import (
"errors"
"log"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
)
/*
Common
Some commonly used functions in ArozOS
*/
// Response related
func SendTextResponse(w http.ResponseWriter, msg string) {
w.Write([]byte(msg))
}
// Send JSON response, with an extra json header
func SendJSONResponse(w http.ResponseWriter, json string) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(json))
}
func SendErrorResponse(w http.ResponseWriter, errMsg string) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
}
func SendOK(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("\"OK\""))
}
// Get GET parameter
func GetPara(r *http.Request, key string) (string, error) {
// Get first value from the URL query
value := r.URL.Query().Get(key)
if len(value) == 0 {
return "", errors.New("invalid " + key + " given")
}
return value, nil
}
// Get GET paramter as boolean, accept 1 or true
func GetBool(r *http.Request, key string) (bool, error) {
x, err := GetPara(r, key)
if err != nil {
return false, err
}
// Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
case "1", "true", "on":
return true, nil
case "0", "false", "off":
return false, nil
}
return false, errors.New("invalid boolean given")
}
// Get POST parameter
func PostPara(r *http.Request, key string) (string, error) {
// Try to parse the form
if err := r.ParseForm(); err != nil {
return "", err
}
// Get first value from the form
x := r.Form.Get(key)
if len(x) == 0 {
return "", errors.New("invalid " + key + " given")
}
return x, nil
}
// Get POST paramter as boolean, accept 1 or true
func PostBool(r *http.Request, key string) (bool, error) {
x, err := PostPara(r, key)
if err != nil {
return false, err
}
// Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
case "1", "true", "on":
return true, nil
case "0", "false", "off":
return false, nil
}
return false, errors.New("invalid boolean given")
}
// Get POST paramter as int
func PostInt(r *http.Request, key string) (int, error) {
x, err := PostPara(r, key)
if err != nil {
return 0, err
}
x = strings.TrimSpace(x)
rx, err := strconv.Atoi(x)
if err != nil {
return 0, err
}
return rx, nil
}
func FileExists(filename string) bool {
_, err := os.Stat(filename)
if err == nil {
// File exists
return true
} else if errors.Is(err, os.ErrNotExist) {
// File does not exist
return false
}
// Some other error
return false
}
func IsDir(path string) bool {
if !FileExists(path) {
return false
}
fi, err := os.Stat(path)
if err != nil {
log.Fatal(err)
return false
}
switch mode := fi.Mode(); {
case mode.IsDir():
return true
case mode.IsRegular():
return false
}
return false
}
func TimeToString(targetTime time.Time) string {
return targetTime.Format("2006-01-02 15:04:05")
}
// Check if given string in a given slice
func StringInArray(arr []string, str string) bool {
for _, a := range arr {
if a == str {
return true
}
}
return false
}
func StringInArrayIgnoreCase(arr []string, str string) bool {
smallArray := []string{}
for _, item := range arr {
smallArray = append(smallArray, strings.ToLower(item))
}
return StringInArray(smallArray, strings.ToLower(str))
}
// Validate if the listening address is correct
func ValidateListeningAddress(address string) bool {
// Check if the address starts with a colon, indicating it's just a port
if strings.HasPrefix(address, ":") {
return true
}
// Split the address into host and port parts
host, port, err := net.SplitHostPort(address)
if err != nil {
// Try to parse it as just a port
if _, err := strconv.Atoi(address); err == nil {
return false // It's just a port number
}
return false // It's an invalid address
}
// Check if the port part is a valid number
if _, err := strconv.Atoi(port); err != nil {
return false
}
// Check if the host part is a valid IP address or empty (indicating any IP)
if host != "" {
if net.ParseIP(host) == nil {
return false
}
}
return true
}

View File

@ -0,0 +1,19 @@
# Zoraxy Plugin
## Overview
This module serves as a template for building your own plugins for the Zoraxy Reverse Proxy. By copying this module to your plugin mod folder, you can create a new plugin with the necessary structure and components.
## Instructions
1. **Copy the Module:**
- Copy the entire `zoraxy_plugin` module to your plugin mod folder.
2. **Include the Structure:**
- Ensure that you maintain the directory structure and file organization as provided in this module.
3. **Modify as Needed:**
- Customize the copied module to implement the desired functionality for your plugin.
## Directory Structure
zoraxy_plugin: Handle -introspect and -configuration process required for plugin loading and startup
embed_webserver: Handle embeded web server routing and injecting csrf token to your plugin served UI pages

View File

@ -0,0 +1,145 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"os"
"strings"
"time"
)
type PluginUiDebugRouter struct {
PluginID string //The ID of the plugin
TargetDir string //The directory where the UI files are stored
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
// The targetDir is the directory where the UI files are stored (e.g. ./www)
// The handlerPrefix is the prefix of the handler used to route this router
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
// All prefix should not end with a slash
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
//Make sure all prefix are in /prefix format
if !strings.HasPrefix(handlerPrefix, "/") {
handlerPrefix = "/" + handlerPrefix
}
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
//Return the PluginUiRouter
return &PluginUiDebugRouter{
PluginID: pluginID,
TargetDir: targetDir,
HandlerPrefix: handlerPrefix,
}
}
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
//Get the CSRF token from header
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
if csrfToken == "" {
csrfToken = "missing-csrf-token"
}
//Return the middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the request is for an HTML file
if strings.HasSuffix(r.URL.Path, ".html") {
//Read the target file from file system
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
//Check if the request is for a directory
//Check if the directory has an index.html file
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
if _, err := os.Stat(targetFilePath); err == nil {
//Serve the index.html file
targetFileContent, err := os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
}
}
//Call the next handler
fsHandler.ServeHTTP(w, r)
})
}
// GetHttpHandler returns the http.Handler for the PluginUiRouter
func (p *PluginUiDebugRouter) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Remove the plugin UI handler path prefix
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL.Path = rewrittenURL
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//Serve the file from the file system
fsHandler := http.FileServer(http.Dir(p.TargetDir))
// Replace {{csrf_token}} with the actual CSRF token and serve the file
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// Attach the file system UI handler to the target http.ServeMux
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@ -0,0 +1,162 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
/*
Dynamic Path Handler
*/
type SniffResult int
const (
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
SniffResultSkip // Skip this plugin and let the next plugin handle the request
)
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
/*
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
You can decide to accept or skip the request based on the request header and paths
*/
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
if !strings.HasSuffix(sniff_ingress, "/") {
sniff_ingress = sniff_ingress + "/"
}
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
}
// Decode the request payload
jsonBytes, err := io.ReadAll(r.Body)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error reading request body:", err)
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
payload, err := DecodeForwardRequestPayload(jsonBytes)
if err != nil {
if p.enableDebugPrint {
fmt.Println("Error decoding request payload:", err)
fmt.Print("Payload: ")
fmt.Println(string(jsonBytes))
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Get the forwarded request UUID
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
payload.requestUUID = forwardUUID
payload.rawRequest = r
sniffResult := handler(&payload)
if sniffResult == SniffResultAccpet {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
} else {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("SKIP"))
}
}))
}
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.enableDebugPrint {
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
if rewrittenURL == "" {
rewrittenURL = "/"
}
if !strings.HasPrefix(rewrittenURL, "/") {
rewrittenURL = "/" + rewrittenURL
}
r.RequestURI = rewrittenURL
handlefunc(w, r)
}))
}
/*
Sniffing and forwarding
The following functions are here to help with
sniffing and forwarding requests to the dynamic
router.
*/
// A custom request object to be used in the dynamic sniffing
type DynamicSniffForwardRequest struct {
Method string `json:"method"`
Hostname string `json:"hostname"`
URL string `json:"url"`
Header map[string][]string `json:"header"`
RemoteAddr string `json:"remote_addr"`
Host string `json:"host"`
RequestURI string `json:"request_uri"`
Proto string `json:"proto"`
ProtoMajor int `json:"proto_major"`
ProtoMinor int `json:"proto_minor"`
/* Internal use */
rawRequest *http.Request `json:"-"`
requestUUID string `json:"-"`
}
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
return DynamicSniffForwardRequest{
Method: r.Method,
Hostname: r.Host,
URL: r.URL.String(),
Header: r.Header,
RemoteAddr: r.RemoteAddr,
Host: r.Host,
RequestURI: r.RequestURI,
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
rawRequest: r,
}
}
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
var payload DynamicSniffForwardRequest
err := json.Unmarshal(jsonBytes, &payload)
if err != nil {
return DynamicSniffForwardRequest{}, err
}
return payload, nil
}
// GetRequest returns the original http.Request object, for debugging purposes
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
return dsfr.rawRequest
}
// GetRequestUUID returns the request UUID
// if this UUID is empty string, that might indicate the request
// is not coming from the dynamic router
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
return dsfr.requestUUID
}

View File

@ -0,0 +1,156 @@
package zoraxy_plugin
import (
"embed"
"fmt"
"io/fs"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type PluginUiRouter struct {
PluginID string //The ID of the plugin
TargetFs *embed.FS //The embed.FS where the UI files are stored
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
EnableDebug bool //Enable debug mode
terminateHandler func() //The handler to be called when the plugin is terminated
}
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored
// The targetFsPrefix should be relative to the root of the embed.FS
// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS
// The handlerPrefix is the prefix of the handler used to route this router
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
// All prefix should not end with a slash
func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
//Make sure all prefix are in /prefix format
if !strings.HasPrefix(targetFsPrefix, "/") {
targetFsPrefix = "/" + targetFsPrefix
}
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
if !strings.HasPrefix(handlerPrefix, "/") {
handlerPrefix = "/" + handlerPrefix
}
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
//Return the PluginUiRouter
return &PluginUiRouter{
PluginID: pluginID,
TargetFs: targetFs,
TargetFsPrefix: targetFsPrefix,
HandlerPrefix: handlerPrefix,
}
}
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
//Get the CSRF token from header
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
if csrfToken == "" {
csrfToken = "missing-csrf-token"
}
//Return the middleware
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the request is for an HTML file
if strings.HasSuffix(r.URL.Path, ".html") {
//Read the target file from embed.FS
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
body := string(targetFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
} else if strings.HasSuffix(r.URL.Path, "/") {
// Check if the directory has an index.html file
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
if err == nil {
body := string(indexFileContent)
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(body))
return
}
}
//Call the next handler
fsHandler.ServeHTTP(w, r)
})
}
// GetHttpHandler returns the http.Handler for the PluginUiRouter
func (p *PluginUiRouter) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//Remove the plugin UI handler path prefix
if p.EnableDebug {
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
}
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL, _ = url.Parse(rewrittenURL)
r.RequestURI = rewrittenURL
if p.EnableDebug {
fmt.Println(r.URL.Path)
}
//Serve the file from the embed.FS
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
if err != nil {
fmt.Println(err.Error())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Replace {{csrf_token}} with the actual CSRF token and serve the file
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
})
}
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
// if mux is nil, the handler will be registered to http.DefaultServeMux
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
p.terminateHandler = termFunc
if mux == nil {
mux = http.DefaultServeMux
}
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
p.terminateHandler()
w.WriteHeader(http.StatusOK)
go func() {
//Make sure the response is sent before the plugin is terminated
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
})
}
// Attach the embed UI handler to the target http.ServeMux
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
if mux == nil {
mux = http.DefaultServeMux
}
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
mux.Handle(p.HandlerPrefix+"/", p.Handler())
}

View File

@ -0,0 +1,105 @@
package zoraxy_plugin
import (
"fmt"
"net/http"
"sort"
"strings"
)
type PathRouter struct {
enableDebugPrint bool
pathHandlers map[string]http.Handler
defaultHandler http.Handler
}
// NewPathRouter creates a new PathRouter
func NewPathRouter() *PathRouter {
return &PathRouter{
enableDebugPrint: false,
pathHandlers: make(map[string]http.Handler),
}
}
// RegisterPathHandler registers a handler for a path
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
path = strings.TrimSuffix(path, "/")
p.pathHandlers[path] = handler
}
// RemovePathHandler removes a handler for a path
func (p *PathRouter) RemovePathHandler(path string) {
delete(p.pathHandlers, path)
}
// SetDefaultHandler sets the default handler for the router
// This handler will be called if no path handler is found
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
p.defaultHandler = handler
}
// SetDebugPrintMode sets the debug print mode
func (p *PathRouter) SetDebugPrintMode(enable bool) {
p.enableDebugPrint = enable
}
// StartStaticCapture starts the static capture ingress
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
if !strings.HasSuffix(capture_ingress, "/") {
capture_ingress = capture_ingress + "/"
}
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p.staticCaptureServeHTTP(w, r)
}))
}
// staticCaptureServeHTTP serves the static capture path using user defined handler
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
capturePath := r.Header.Get("X-Zoraxy-Capture")
if capturePath != "" {
if p.enableDebugPrint {
fmt.Printf("Using capture path: %s\n", capturePath)
}
originalURI := r.Header.Get("X-Zoraxy-Uri")
r.URL.Path = originalURI
if handler, ok := p.pathHandlers[capturePath]; ok {
handler.ServeHTTP(w, r)
return
}
}
p.defaultHandler.ServeHTTP(w, r)
}
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
if p.enableDebugPrint {
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
keys := make([]string, 0, len(r.Header))
for key := range r.Header {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
for _, value := range r.Header[key] {
fmt.Printf("%s: %s\n", key, value)
}
}
fmt.Printf("\n\n**Request Details**\n\n")
fmt.Printf("Method: %s\n", r.Method)
fmt.Printf("URL: %s\n", r.URL.String())
fmt.Printf("Proto: %s\n", r.Proto)
fmt.Printf("Host: %s\n", r.Host)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
fmt.Printf("ContentLength: %d\n", r.ContentLength)
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
fmt.Printf("Close: %v\n", r.Close)
fmt.Printf("Form: %v\n", r.Form)
fmt.Printf("PostForm: %v\n", r.PostForm)
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
fmt.Printf("Trailer: %v\n", r.Trailer)
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
fmt.Printf("RequestURI: %s\n", r.RequestURI)
}
}

View File

@ -0,0 +1,176 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"os"
"strings"
)
/*
Plugins Includes.go
This file is copied from Zoraxy source code
You can always find the latest version under mod/plugins/includes.go
Usually this file are backward compatible
*/
type PluginType int
const (
PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic
PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore
)
type StaticCaptureRule struct {
CapturePath string `json:"capture_path"`
//To be expanded
}
type ControlStatusCode int
const (
ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic
ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic
ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error
)
type SubscriptionEvent struct {
EventName string `json:"event_name"`
EventSource string `json:"event_source"`
Payload string `json:"payload"` //Payload of the event, can be empty
}
type RuntimeConstantValue struct {
ZoraxyVersion string `json:"zoraxy_version"`
ZoraxyUUID string `json:"zoraxy_uuid"`
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
}
/*
IntroSpect Payload
When the plugin is initialized with -introspect flag,
the plugin shell return this payload as JSON and exit
*/
type IntroSpect struct {
/* Plugin metadata */
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
Name string `json:"name"` //Name of your plugin
Author string `json:"author"` //Author name of your plugin
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
Description string `json:"description"` //Description of your plugin
URL string `json:"url"` //URL of your plugin
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
VersionMajor int `json:"version_major"` //Major version of your plugin
VersionMinor int `json:"version_minor"` //Minor version of your plugin
VersionPatch int `json:"version_patch"` //Patch version of your plugin
/*
Endpoint Settings
*/
/*
Static Capture Settings
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
This is faster than dynamic capture, but less flexible
*/
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
/*
Dynamic Capture Settings
Once plugin is enabled, these rules will be captured and forward to plugin sniff
if the plugin sniff returns 280, the traffic will be captured
otherwise, the traffic will be forwarded to the next plugin
This is slower than static capture, but more flexible
*/
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
/* UI Path for your plugin */
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
/* Subscriptions Settings */
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
}
/*
ServeIntroSpect Function
This function will check if the plugin is initialized with -introspect flag,
if so, it will print the intro spect and exit
Place this function at the beginning of your plugin main function
*/
func ServeIntroSpect(pluginSpect *IntroSpect) {
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
//Print the intro spect and exit
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
fmt.Println(string(jsonData))
os.Exit(0)
}
}
/*
ConfigureSpec Payload
Zoraxy will start your plugin with -configure flag,
the plugin shell read this payload as JSON and configure itself
by the supplied values like starting a web server at given port
that listens to 127.0.0.1:port
*/
type ConfigureSpec struct {
Port int `json:"port"` //Port to listen
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
//To be expanded
}
/*
RecvExecuteConfigureSpec Function
This function will read the configure spec from Zoraxy
and return the ConfigureSpec object
Place this function after ServeIntroSpect function in your plugin main function
*/
func RecvConfigureSpec() (*ConfigureSpec, error) {
for i, arg := range os.Args {
if strings.HasPrefix(arg, "-configure=") {
var configSpec ConfigureSpec
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
return nil, err
}
return &configSpec, nil
} else if arg == "-configure" {
var configSpec ConfigureSpec
var nextArg string
if len(os.Args) > i+1 {
nextArg = os.Args[i+1]
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("No port specified after -configure flag")
}
return &configSpec, nil
}
}
return nil, fmt.Errorf("No -configure flag found")
}
/*
ServeAndRecvSpec Function
This function will serve the intro spect and return the configure spec
See the ServeIntroSpect and RecvConfigureSpec for more details
*/
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
ServeIntroSpect(pluginSpect)
return RecvConfigureSpec()
}

View File

@ -0,0 +1,69 @@
package main
import (
"fmt"
"net/http"
"os"
"aroz.org/zoraxy/ztnc/mod/database"
"aroz.org/zoraxy/ztnc/mod/database/dbinc"
"aroz.org/zoraxy/ztnc/mod/ganserv"
"aroz.org/zoraxy/ztnc/mod/utils"
)
func startGanNetworkController() error {
fmt.Println("Starting ZeroTier Network Controller")
//Create a new database
var err error
sysdb, err = database.NewDatabase(DB_FILE_PATH, dbinc.BackendBoltDB)
if err != nil {
return err
}
//Initiate the GAN server manager
usingZtAuthToken := ""
ztAPIPort := 9993
if utils.FileExists(AUTH_TOKEN_PATH) {
authToken, err := os.ReadFile(AUTH_TOKEN_PATH)
if err != nil {
fmt.Println("Error reading auth config file:", err)
return err
}
usingZtAuthToken = string(authToken)
fmt.Println("Loaded ZeroTier Auth Token from file")
}
if usingZtAuthToken == "" {
usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey()
if err != nil {
fmt.Println("Error getting ZeroTier Auth Token:", err)
}
}
ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
AuthToken: usingZtAuthToken,
ApiPort: ztAPIPort,
Database: sysdb,
})
return nil
}
func initApiEndpoints() {
//UI_RELPATH must be the same as the one in the plugin intro spect
// as Zoraxy plugin UI proxy will only forward the UI path to your plugin
http.HandleFunc(UI_RELPATH+"/api/gan/network/info", ganManager.HandleGetNodeID)
http.HandleFunc(UI_RELPATH+"/api/gan/network/add", ganManager.HandleAddNetwork)
http.HandleFunc(UI_RELPATH+"/api/gan/network/remove", ganManager.HandleRemoveNetwork)
http.HandleFunc(UI_RELPATH+"/api/gan/network/list", ganManager.HandleListNetwork)
http.HandleFunc(UI_RELPATH+"/api/gan/network/name", ganManager.HandleNetworkNaming)
http.HandleFunc(UI_RELPATH+"/api/gan/network/setRange", ganManager.HandleSetRanges)
http.HandleFunc(UI_RELPATH+"/api/gan/network/join", ganManager.HandleServerJoinNetwork)
http.HandleFunc(UI_RELPATH+"/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
http.HandleFunc(UI_RELPATH+"/api/gan/members/list", ganManager.HandleMemberList)
http.HandleFunc(UI_RELPATH+"/api/gan/members/ip", ganManager.HandleMemberIP)
http.HandleFunc(UI_RELPATH+"/api/gan/members/name", ganManager.HandleMemberNaming)
http.HandleFunc(UI_RELPATH+"/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
http.HandleFunc(UI_RELPATH+"/api/gan/members/delete", ganManager.HandleMemberDelete)
}

View File

@ -1,3 +1,4 @@
<!-- This is being loaded in index.html as ajax -->
<div class="standardContainer">
<button onclick="exitToGanList();" class="ui large circular black icon button"><i class="angle left icon"></i></button>
<div style="max-width: 300px; margin-top: 1em;">
@ -16,12 +17,12 @@
<div class="field">
<label>Network Name</label>
<input type="text" id="gaNetNameInput" placeholder="">
</div>
</div>
<div class="field">
<label>Network Description</label>
<textarea id="gaNetDescInput" style="resize: none;"></textarea>
<button onclick="saveNameAndDesc(this);" class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui save icon"></i> Save</button>
<button onclick='$("#gannetDetailEdit").slideUp("fast");' class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui red remove icon"></i> Cancel</button>
<label>Network Description</label>
<textarea id="gaNetDescInput" style="resize: none;"></textarea>
<button onclick="saveNameAndDesc(this);" class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui save icon"></i> Save</button>
<button onclick='$("#gannetDetailEdit").slideUp("fast");' class="ui basic right floated button" style="margin-top: 0.6em;"><i class="ui red remove icon"></i> Cancel</button>
</div>
<br><br>
</div>
@ -215,7 +216,7 @@
var cidr = $(".iprange.active").attr("cidr");
$.cjax({
url: "/api/gan/network/setRange",
url: "./api/gan/network/setRange",
metohd: "POST",
data:{
netid: currentGANetID,
@ -225,9 +226,9 @@
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000)
parent.msgbox(data.error, false, 5000)
}else{
msgbox("Network Range Updated")
parent.msgbox("Network Range Updated")
}
}
})
@ -241,7 +242,7 @@
$(object).addClass("loading");
}
$.cjax({
url: "/api/gan/network/name",
url: "./api/gan/network/name",
method: "POST",
data: {
netid: currentGANetID,
@ -252,7 +253,7 @@
initNetNameAndDesc();
if (object != undefined){
$(object).removeClass("loading");
msgbox("Network Metadata Updated");
parent.msgbox("Network Metadata Updated");
}
$("#gannetDetailEdit").slideUp("fast");
}
@ -261,9 +262,9 @@
function initNetNameAndDesc(){
//Get the details of the net
$.get("/api/gan/network/name?netid=" + currentGANetID, function(data){
$.get("./api/gan/network/name?netid=" + currentGANetID, function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
parent.msgbox(data.error, false, 6000);
}else{
$("#gaNetNameInput").val(data[0]);
$(".ganetName").html(data[0]);
@ -274,10 +275,10 @@
}
function initNetDetails(){
//Get the details of the net
$.get("/api/gan/network/list?netid=" + currentGANetID, function(data){
//Get the details of the net
$.get("./api/gan/network/list?netid=" + currentGANetID, function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
parent.msgbox(data.error, false, 6000);
}else{
currentGaNetDetails = data;
highlightCurrentGANetCIDR();
@ -288,7 +289,7 @@
//Handle delete IP from memeber
function deleteIpFromMemeber(memberid, ip){
$.cjax({
url: "/api/gan/members/ip",
url: "./api/gan/members/ip",
metohd: "POST",
data: {
netid: currentGANetID,
@ -298,9 +299,9 @@
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
parent.msgbox(data.error, false, 5000);
}else{
msgbox("IP removed from member " + memberid)
parent.msgbox("IP removed from member " + memberid)
}
renderMemeberTable();
}
@ -330,12 +331,12 @@
}
if (!isValidIPv4Address(newip)){
msgbox(newip + " is not a valid IPv4 address", false, 5000)
parent.msgbox(newip + " is not a valid IPv4 address", false, 5000)
return
}
$.cjax({
url: "/api/gan/members/ip",
url: "./api/gan/members/ip",
metohd: "POST",
data: {
netid: currentGANetID,
@ -345,9 +346,9 @@
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
parent.msgbox(data.error, false, 5000);
}else{
msgbox("IP added to member " + memberid)
parent.msgbox("IP added to member " + memberid)
}
renderMemeberTable();
}
@ -357,7 +358,7 @@
//Member table populate
function renderMemeberTable(forceUpdate = false) {
$.ajax({
url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true',
url: './api/gan/members/list?netid=' + currentGANetID + '&detail=true',
type: 'GET',
success: function(data) {
let tableBody = $('#networkMemeberTable');
@ -462,7 +463,7 @@
let addr = $(this).attr("addr");
let targetDOM = $(this);
$.cjax({
url: "/api/gan/members/name",
url: "./api/gan/members/name",
method: "POST",
data: {
netid: currentGANetID,
@ -481,14 +482,14 @@
function renameMember(targetMemberAddr){
if (targetMemberAddr == ""){
msgbox("Member address cannot be empty", false, 5000)
parent.msgbox("Member address cannot be empty", false, 5000)
return
}
let newname = prompt("Enter a easy manageable name for " + targetMemberAddr, "");
if (newname != null && newname.trim() != "") {
$.cjax({
url: "/api/gan/members/name",
$.cjax({
url: "./api/gan/members/name",
method: "POST",
data: {
netid: currentGANetID,
@ -497,13 +498,13 @@
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
parent.msgbox(data.error, false, 6000);
}else{
msgbox("Member Name Updated");
parent.msgbox("Member Name Updated");
}
renderMemeberTable(true);
}
})
})
}
}
@ -554,7 +555,7 @@
let targetMemberAddr = $(object).attr("addr");
let isAuthed = object.checked;
$.cjax({
url: "/api/gan/members/authorize",
url: "./api/gan/members/authorize",
method: "POST",
data: {
netid:currentGANetID,
@ -563,12 +564,12 @@
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
parent.msgbox(data.error, false, 6000);
}else{
if (isAuthed){
msgbox("Member Authorized");
parent.msgbox("Member Authorized");
}else{
msgbox("Member Deauthorized");
parent.msgbox("Member Deauthorized");
}
}
@ -579,25 +580,26 @@
}
function handleMemberDelete(addr){
if (confirm("Confirm delete member " + addr + " ?")){
$.cjax({
url: "/api/gan/members/delete",
method: "POST",
data: {
netid:currentGANetID,
memid: addr,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Member Deleted");
parent.confirmBox("Confirm delete member " + addr + " ?", function(choice){
if (choice){
$.cjax({
url: "./api/gan/members/delete",
method: "POST",
data: {
netid:currentGANetID,
memid: addr,
},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false, 6000);
}else{
parent.msgbox("Member Deleted");
}
renderMemeberTable(true);
}
renderMemeberTable(true);
}
});
}
});
}
});
}
//Add and remove this controller node to network as member
@ -606,7 +608,7 @@
$(".addControllerToNetworkBtn").addClass("loading");
$.cjax({
url: "/api/gan/network/join",
url: "./api/gan/network/join",
method: "POST",
data: {
netid:currentGANetID,
@ -615,13 +617,18 @@
$(".addControllerToNetworkBtn").removeClass("disabled");
$(".addControllerToNetworkBtn").removeClass("loading");
if (data.error != undefined){
msgbox(data.error, false, 6000);
parent.msgbox(data.error, false, 6000);
}else{
msgbox("Controller joint " + currentGANetID);
parent.msgbox("Controller joint " + currentGANetID);
}
setTimeout(function(){
renderMemeberTable(true);
}, 3000)
},
error: function(){
$(".addControllerToNetworkBtn").removeClass("disabled");
$(".addControllerToNetworkBtn").removeClass("loading");
}
});
}
@ -631,16 +638,16 @@
$(".removeControllerFromNetworkBtn").addClass("loading");
$.cjax({
url: "/api/gan/network/leave",
url: "./api/gan/network/leave",
method: "POST",
data: {
netid:currentGANetID,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
parent.msgbox(data.error, false, 6000);
}else{
msgbox("Controller left " + currentGANetID);
parent.msgbox("Controller left " + currentGANetID);
}
renderMemeberTable(true);
$(".removeControllerFromNetworkBtn").removeClass("disabled");
@ -654,7 +661,7 @@
currentGANetID = ganetId;
$(".ganetID").text(ganetId);
initNetNameAndDesc(ganetId);
generateIPRangeTable(netRanges);
generateIPRangeTable(netRanges);
initNetDetails();
renderMemeberTable(true);
@ -669,19 +676,77 @@
}
//Switch from other tabs back to this, exit to GAN list
tabSwitchEventBind["gan"] = function(){
exitToGanList();
}
//Exit point
function exitToGanList(){
$("#gan").load("./components/gan.html", function(){
if (tabSwitchEventBind["gan"]){
tabSwitchEventBind["gan"]();
}
});
location.href = "./index.html"
}
//Debug functions
if (typeof(msgbox) == "undefined"){
msgbox = function(msg, error=false, timeout=3000){
console.log(msg);
}
}
function ip2long (argIP) {
// discuss at: https://locutus.io/php/ip2long/
// original by: Waldo Malqui Silva (https://waldo.malqui.info)
// improved by: Victor
// revised by: fearphage (https://my.opera.com/fearphage/)
// revised by: Theriault (https://github.com/Theriault)
// estarget: es2015
// example 1: ip2long('192.0.34.166')
// returns 1: 3221234342
// example 2: ip2long('0.0xABCDEF')
// returns 2: 11259375
// example 3: ip2long('255.255.255.256')
// returns 3: false
let i = 0
// PHP allows decimal, octal, and hexadecimal IP components.
// PHP allows between 1 (e.g. 127) to 4 (e.g 127.0.0.1) components.
const pattern = new RegExp([
'^([1-9]\\d*|0[0-7]*|0x[\\da-f]+)',
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?',
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?',
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?$'
].join(''), 'i')
argIP = argIP.match(pattern) // Verify argIP format.
if (!argIP) {
// Invalid format.
return false
}
// Reuse argIP variable for component counter.
argIP[0] = 0
for (i = 1; i < 5; i += 1) {
argIP[0] += !!((argIP[i] || '').length)
argIP[i] = parseInt(argIP[i]) || 0
}
// Continue to use argIP for overflow values.
// PHP does not allow any component to overflow.
argIP.push(256, 256, 256, 256)
// Recalculate overflow of last component supplied to make up for missing components.
argIP[4 + argIP[0]] *= Math.pow(256, 4 - argIP[0])
if (argIP[1] >= argIP[5] ||
argIP[2] >= argIP[6] ||
argIP[3] >= argIP[7] ||
argIP[4] >= argIP[8]) {
return false
}
return argIP[1] * (argIP[0] === 1 || 16777216) +
argIP[2] * (argIP[0] <= 2 || 65536) +
argIP[3] * (argIP[0] <= 3 || 256) +
argIP[4] * 1
}
function long2ip (ip) {
// discuss at: https://locutus.io/php/long2ip/
// original by: Waldo Malqui Silva (https://fayr.us/waldo/)
// example 1: long2ip( 3221234342 )
// returns 1: '192.0.34.166'
if (!isFinite(ip)) {
return false
}
return [ip >>> 24 & 0xFF, ip >>> 16 & 0xFF, ip >>> 8 & 0xFF, ip & 0xFF].join('.')
}
</script>
</script>

View File

@ -0,0 +1,267 @@
<html>
<head>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
<meta charset="UTF-8">
<meta name="theme-color" content="#4b75ff">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="icon" type="image/png" href="/favicon.png" />
<title>Global Area Network | Zoraxy</title>
<link rel="stylesheet" href="/script/semantic/semantic.min.css">
<script src="/script/jquery-3.6.0.min.js"></script>
<script src="/script/semantic/semantic.min.js"></script>
<script src="/script/tablesort.js"></script>
<script src="/script/countryCode.js"></script>
<script src="/script/chart.js"></script>
<script src="/script/utils.js"></script>
<link rel="stylesheet" href="/main.css">
<style>
body{
background:none;
}
</style>
</head>
<body>
<!-- Dark theme script must be included after body tag-->
<link rel="stylesheet" href="/darktheme.css">
<script src="/script/darktheme.js"></script>
<div id="ganetWindow" class="standardContainer">
<div class="ui basic segment">
<h2>Global Area Network</h2>
<p>Virtual Network Hub that allows all networked devices to communicate as if they all reside in the same physical data center or cloud region</p>
</div>
<div class="gansnetworks">
<div class="ganstats ui basic segment">
<div style="float: right; max-width: 300px; margin-top: 0.4em;">
<h1 class="ui header" style="text-align: right;">
<span class="ganControllerID"></span>
<div class="sub header">Network Controller ID</div>
</h1>
</div>
<div class="ui list">
<div class="item">
<i class="exchange icon"></i>
<div class="content">
<div class="header" style="font-size: 1.2em;" id="ganetCount">0</div>
<div class="description">Networks</div>
</div>
</div>
<div class="item">
<i class="desktop icon"></i>
<div class="content">
<div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div>
<div class="description" id="connectedNodes" count="0">Connected Nodes</div>
</div>
</div>
</div>
</div>
<div class="ganlist">
<button class="ui basic orange button" onclick="addGANet();">Create New Network</button>
<div class="ui divider"></div>
<!--
<div class="ui icon input" style="margin-bottom: 1em;">
<input type="text" placeholder="Search a Network">
<i class="circular search link icon"></i>
</div>-->
<div style="width: 100%; overflow-x: auto;">
<table class="ui celled basic unstackable striped table">
<thead>
<tr>
<th>Network ID</th>
<th>Name</th>
<th>Description</th>
<th>Subnet (Assign Range)</th>
<th>Nodes</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="GANetList">
<tr>
<td colspan="6"><i class="ui green circle check icon"></i> No Global Area Network Found on this host</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
/*
Network Management Functions
*/
function handleAddNetwork(){
let networkName = $("#networkName").val().trim();
if (networkName == ""){
parent.msgbox("Network name cannot be empty", false, 5000);
return;
}
//Add network with default settings
addGANet(networkName, "192.168.196.0/24");
$("#networkName").val("");
}
function initGANetID(){
$.get("./api/gan/network/info", function(data){
if (data.error !== undefined){
parent.msgbox(data.error, false, 5000)
}else{
if (data != ""){
$(".ganControllerID").text(data);
}
}
})
}
function addGANet() {
$.cjax({
url: "./api/gan/network/add",
type: "POST",
dataType: "json",
data: {},
success: function(response) {
if (response.error != undefined){
parent.msgbox(response.error, false, 5000);
}else{
parent.msgbox("Network added successfully");
}
console.log("Network added successfully:", response);
listGANet();
},
error: function(xhr, status, error) {
console.log("Error adding network:", error);
}
});
}
function listGANet(){
$("#connectedNodes").attr("count", "0");
$.get("./api/gan/network/list", function(data){
$("#GANetList").empty();
if (data.error != undefined){
console.log(data.error);
parent.msgbox("Unable to load auth token for GANet", false, 5000);
//token error or no zerotier found
$(".gansnetworks").addClass("disabled");
$("#GANetList").append(`<tr>
<td colspan="6"><i class="red times circle icon"></i> Auth token access error or not found</td>
</tr>`);
$(".ganControllerID").text('Access Denied');
}else{
var nodeCount = 0;
data.forEach(function(gan){
$("#GANetList").append(`<tr class="ganetEntry" addr="${gan.nwid}">
<td><a href="#" onclick="event.preventDefault(); openGANetDetails('${gan.nwid}');">${gan.nwid}</a></td>
<td>${gan.name}</td>
<td class="gandesc" addr="${gan.nwid}"></td>
<td class="ganetSubnet"></td>
<td class="ganetNodes"></td>
<td>
<button onclick="openGANetDetails('${gan.nwid}');" class="ui tiny basic icon button" title="Edit Network"><i class="edit icon"></i></button>
<button onclick="removeGANet('${gan.nwid}');" class="ui tiny basic icon button" title="Remove Network"><i class="red remove icon"></i></button>
</td>
</tr>`);
nodeCount += 0;
});
if (data.length == 0){
$("#GANetList").append(`<tr>
<td colspan="6"><i class="ui green circle check icon"></i> No Global Area Network Found on this host</td>
</tr>`);
}
$("#ganodeCount").text(nodeCount);
$("#ganetCount").text(data.length);
//Load description
$(".gandesc").each(function(){
let addr = $(this).attr("addr");
let domEle = $(this);
$.get("./api/gan/network/name?netid=" + addr, function(data){
$(domEle).text(data[1]);
});
});
$(".ganetEntry").each(function(){
let addr = $(this).attr("addr");
let subnetEle = $(this).find(".ganetSubnet");
let nodeEle = $(this).find(".ganetNodes");
$.get("./api/gan/network/list?netid=" + addr, function(data){
if (data.routes != undefined && data.routes.length > 0){
if (data.ipAssignmentPools != undefined && data.ipAssignmentPools.length > 0){
$(subnetEle).html(`${data.routes[0].target} <br> (${data.ipAssignmentPools[0].ipRangeStart} - ${data.ipAssignmentPools[0].ipRangeEnd})`);
}else{
$(subnetEle).html(`${data.routes[0].target}<br>(Unassigned Range)`);
}
}else{
$(subnetEle).text("Unassigned");
}
//console.log(data);
});
$.get("./api/gan/members/list?netid=" + addr, function(data){
$(nodeEle).text(data.length);
let currentNodesCount = parseInt($("#connectedNodes").attr("count"));
currentNodesCount += data.length;
$("#connectedNodes").attr("count", currentNodesCount);
$("#ganodeCount").text($("#connectedNodes").attr("count"));
})
});
}
})
}
//Remove the given GANet
function removeGANet(netid){
//Reusing Zoraxy confirm box
parent.confirmBox("Confirm remove " + netid + "?", function(choice){
if (choice == true){
$.cjax({
url: "./api/gan/network/remove",
type: "POST",
dataType: "json",
data: {
id: netid,
},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false, 5000);
}else{
parent.msgbox("Net " + netid + " removed");
}
listGANet();
}
});
}
});
}
function openGANetDetails(netid){
$("#ganetWindow").load("./details.html", function(){
setTimeout(function(){
initGanetDetails(netid);
});
});
}
$(document).ready(function(){
listGANet();
initGANetID();
});
if (typeof(msgbox) == "undefined"){
msgbox = function(msg, error=false, timeout=3000){
console.log(msg);
}
}
</script>
</body>
</html>

View File

@ -1,5 +1,5 @@
# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 freebsd/amd64
temp = $(subst /, ,$@)
os = $(word 1, $(temp))
arch = $(word 2, $(temp))

View File

@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"net/http"
"sort"
"strings"
"github.com/google/uuid"
@ -230,7 +231,17 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
return
}
rule.AddCountryCodeToBlackList(countryCode, comment)
//Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.AddCountryCodeToBlackList(code, comment)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.AddCountryCodeToBlackList(countryCode, comment)
}
utils.SendOK(w)
}
@ -254,7 +265,17 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
return
}
rule.RemoveCountryCodeFromBlackList(countryCode)
//Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.RemoveCountryCodeFromBlackList(code)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.RemoveCountryCodeFromBlackList(countryCode)
}
utils.SendOK(w)
}
@ -397,7 +418,17 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment)
rule.AddCountryCodeToWhitelist(countryCode, comment)
//Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.AddCountryCodeToWhitelist(code, comment)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.AddCountryCodeToWhitelist(countryCode, comment)
}
utils.SendOK(w)
}
@ -420,7 +451,17 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
return
}
rule.RemoveCountryCodeFromWhitelist(countryCode)
//Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.RemoveCountryCodeFromWhitelist(code)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.RemoveCountryCodeFromWhitelist(countryCode)
}
utils.SendOK(w)
}
@ -505,3 +546,71 @@ func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w)
}
}
func handleWhitelistAllowLoopback(w http.ResponseWriter, r *http.Request) {
enable, _ := utils.PostPara(r, "enable")
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if enable == "" {
//Return the current enabled state
currentEnabled := rule.WhitelistAllowLocalAndLoopback
js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js))
} else {
if enable == "true" {
rule.ToggleAllowLoopback(true)
} else if enable == "false" {
rule.ToggleAllowLoopback(false)
} else {
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
return
}
utils.SendOK(w)
}
}
// List all quick ban ip address
func handleListQuickBan(w http.ResponseWriter, r *http.Request) {
currentSummary := statisticCollector.GetCurrentDailySummary()
type quickBanEntry struct {
IpAddr string
Count int
CountryCode string
}
result := []quickBanEntry{}
currentSummary.RequestClientIp.Range(func(key, value interface{}) bool {
ip := key.(string)
count := value.(int)
thisEntry := quickBanEntry{
IpAddr: ip,
Count: count,
}
//Get the country code
geoinfo, err := geodbStore.ResolveCountryCodeFromIP(ip)
if err == nil {
thisEntry.CountryCode = geoinfo.CountryIsoCode
}
result = append(result, thisEntry)
return true
})
//Sort result based on count
sort.Slice(result, func(i, j int) bool {
return result[i].Count > result[j].Count
})
js, _ := json.Marshal(result)
utils.SendJSONResponse(w, string(js))
}

View File

@ -38,7 +38,21 @@ func initACME() *acme.ACMEHandler {
port = getRandomPort(30000)
}
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb)
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb, SystemWideLogger)
}
// Restart ACME handler and auto renewer
func restartACMEHandler() {
SystemWideLogger.Println("Restarting ACME handler")
//Clos the current handler and auto renewer
acmeHandler.Close()
acmeAutoRenewer.Close()
acmeDeregisterSpecialRoutingRule()
//Reinit the handler with a new random port
acmeHandler = initACME()
acmeRegisterSpecialRoutingRule()
}
// create the special routing rule for ACME
@ -82,12 +96,29 @@ func acmeRegisterSpecialRoutingRule() {
}
}
// remove the special routing rule for ACME
func acmeDeregisterSpecialRoutingRule() {
SystemWideLogger.Println("Removing ACME routing rule")
dynamicProxyRouter.RemoveRoutingRule("acme-autorenew")
}
// This function check if the renew setup is satisfied. If not, toggle them automatically
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
isForceHttpsRedirectEnabledOriginally := false
requireRestorePort80 := false
dnsPara, _ := utils.PostBool(r, "dns")
if !dnsPara {
if dynamicProxyRouter.Option.Port == 443 {
//Check if port 80 is enabled
if !dynamicProxyRouter.Option.ListenOnPort80 {
//Enable port 80 temporarily
SystemWideLogger.PrintAndLog("ACME", "Temporarily enabling port 80 listener to handle ACME request ", nil)
dynamicProxyRouter.UpdatePort80ListenerState(true)
requireRestorePort80 = true
time.Sleep(2 * time.Second)
}
//Enable port 80 to 443 redirect
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
@ -107,8 +138,8 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
}
}
//Add a 3 second delay to make sure everything is settle down
time.Sleep(3 * time.Second)
//Add a 2 second delay to make sure everything is settle down
time.Sleep(2 * time.Second)
// Pass over to the acmeHandler to deal with the communication
acmeHandler.HandleRenewCertificate(w, r)
@ -117,13 +148,17 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
tlsCertManager.UpdateLoadedCertList()
//Restore original settings
if dynamicProxyRouter.Option.Port == 443 && !dnsPara {
if !isForceHttpsRedirectEnabledOriginally {
//Default is off. Turn the redirection off
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
if requireRestorePort80 {
//Restore port 80 listener
SystemWideLogger.PrintAndLog("ACME", "Restoring previous port 80 listener settings", nil)
dynamicProxyRouter.UpdatePort80ListenerState(false)
}
if !isForceHttpsRedirectEnabledOriginally {
//Default is off. Turn the redirection off
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
}
// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation

View File

@ -2,12 +2,15 @@ package main
import (
"encoding/json"
"io/fs"
"net/http"
"net/http/pprof"
"imuslab.com/zoraxy/mod/acme/acmedns"
"imuslab.com/zoraxy/mod/acme/acmewizard"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/ipscan"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils"
@ -17,62 +20,40 @@ import (
API.go
This file contains all the API called by the web management interface
*/
var requireAuth = true
func initAPIs(targetMux *http.ServeMux) {
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
AuthAgent: authAgent,
RequireAuth: requireAuth,
TargetMux: targetMux,
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
},
})
//Register the standard web services urls
fs := http.FileServer(http.FS(webres))
if development {
fs = http.FileServer(http.Dir("web/"))
}
//Add a layer of middleware for advance control
advHandler := FSHandler(fs)
targetMux.Handle("/", advHandler)
//Authentication APIs
registerAuthAPIs(requireAuth, targetMux)
//Reverse proxy
// Register the APIs for HTTP proxy management functions
func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
/* Reverse Proxy Settings & Status */
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
authRouter.HandleFunc("/api/proxy/listTags", ReverseProxyListTags)
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS)
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
//Reverse proxy upstream (load balance) APIs
/* Reverse proxy upstream (load balance) */
authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd)
authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority)
authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete)
//Reverse proxy virtual directory APIs
/* Reverse proxy virtual directory */
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
//Reverse proxy user define header apis
/* Reverse proxy user-defined header */
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
@ -80,12 +61,15 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop)
authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite)
authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy)
//Reverse proxy auth related APIs
authRouter.HandleFunc("/api/proxy/header/handleWsHeaderBehavior", HandleWsHeaderBehavior)
/* Reverse proxy auth related */
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths)
}
//TLS / SSL config
// Register the APIs for TLS / SSL certificate management functions
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
@ -94,64 +78,77 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
}
//Redirection config
// Register the APIs for Authentication handlers like Authelia and OAUTH2
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS)
authRouter.HandleFunc("/api/sso/Authentik", authentikRouter.HandleSetAuthentikURLAndHTTPS)
}
// Register the APIs for redirection rules management functions
func RegisterRedirectionAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule)
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
}
//Access Rules API
// Register the APIs for access rules management functions
func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) {
/* Access Rules Settings & Status */
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
//Blacklist APIs
/* Blacklist */
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
//Whitelist APIs
/* Whitelist */
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback)
/* Quick Ban List */
authRouter.HandleFunc("/api/quickban/list", handleListQuickBan)
}
//Path Blocker APIs
// Register the APIs for path blocking rules management functions, WIP
func RegisterPathRuleAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath)
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath)
}
//Statistic & uptime monitoring API
// Register the APIs statistic anlysis and uptime monitoring functions
func RegisterStatisticalAPIs(authRouter *auth.RouterDef) {
/* Traffic Summary */
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
/* Zoraxy Analytic */
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
/* UpTime Monitor */
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
}
//Global Area Network APIs
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
authRouter.HandleFunc("/api/gan/network/list", ganManager.HandleListNetwork)
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
//Stream (TCP / UDP) Proxy
// Register the APIs for Stream (TCP / UDP) Proxy management functions
func RegisterStreamProxyAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
@ -159,20 +156,59 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
}
//mDNS APIs
// Register the APIs for mDNS service management functions
func RegisterMDNSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning)
}
//Zoraxy Analytic
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
// Register the APIs for ACME and Auto Renewer management functions
func RegisterACMEAndAutoRenewerAPIs(authRouter *auth.RouterDef) {
/* ACME Core */
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
/* Auto Renewer */
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HandleSetDNS)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
/* ACME Wizard */
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck)
}
//Network utilities
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
// Register the APIs for Static Web Server management functions
func RegisterStaticWebServerAPIs(authRouter *auth.RouterDef) {
/* Static Web Server Controls */
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
/* File Manager */
if *allowWebFileManager {
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
}
}
// Register the APIs for Network Utilities functions
func RegisterNetworkUtilsAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan)
authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort)
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
@ -185,66 +221,28 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
//Account Reset
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
//ACME & Auto Renewer
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HanldeSetDNS)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
//Static Web Server
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
if *allowWebFileManager {
//Web Directory Manager file operation functions
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
}
//Docker UX Optimizations
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
//Others
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
//If you got APIs to add, append them here
}
// Function to renders Auth related APIs
func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
//Auth APIs
func RegisterPluginAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/plugins/list", pluginManager.HandleListPlugins)
authRouter.HandleFunc("/api/plugins/enable", pluginManager.HandleEnablePlugin)
authRouter.HandleFunc("/api/plugins/disable", pluginManager.HandleDisablePlugin)
authRouter.HandleFunc("/api/plugins/icon", pluginManager.HandleLoadPluginIcon)
authRouter.HandleFunc("/api/plugins/info", pluginManager.HandlePluginInfo)
authRouter.HandleFunc("/api/plugins/groups/list", pluginManager.HandleListPluginGroups)
authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup)
authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup)
authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup)
authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins)
authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList)
authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin)
authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin)
}
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
func RegisterAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
targetMux.HandleFunc("/api/auth/login", authAgent.HandleLogin)
targetMux.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
targetMux.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
@ -260,21 +258,17 @@ func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
js, _ := json.Marshal(username)
utils.SendJSONResponse(w, string(js))
})
targetMux.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
uc := authAgent.GetUserCounts()
js, _ := json.Marshal(uc)
js, _ := json.Marshal(authAgent.GetUserCounts())
utils.SendJSONResponse(w, string(js))
})
targetMux.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
if authAgent.GetUserCounts() == 0 {
//Allow register root admin
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {
})
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {})
} else {
//This function is disabled
utils.SendErrorResponse(w, "Root management account already exists")
@ -315,5 +309,67 @@ func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
authAgent.UnregisterUser(username)
authAgent.CreateUserAccount(username, newPassword, "")
})
}
/* Register all the APIs */
func initAPIs(targetMux *http.ServeMux) {
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
AuthAgent: authAgent,
RequireAuth: requireAuth,
TargetMux: targetMux,
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
},
})
// Register the standard web services URLs
var staticWebRes http.Handler
if *development_build {
staticWebRes = http.FileServer(http.Dir("web/"))
} else {
subFS, err := fs.Sub(webres, "web")
if err != nil {
panic("Failed to strip 'web/' from embedded resources: " + err.Error())
}
staticWebRes = http.FileServer(http.FS(subFS))
}
//Add a layer of middleware for advance control
advHandler := FSHandler(staticWebRes)
targetMux.Handle("/", advHandler)
//Register the APIs
RegisterAuthAPIs(requireAuth, targetMux)
RegisterHTTPProxyAPIs(authRouter)
RegisterTLSAPIs(authRouter)
RegisterAuthenticationHandlerAPIs(authRouter)
RegisterRedirectionAPIs(authRouter)
RegisterAccessRuleAPIs(authRouter)
RegisterPathRuleAPIs(authRouter)
RegisterStatisticalAPIs(authRouter)
RegisterStreamProxyAPIs(authRouter)
RegisterMDNSAPIs(authRouter)
RegisterNetworkUtilsAPIs(authRouter)
RegisterACMEAndAutoRenewerAPIs(authRouter)
RegisterStaticWebServerAPIs(authRouter)
RegisterPluginAPIs(authRouter)
//Account Reset
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
//Docker UX Optimizations
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
//Others
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
}

View File

@ -177,7 +177,10 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) {
// Handle front-end toggling TLS mode
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
currentTlsSetting := false
currentTlsSetting := true //Default to true
if dynamicProxyRouter.Option != nil {
currentTlsSetting = dynamicProxyRouter.Option.UseTls
}
if sysdb.KeyExists("settings", "usetls") {
sysdb.Read("settings", "usetls", &currentTlsSetting)
}

View File

@ -48,18 +48,23 @@ func LoadReverseProxyConfig(configFilepath string) error {
}
//Parse it into dynamic proxy endpoint
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
thisConfigEndpoint := dynamicproxy.GetDefaultProxyEndpoint()
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
if err != nil {
return err
}
//Make sure the tags are not nil
if thisConfigEndpoint.Tags == nil {
thisConfigEndpoint.Tags = []string{}
}
//Matching domain not set. Assume root
if thisConfigEndpoint.RootOrMatchingDomain == "" {
thisConfigEndpoint.RootOrMatchingDomain = "/"
}
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root {
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
//This is a root config file
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
@ -68,7 +73,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host {
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost {
//This is a host config file
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
@ -97,7 +102,7 @@ func filterProxyConfigFilename(filename string) string {
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
//Get filename for saving
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
if endpoint.ProxyType == dynamicproxy.ProxyType_Root {
if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
filename = "./conf/proxy/root.config"
}
@ -129,27 +134,23 @@ func RemoveReverseProxyConfig(endpoint string) error {
// Get the default root config that point to the internal static web server
// this will be used if root config is not found (new deployment / missing root.config file)
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
//Default settings
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
ActiveOrigins: []*loadbalance.Upstream{
{
OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false,
SkipCertValidations: false,
Weight: 0,
},
//Get the default proxy endpoint
rootProxyEndpointConfig := dynamicproxy.GetDefaultProxyEndpoint()
rootProxyEndpointConfig.ProxyType = dynamicproxy.ProxyTypeRoot
rootProxyEndpointConfig.RootOrMatchingDomain = "/"
rootProxyEndpointConfig.ActiveOrigins = []*loadbalance.Upstream{
{
OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false,
SkipCertValidations: false,
Weight: 0,
},
InactiveOrigins: []*loadbalance.Upstream{},
BypassGlobalTLS: false,
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
RequireBasicAuth: false,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
})
}
rootProxyEndpointConfig.DefaultSiteOption = dynamicproxy.DefaultSite_InternalStaticWebServer
rootProxyEndpointConfig.DefaultSiteValue = ""
//Default settings
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&rootProxyEndpointConfig)
if err != nil {
return nil, err
}
@ -167,17 +168,20 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
if includeSysDBRaw == "true" {
//Include the system database in backup snapshot
//Temporary set it to read only
sysdb.ReadOnly = true
includeSysDB = true
}
// Specify the folder path to be zipped
folderPath := "./conf/"
if !utils.FileExists("./conf") {
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
return
}
folderPath := "./conf"
// Set the Content-Type header to indicate it's a zip file
w.Header().Set("Content-Type", "application/zip")
// Set the Content-Disposition header to specify the file name
w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"")
// Set the Content-Disposition header to specify the file name, add timestamp to the filename
w.Header().Set("Content-Disposition", "attachment; filename=\"zoraxy-config-"+time.Now().Format("2006-01-02-15-04-05")+".zip\"")
// Create a zip writer
zipWriter := zip.NewWriter(w)
@ -227,7 +231,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
}
// Open the file on disk
file, err := os.Open("sys.db")
file, err := os.Open("./sys.db")
if err != nil {
SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
return
@ -241,8 +245,6 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
return
}
//Restore sysdb state
sysdb.ReadOnly = false
}
if err != nil {
@ -278,6 +280,8 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
targetDir := "./conf"
if utils.FileExists(targetDir) {
//Backup the old config to old
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
//os.Rename(*path_conf, backupPath)
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
}

156
src/def.go Normal file
View File

@ -0,0 +1,156 @@
package main
/*
Type and flag definations
This file contains all the type and flag definations
Author: tobychui
*/
import (
"embed"
"flag"
"net/http"
"time"
"imuslab.com/zoraxy/mod/auth/sso/authentik"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/forwardproxy"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/info/logviewer"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
"imuslab.com/zoraxy/mod/plugins"
"imuslab.com/zoraxy/mod/sshprox"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/statistic/analytic"
"imuslab.com/zoraxy/mod/streamproxy"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/webserv"
)
const (
/* Build Constants */
SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.2.1"
/* System Constants */
TMP_FOLDER = "./tmp"
WEBSERV_DEFAULT_PORT = 5487
MDNS_HOSTNAME_PREFIX = "zoraxy_" /* Follow by node UUID */
MDNS_IDENTIFY_DEVICE_TYPE = "Network Gateway"
MDNS_IDENTIFY_DOMAIN = "zoraxy.aroz.org"
MDNS_IDENTIFY_VENDOR = "imuslab.com"
MDNS_SCAN_TIMEOUT = 30 /* Seconds */
MDNS_SCAN_UPDATE_INTERVAL = 15 /* Minutes */
GEODB_CACHE_CLEAR_INTERVAL = 15 /* Minutes */
ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json"
CSRF_COOKIENAME = "zoraxy_csrf"
LOG_PREFIX = "zr"
LOG_EXTENSION = ".log"
STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */
/* Configuration Folder Storage Path Constants */
CONF_HTTP_PROXY = "./conf/proxy"
CONF_STREAM_PROXY = "./conf/streamproxy"
CONF_CERT_STORE = "./conf/certs"
CONF_REDIRECTION = "./conf/redirect"
CONF_ACCESS_RULE = "./conf/access"
CONF_PATH_RULE = "./conf/rules/pathrules"
CONF_PLUGIN_GROUPS = "./conf/plugin_groups.json"
)
/* System Startup Flags */
var (
webUIPort = flag.String("port", ":8000", "Management web interface listening port")
databaseBackend = flag.String("db", "auto", "Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV")
noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
showver = flag.Bool("version", false, "Show version of this server")
allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
/* Default Configuration Flags */
defaultInboundPort = flag.Int("default_inbound_port", 443, "Default web server listening port")
defaultEnableInboundTraffic = flag.Bool("default_inbound_enabled", true, "If web server is enabled by default")
/* Path Configuration Flags */
//path_database = flag.String("dbpath", "./sys.db", "Database path")
//path_conf = flag.String("conf", "./conf", "Configuration folder path")
path_uuid = flag.String("uuid", "./sys.uuid", "sys.uuid file path")
path_logFile = flag.String("log", "./log", "Log folder path")
path_webserver = flag.String("webroot", "./www", "Static web server root folder. Only allow change in start paramters")
path_plugin = flag.String("plugin", "./plugins", "Plugin folder path")
/* Maintaince & Development Function Flags */
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
development_build = flag.Bool("dev", false, "Use external web folder for UI development")
)
/* Global Variables and Handlers */
var (
nodeUUID = "generic" //System uuid in uuidv4 format, load from database on startup
bootTime = time.Now().Unix()
requireAuth = true //Require authentication for webmin panel, override from flag
/*
Binary Embedding File System
*/
//go:embed web/*
webres embed.FS
/*
Handler Modules
*/
sysdb *database.Database //System database
authAgent *auth.AuthAgent //Authentication agent
tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets
webminPanelMux *http.ServeMux //Server mux for handling webmin panel APIs
csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
accessController *access.Controller //Access controller, handle black list and white list
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
statisticCollector *statistic.Collector //Collecting statistic from visitors
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
mdnsScanner *mdns.MDNSHost //mDNS discovery services
webSshManager *sshprox.Manager //Web SSH connection service
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
pluginManager *plugins.Manager //Plugin manager for managing plugins
//Authentication Provider
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
authentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
SystemWideLogger *logger.Logger //Logger for Zoraxy
LogViewer *logviewer.Viewer //Log viewer HTTP handlers
)

View File

@ -1,13 +1,14 @@
module imuslab.com/zoraxy
go 1.21
go 1.22.0
toolchain go1.22.2
require (
github.com/armon/go-radix v1.0.0
github.com/boltdb/bolt v1.3.1
github.com/docker/docker v27.0.0+incompatible
github.com/go-acme/lego/v4 v4.16.1
github.com/go-acme/lego/v4 v4.21.0
github.com/go-ping/ping v1.1.0
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2
@ -15,93 +16,109 @@ require (
github.com/grandcat/zeroconf v1.0.0
github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.26
golang.org/x/net v0.25.0
golang.org/x/sys v0.20.0
golang.org/x/text v0.15.0
github.com/shirou/gopsutil/v4 v4.25.1
github.com/syndtr/goleveldb v1.0.0
golang.org/x/net v0.33.0
golang.org/x/text v0.21.0
)
require (
cloud.google.com/go/compute v1.25.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
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
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
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/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
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 // indirect
github.com/aws/aws-sdk-go-v2 v1.32.7 // indirect
github.com/aws/aws-sdk-go-v2/config v1.28.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.48 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect
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/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
github.com/civo/civogo v0.3.11 // indirect
github.com/cloudflare/cloudflare-go v0.86.0 // indirect
github.com/cloudflare/cloudflare-go v0.112.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.9.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dnsimple/dnsimple-go v1.2.0 // indirect
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/exoscale/egoscale v0.102.3 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
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-resty/resty/v2 v2.11.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // 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.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
github.com/gophercloud/gophercloud v1.0.0 // indirect
github.com/gorilla/csrf v1.7.2 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gorilla/csrf v1.7.2
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
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.5 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // 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
@ -111,11 +128,11 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/linode/linodego v1.28.0 // indirect
github.com/linode/linodego v1.44.0 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -126,66 +143,62 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
github.com/nrdcg/desec v0.7.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
github.com/nrdcg/desec v0.10.0 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.2.0 // indirect
github.com/nrdcg/freemyip v0.3.0 // indirect
github.com/nrdcg/goinwx v0.10.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/porkbun v0.3.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
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/ovh/go-ovh v1.4.3 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/ovh/go-ovh v1.6.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.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.4.0 // indirect
github.com/sacloud/api-client-go v0.2.8 // indirect
github.com/sacloud/go-http v0.1.6 // indirect
github.com/sacloud/iaas-api-go v1.11.1 // indirect
github.com/sacloud/packages-go v0.0.9 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // 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/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.3 // 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.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect
github.com/transip/gotransip/v6 v6.23.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // 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/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/v2 v2.17.2 // indirect
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
go.opentelemetry.io/otel v1.27.0 // 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
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
go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/otel/sdk v1.27.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect
go.uber.org/ratelimit v0.2.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/api v0.169.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
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/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect
google.golang.org/api v0.214.0 // indirect
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect
gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect

Some files were not shown because too many files have changed in this diff Show More