124 Commits

Author SHA1 Message Date
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
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
199 changed files with 77314 additions and 52623 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.

13
.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,13 @@ 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

View File

@ -1,3 +1,63 @@
# 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

View File

@ -33,6 +33,7 @@ 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
@ -100,10 +101,20 @@ 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
@ -114,12 +125,16 @@ Usage of zoraxy:
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 change in start parameters (default "./www")
Static web server root folder. Only allow change in start paramters (default "./www")
-ztauth string
ZeroTier authtoken for the local node
-ztport int
@ -134,7 +149,8 @@ If you already have an upstream reverse proxy server in place with permission ma
./zoraxy -noauth=true
```
*Note: 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.*
> [!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 +170,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,7 +191,7 @@ 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

View File

@ -32,7 +32,7 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne
FROM docker.io/ubuntu:latest
RUN apt-get update -y &&\
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates openssh-server
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
@ -44,6 +44,7 @@ ENV ZEROTIER="false"
ENV AUTORENEW="86400"
ENV CFGUPGRADE="true"
ENV DB="auto"
ENV DOCKER="true"
ENV EARLYRENEW="30"
ENV FASTGEOIP="false"
@ -52,13 +53,14 @@ ENV MDNSNAME="''"
ENV NOAUTH="false"
ENV PORT="8000"
ENV SSHLB="false"
ENV UPDATE_GEOIP="false"
ENV VERSION="false"
ENV WEBFM="true"
ENV WEBROOT="./www"
ENV ZTAUTH=""
ENV ZTPORT="9993"
VOLUME [ "/opt/zoraxy/config/", "/var/lib/zerotier-one/" ]
VOLUME [ "/opt/zoraxy/config/" ]
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]

View File

@ -9,7 +9,7 @@
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/).
In the examples below, make sure to update `/path/to/zoraxy/config/` with your actual path. 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.
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.
@ -23,11 +23,9 @@ docker run -d \
-p 443:443 \
-p 8000:8000 \
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
-v /path/to/zerotier/config/:/var/lib/zerotier-one/ \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /etc/localtime:/etc/localtime \
-e FASTGEOIP="true" \
-e ZEROTIER="true" \
zoraxydocker/zoraxy:latest
```
@ -45,12 +43,10 @@ services:
- 8000:8000
volumes:
- /path/to/zoraxy/config/:/opt/zoraxy/config/
- /path/to/zerotier/config/:/var/lib/zerotier-one/
- /var/run/docker.sock:/var/run/docker.sock
- /etc/localtime:/etc/localtime
environment:
FASTGEOIP: "true"
ZEROTIER: "true"
```
### Ports
@ -66,7 +62,6 @@ services:
| Volume | Details |
|:-|:-|
| `/opt/zoraxy/config/` | Zoraxy configuration. |
| `/var/lib/zerotier-one/` | ZeroTier configuration. Only required if you wish to use ZeroTier. |
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
@ -78,6 +73,7 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
|:-|:-|:-|
| `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). |
@ -86,6 +82,7 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
| `NOAUTH` | `false` (Boolean) | Disable authentication for management interface. |
| `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. |
@ -96,3 +93,12 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
> [!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.
### Building
To build the Docker image:
- Check out the repository/branch.
- Copy the Zoraxy `src/` 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.

View File

@ -1,17 +1,35 @@
#!/usr/bin/env bash
trap cleanup TERM INT
cleanup() {
echo "Shutting down..."
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
exit 0
}
update-ca-certificates
echo "CA certificates updated"
echo "CA certificates updated."
zoraxy -update_geoip=true
echo "Updated GeoIP data."
if [ "$ZEROTIER" = "true" ]; then
zerotier-one -d
echo "ZeroTier daemon started"
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..."
exec zoraxy \
zoraxy \
-autorenew="$AUTORENEW" \
-cfgupgrade="$CFGUPGRADE" \
-db="$DB" \
-docker="$DOCKER" \
-earlyrenew="$EARLYRENEW" \
-fastgeoip="$FASTGEOIP" \
@ -20,9 +38,15 @@ exec zoraxy \
-noauth="$NOAUTH" \
-port=:"$PORT" \
-sshlb="$SSHLB" \
-update_geoip="$UPDATE_GEOIP" \
-version="$VERSION" \
-webfm="$WEBFM" \
-webroot="$WEBROOT" \
-ztauth="$ZTAUTH" \
-ztport="$ZTPORT"
-ztport="$ZTPORT" \
&
zoraxypid=$!
wait $zoraxypid
wait $zerotierpid

View File

@ -0,0 +1,22 @@
#!/bin/bash
# Iterate over all directories in the current directory
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,70 @@
package main
import (
"fmt"
"net/http"
"strconv"
plugin "aroz.org/zoraxy/debugger/mod/zoraxy_plugin"
)
const (
PLUGIN_ID = "org.aroz.zoraxy.debugger"
UI_PATH = "/debug"
)
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,
GlobalCapturePaths: []plugin.CaptureRule{
{
CapturePath: "/debug_test", //Capture all traffic of all HTTP proxy rule
IncludeSubPaths: true,
},
},
GlobalCaptureIngress: "",
AlwaysCapturePaths: []plugin.CaptureRule{},
AlwaysCaptureIngress: "",
UIPath: UI_PATH,
/*
SubscriptionPath: "/subept",
SubscriptionsEvents: []plugin.SubscriptionEvent{
*/
})
if err != nil {
//Terminate or enter standalone mode here
panic(err)
}
// Register the shutdown handler
plugin.RegisterShutdownHandler(func() {
// Do cleanup here if needed
fmt.Println("Debugger Terminated")
})
http.HandleFunc(UI_PATH+"/", RenderDebugUI)
http.HandleFunc("/gcapture", HandleIngressCapture)
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 HandleIngressCapture(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Capture request received")
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("This request is captured by the debugger"))
}

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,106 @@
package zoraxy_plugin
import (
"embed"
"fmt"
"io/fs"
"net/http"
"net/url"
"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
}
// 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, "/") {
// Redirect to the index.html
http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound)
return
}
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)
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(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
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL, _ = url.Parse(rewrittenURL)
r.RequestURI = rewrittenURL
//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)
})
}

View File

@ -0,0 +1,198 @@
package zoraxy_plugin
import (
"encoding/json"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
)
/*
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 CaptureRule struct {
CapturePath string `json:"capture_path"`
IncludeSubPaths bool `json:"include_sub_paths"`
}
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"`
}
/*
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
*/
/*
Global Capture Settings
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
This captures the whole traffic of Zoraxy
*/
GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
/*
Always Capture Settings
Once the plugin is enabled on a given HTTP Proxy rule,
these always applies
*/
AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_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()
}
/*
Shutdown handler
This function will register a shutdown handler for the plugin
The shutdown callback will be called when the plugin is shutting down
You can use this to clean up resources like closing database connections
*/
func RegisterShutdownHandler(shutdownCallback func()) {
// Set up a channel to receive OS signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Start a goroutine to listen for signals
go func() {
<-sigChan
shutdownCallback()
os.Exit(0)
}()
}

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/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,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>

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,128 @@
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
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, "/") {
// Redirect to the index.html
http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound)
return
}
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)
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(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
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL, _ = url.Parse(rewrittenURL)
r.RequestURI = rewrittenURL
//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)
}()
})
}

View File

@ -0,0 +1,174 @@
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 CaptureRule struct {
CapturePath string `json:"capture_path"`
IncludeSubPaths bool `json:"include_sub_paths"`
}
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"`
}
/*
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
*/
/*
Global Capture Settings
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
This captures the whole traffic of Zoraxy
*/
GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
/*
Always Capture Settings
Once the plugin is enabled on a given HTTP Proxy rule,
these always applies
*/
AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_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,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,81 @@
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)
// 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
http.Handle(UI_RELPATH+"/", uiRouter.Handler())
// 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

@ -0,0 +1,80 @@
package ganserv
import (
"errors"
"log"
"os"
"runtime"
"strings"
)
func TryLoadorAskUserForAuthkey() (string, error) {
//Check for zt auth token
value, exists := os.LookupEnv("ZT_AUTH")
if !exists {
log.Println("Environment variable ZT_AUTH not defined. Trying to load authtoken from file.")
} else {
return value, nil
}
authKey := ""
if runtime.GOOS == "windows" {
if isAdmin() {
//Read the secret file directly
b, err := os.ReadFile("C:\\ProgramData\\ZeroTier\\One\\authtoken.secret")
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = string(b)
} else {
log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
}
} else {
//Elavate the permission to admin
ak, err := readAuthTokenAsAdmin()
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = ak
} else {
log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
}
}
} else if runtime.GOOS == "linux" {
if isAdmin() {
//Try to read from source using sudo
ak, err := readAuthTokenAsAdmin()
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = strings.TrimSpace(ak)
} else {
log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
}
} else {
//Try read from source
b, err := os.ReadFile("/var/lib/zerotier-one/authtoken.secret")
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = string(b)
} else {
log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
}
}
} else if runtime.GOOS == "darwin" {
b, err := os.ReadFile("/Library/Application Support/ZeroTier/One/authtoken.secret")
if err == nil {
log.Println("Zerotier authkey loaded")
authKey = string(b)
} else {
log.Println("Unable to read authkey at /Library/Application Support/ZeroTier/One/authtoken.secret ", err.Error())
}
}
authKey = strings.TrimSpace(authKey)
if authKey == "" {
return "", errors.New("Unable to load authkey from file")
}
return authKey, nil
}

View File

@ -0,0 +1,37 @@
//go:build linux
// +build linux
package ganserv
import (
"os"
"os/exec"
"os/user"
"strings"
"aroz.org/zoraxy/ztnc/mod/utils"
)
func readAuthTokenAsAdmin() (string, error) {
if utils.FileExists("./conf/authtoken.secret") {
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err == nil {
return strings.TrimSpace(string(authKey)), nil
}
}
cmd := exec.Command("sudo", "cat", "/var/lib/zerotier-one/authtoken.secret")
output, err := cmd.Output()
if err != nil {
return "", err
}
return string(output), nil
}
func isAdmin() bool {
currentUser, err := user.Current()
if err != nil {
return false
}
return currentUser.Username == "root"
}

View File

@ -0,0 +1,73 @@
//go:build windows
// +build windows
package ganserv
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"syscall"
"time"
"aroz.org/zoraxy/ztnc/mod/utils"
"golang.org/x/sys/windows"
)
// Use admin permission to read auth token on Windows
func readAuthTokenAsAdmin() (string, error) {
//Check if the previous startup already extracted the authkey
if utils.FileExists("./conf/authtoken.secret") {
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err == nil {
return strings.TrimSpace(string(authKey)), nil
}
}
verb := "runas"
exe := "cmd.exe"
cwd, _ := os.Getwd()
output, _ := filepath.Abs(filepath.Join("./conf/", "authtoken.secret"))
os.WriteFile(output, []byte(""), 0775)
args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"")
verbPtr, _ := syscall.UTF16PtrFromString(verb)
exePtr, _ := syscall.UTF16PtrFromString(exe)
cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
argPtr, _ := syscall.UTF16PtrFromString(args)
var showCmd int32 = 1 //SW_NORMAL
err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd)
if err != nil {
return "", err
}
log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData")
retry := 0
time.Sleep(3 * time.Second)
for !utils.FileExists("./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
}
return strings.TrimSpace(string(authKey)), nil
}
// Check if admin on Windows
func isAdmin() bool {
_, err := os.Open("\\\\.\\PHYSICALDRIVE0")
if err != nil {
return false
}
return true
}

View File

@ -0,0 +1,130 @@
package ganserv
import (
"log"
"net"
"aroz.org/zoraxy/ztnc/mod/database"
)
/*
Global Area Network
Server side implementation
This module do a few things to help manage
the system GANs
- Provide DHCP assign to client
- Provide a list of connected nodes in the same VLAN
- Provide proxy of packet if the target VLAN is online but not reachable
Also provide HTTP Handler functions for management
- Create Network
- Update Network Properties (Name / Desc)
- Delete Network
- Authorize Node
- Deauthorize Node
- Set / Get Network Prefered Subnet Mask
- Handle Node ping
*/
type Node struct {
Auth bool //If the node is authorized in this network
ClientID string //The client ID
MAC string //The tap MAC this client is using
Name string //Name of the client in this network
Description string //Description text
ManagedIP net.IP //The IP address assigned by this network
LastSeen int64 //Last time it is seen from this host
ClientVersion string //Client application version
PublicIP net.IP //Public IP address as seen from this host
}
type Network struct {
UID string //UUID of the network, must be a 16 char random ASCII string
Name string //Name of the network, ASCII only
Description string //Description of the network
CIDR string //The subnet masked use by this network
Nodes []*Node //The nodes currently attached in this network
}
type NetworkManagerOptions struct {
Database *database.Database
AuthToken string
ApiPort int
}
type NetworkMetaData struct {
Desc string
}
type MemberMetaData struct {
Name string
}
type NetworkManager struct {
authToken string
apiPort int
ControllerID string
option *NetworkManagerOptions
networksMetadata map[string]NetworkMetaData
}
// Create a new GAN manager
func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
option.Database.NewTable("ganserv")
//Load network metadata
networkMeta := map[string]NetworkMetaData{}
if option.Database.KeyExists("ganserv", "networkmeta") {
option.Database.Read("ganserv", "networkmeta", &networkMeta)
}
//Start the zerotier instance if not exists
//Get controller info
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
if err != nil {
log.Println("ZeroTier connection failed: ", err.Error())
return &NetworkManager{
authToken: option.AuthToken,
apiPort: option.ApiPort,
ControllerID: "",
option: option,
networksMetadata: networkMeta,
}
}
return &NetworkManager{
authToken: option.AuthToken,
apiPort: option.ApiPort,
ControllerID: instanceInfo.Address,
option: option,
networksMetadata: networkMeta,
}
}
func (m *NetworkManager) GetNetworkMetaData(netid string) *NetworkMetaData {
md, ok := m.networksMetadata[netid]
if !ok {
return &NetworkMetaData{}
}
return &md
}
func (m *NetworkManager) WriteNetworkMetaData(netid string, meta *NetworkMetaData) {
m.networksMetadata[netid] = *meta
m.option.Database.Write("ganserv", "networkmeta", m.networksMetadata)
}
func (m *NetworkManager) GetMemberMetaData(netid string, memid string) *MemberMetaData {
thisMemberData := MemberMetaData{}
m.option.Database.Read("ganserv", "memberdata_"+netid+"_"+memid, &thisMemberData)
return &thisMemberData
}
func (m *NetworkManager) WriteMemeberMetaData(netid string, memid string, meta *MemberMetaData) {
m.option.Database.Write("ganserv", "memberdata_"+netid+"_"+memid, meta)
}

View File

@ -0,0 +1,504 @@
package ganserv
import (
"encoding/json"
"net"
"net/http"
"regexp"
"strings"
"aroz.org/zoraxy/ztnc/mod/utils"
)
func (m *NetworkManager) HandleGetNodeID(w http.ResponseWriter, r *http.Request) {
if m.ControllerID == "" {
//Node id not exists. Check again
instanceInfo, err := getControllerInfo(m.option.AuthToken, m.option.ApiPort)
if err != nil {
utils.SendErrorResponse(w, "unable to access node id information")
return
}
m.ControllerID = instanceInfo.Address
}
js, _ := json.Marshal(m.ControllerID)
utils.SendJSONResponse(w, string(js))
}
func (m *NetworkManager) HandleAddNetwork(w http.ResponseWriter, r *http.Request) {
networkInfo, err := m.createNetwork()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Network created. Assign it the standard network settings
err = m.configureNetwork(networkInfo.Nwid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
// Return the new network ID
js, _ := json.Marshal(networkInfo.Nwid)
utils.SendJSONResponse(w, string(js))
}
func (m *NetworkManager) HandleRemoveNetwork(w http.ResponseWriter, r *http.Request) {
networkID, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty network id given")
return
}
if !m.networkExists(networkID) {
utils.SendErrorResponse(w, "network id not exists")
return
}
err = m.deleteNetwork(networkID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
}
utils.SendOK(w)
}
func (m *NetworkManager) HandleListNetwork(w http.ResponseWriter, r *http.Request) {
netid, _ := utils.GetPara(r, "netid")
if netid != "" {
targetNetInfo, err := m.getNetworkInfoById(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(targetNetInfo)
utils.SendJSONResponse(w, string(js))
} else {
// Return the list of networks as JSON
networkIds, err := m.listNetworkIds()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
networkInfos := []*NetworkInfo{}
for _, id := range networkIds {
thisNetInfo, err := m.getNetworkInfoById(id)
if err == nil {
networkInfos = append(networkInfos, thisNetInfo)
}
}
js, _ := json.Marshal(networkInfos)
utils.SendJSONResponse(w, string(js))
}
}
func (m *NetworkManager) HandleNetworkNaming(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "network id not given")
return
}
if !m.networkExists(netid) {
utils.SendErrorResponse(w, "network not eixsts")
}
newName, _ := utils.PostPara(r, "name")
newDesc, _ := utils.PostPara(r, "desc")
if newName != "" && newDesc != "" {
//Strip away html from name and desc
re := regexp.MustCompile("<[^>]*>")
newName := re.ReplaceAllString(newName, "")
newDesc := re.ReplaceAllString(newDesc, "")
//Set the new network name and desc
err = m.setNetworkNameAndDescription(netid, newName, newDesc)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
} else {
//Get current name and description
name, desc, err := m.getNetworkNameAndDescription(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal([]string{name, desc})
utils.SendJSONResponse(w, string(js))
}
}
func (m *NetworkManager) HandleNetworkDetails(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "netid not given")
return
}
targetNetwork, err := m.getNetworkInfoById(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(targetNetwork)
utils.SendJSONResponse(w, string(js))
}
func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "netid not given")
return
}
cidr, err := utils.PostPara(r, "cidr")
if err != nil {
utils.SendErrorResponse(w, "cidr not given")
return
}
ipstart, err := utils.PostPara(r, "ipstart")
if err != nil {
utils.SendErrorResponse(w, "ipstart not given")
return
}
ipend, err := utils.PostPara(r, "ipend")
if err != nil {
utils.SendErrorResponse(w, "ipend not given")
return
}
//Validate the CIDR is real, the ip range is within the CIDR range
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
utils.SendErrorResponse(w, "invalid cidr string given")
return
}
startIP := net.ParseIP(ipstart)
endIP := net.ParseIP(ipend)
if startIP == nil || endIP == nil {
utils.SendErrorResponse(w, "invalid start or end ip given")
return
}
withinRange := ipnet.Contains(startIP) && ipnet.Contains(endIP)
if !withinRange {
utils.SendErrorResponse(w, "given CIDR did not cover all of the start to end ip range")
return
}
err = m.configureNetwork(netid, startIP.String(), endIP.String(), strings.TrimSpace(cidr))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle listing of network members. Set details=true for listing all details
func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) {
netid, err := utils.GetPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "netid is empty")
return
}
details, _ := utils.GetPara(r, "detail")
memberIds, err := m.getNetworkMembers(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if details == "" {
//Only show client ids
js, _ := json.Marshal(memberIds)
utils.SendJSONResponse(w, string(js))
} else {
//Show detail members info
detailMemberInfo := []*MemberInfo{}
for _, thisMemberId := range memberIds {
memInfo, err := m.getNetworkMemberInfo(netid, thisMemberId)
if err == nil {
detailMemberInfo = append(detailMemberInfo, memInfo)
}
}
js, _ := json.Marshal(detailMemberInfo)
utils.SendJSONResponse(w, string(js))
}
}
// Handle Authorization of members
func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
memberid, err := utils.PostPara(r, "memid")
if err != nil {
utils.SendErrorResponse(w, "memid not set")
return
}
//Check if the target memeber exists
if !m.memberExistsInNetwork(netid, memberid) {
utils.SendErrorResponse(w, "member not exists in given network")
return
}
setAuthorized, err := utils.PostPara(r, "auth")
if err != nil || setAuthorized == "" {
//Get the member authorization state
memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(memberInfo.Authorized)
utils.SendJSONResponse(w, string(js))
} else if setAuthorized == "true" {
m.AuthorizeMember(netid, memberid, true)
} else if setAuthorized == "false" {
m.AuthorizeMember(netid, memberid, false)
} else {
utils.SendErrorResponse(w, "unknown operation state: "+setAuthorized)
}
}
// Handle Delete or Add IP for a member in a network
func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
memberid, err := utils.PostPara(r, "memid")
if err != nil {
utils.SendErrorResponse(w, "memid not set")
return
}
opr, err := utils.PostPara(r, "opr")
if err != nil {
utils.SendErrorResponse(w, "opr not defined")
return
}
targetip, _ := utils.PostPara(r, "ip")
memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if opr == "add" {
if targetip == "" {
utils.SendErrorResponse(w, "ip not set")
return
}
if !isValidIPAddr(targetip) {
utils.SendErrorResponse(w, "ip address not valid")
return
}
newIpList := append(memberInfo.IPAssignments, targetip)
err = m.setAssignedIps(netid, memberid, newIpList)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
} else if opr == "del" {
if targetip == "" {
utils.SendErrorResponse(w, "ip not set")
return
}
//Delete user ip from the list
newIpList := []string{}
for _, thisIp := range memberInfo.IPAssignments {
if thisIp != targetip {
newIpList = append(newIpList, thisIp)
}
}
err = m.setAssignedIps(netid, memberid, newIpList)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
} else if opr == "get" {
js, _ := json.Marshal(memberInfo.IPAssignments)
utils.SendJSONResponse(w, string(js))
} else {
utils.SendErrorResponse(w, "unsupported opr type: "+opr)
}
}
// Handle naming for members
func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
memberid, err := utils.PostPara(r, "memid")
if err != nil {
utils.SendErrorResponse(w, "memid not set")
return
}
if !m.memberExistsInNetwork(netid, memberid) {
utils.SendErrorResponse(w, "target member not exists in given network")
return
}
//Read memeber data
targetMemberData := m.GetMemberMetaData(netid, memberid)
newname, err := utils.PostPara(r, "name")
if err != nil {
//Send over the member data
js, _ := json.Marshal(targetMemberData)
utils.SendJSONResponse(w, string(js))
} else {
//Write member data
targetMemberData.Name = newname
m.WriteMemeberMetaData(netid, memberid, targetMemberData)
utils.SendOK(w)
}
}
// Handle delete of a given memver
func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
memberid, err := utils.PostPara(r, "memid")
if err != nil {
utils.SendErrorResponse(w, "memid not set")
return
}
//Check if that member is authorized.
memberInfo, err := m.getNetworkMemberInfo(netid, memberid)
if err != nil {
utils.SendErrorResponse(w, "member not exists in given GANet")
return
}
if memberInfo.Authorized {
//Deauthorized this member before deleting
m.AuthorizeMember(netid, memberid, false)
}
//Remove the memeber
err = m.deleteMember(netid, memberid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Check if a given network id is a network hosted on this zoraxy node
func (m *NetworkManager) IsLocalGAN(networkId string) bool {
networks, err := m.listNetworkIds()
if err != nil {
return false
}
for _, network := range networks {
if network == networkId {
return true
}
}
return false
}
// Handle server instant joining a given network
func (m *NetworkManager) HandleServerJoinNetwork(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
//Check if the target network is a network hosted on this server
if !m.IsLocalGAN(netid) {
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
return
}
if m.memberExistsInNetwork(netid, m.ControllerID) {
utils.SendErrorResponse(w, "controller already inside network")
return
}
//Join the network
err = m.joinNetwork(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle server instant leaving a given network
func (m *NetworkManager) HandleServerLeaveNetwork(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
//Check if the target network is a network hosted on this server
if !m.IsLocalGAN(netid) {
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
return
}
//Leave the network
err = m.leaveNetwork(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Remove it from target network if it is authorized
err = m.deleteMember(netid, m.ControllerID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}

View File

@ -0,0 +1,39 @@
package ganserv
import (
"fmt"
"math/rand"
"net"
"time"
)
//Get a random free IP from the pool
func (n *Network) GetRandomFreeIP() (net.IP, error) {
// Get all IP addresses in the subnet
ips, err := GetAllAddressFromCIDR(n.CIDR)
if err != nil {
return nil, err
}
// Filter out used IPs
usedIPs := make(map[string]bool)
for _, node := range n.Nodes {
usedIPs[node.ManagedIP.String()] = true
}
availableIPs := []string{}
for _, ip := range ips {
if !usedIPs[ip] {
availableIPs = append(availableIPs, ip)
}
}
// Randomly choose an available IP
if len(availableIPs) == 0 {
return nil, fmt.Errorf("no available IP")
}
rand.Seed(time.Now().UnixNano())
randIndex := rand.Intn(len(availableIPs))
pickedFreeIP := availableIPs[randIndex]
return net.ParseIP(pickedFreeIP), nil
}

View File

@ -0,0 +1,55 @@
package ganserv_test
import (
"fmt"
"net"
"strconv"
"testing"
"aroz.org/zoraxy/ztnc/mod/ganserv"
)
func TestGetRandomFreeIP(t *testing.T) {
n := ganserv.Network{
CIDR: "172.16.0.0/12",
Nodes: []*ganserv.Node{
{
Name: "nodeC1",
ManagedIP: net.ParseIP("172.16.1.142"),
},
{
Name: "nodeC2",
ManagedIP: net.ParseIP("172.16.5.174"),
},
},
}
// Call the function for 10 times
for i := 0; i < 10; i++ {
freeIP, err := n.GetRandomFreeIP()
fmt.Println("["+strconv.Itoa(i)+"] Free IP address assigned: ", freeIP)
// Assert that no error occurred
if err != nil {
t.Errorf("Unexpected error: %s", err.Error())
}
// Assert that the returned IP is a valid IPv4 address
if freeIP.To4() == nil {
t.Errorf("Invalid IP address format: %s", freeIP.String())
}
// Assert that the returned IP is not already used by a node
for _, node := range n.Nodes {
if freeIP.Equal(node.ManagedIP) {
t.Errorf("Returned IP is already in use: %s", freeIP.String())
}
}
n.Nodes = append(n.Nodes, &ganserv.Node{
Name: "NodeT" + strconv.Itoa(i),
ManagedIP: freeIP,
})
}
}

View File

@ -0,0 +1,55 @@
package ganserv
import (
"net"
)
//Generate all ip address from a CIDR
func GetAllAddressFromCIDR(cidr string) ([]string, error) {
ip, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return nil, err
}
var ips []string
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
ips = append(ips, ip.String())
}
// remove network address and broadcast address
return ips[1 : len(ips)-1], nil
}
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}
func isValidIPAddr(ipAddr string) bool {
ip := net.ParseIP(ipAddr)
if ip == nil {
return false
}
return true
}
func ipWithinCIDR(ipAddr string, cidr string) bool {
// Parse the CIDR string
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return false
}
// Parse the IP address
ip := net.ParseIP(ipAddr)
if ip == nil {
return false
}
// Check if the IP address is in the CIDR range
return ipNet.Contains(ip)
}

View File

@ -0,0 +1,669 @@
package ganserv
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
)
/*
zerotier.go
This hold the functions that required to communicate with
a zerotier instance
See more on
https://docs.zerotier.com/self-hosting/network-controllers/
*/
type NodeInfo struct {
Address string `json:"address"`
Clock int64 `json:"clock"`
Config struct {
Settings struct {
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,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"`
PlanetWorldID int `json:"planetWorldId"`
PlanetWorldTimestamp int64 `json:"planetWorldTimestamp"`
PublicIdentity string `json:"publicIdentity"`
TCPFallbackActive bool `json:"tcpFallbackActive"`
Version string `json:"version"`
VersionBuild int `json:"versionBuild"`
VersionMajor int `json:"versionMajor"`
VersionMinor int `json:"versionMinor"`
VersionRev int `json:"versionRev"`
}
type ErrResp struct {
Message string `json:"message"`
}
type NetworkInfo struct {
AuthTokens []interface{} `json:"authTokens"`
AuthorizationEndpoint string `json:"authorizationEndpoint"`
Capabilities []interface{} `json:"capabilities"`
ClientID string `json:"clientId"`
CreationTime int64 `json:"creationTime"`
DNS []interface{} `json:"dns"`
EnableBroadcast bool `json:"enableBroadcast"`
ID string `json:"id"`
IPAssignmentPools []interface{} `json:"ipAssignmentPools"`
Mtu int `json:"mtu"`
MulticastLimit int `json:"multicastLimit"`
Name string `json:"name"`
Nwid string `json:"nwid"`
Objtype string `json:"objtype"`
Private bool `json:"private"`
RemoteTraceLevel int `json:"remoteTraceLevel"`
RemoteTraceTarget interface{} `json:"remoteTraceTarget"`
Revision int `json:"revision"`
Routes []interface{} `json:"routes"`
Rules []struct {
Not bool `json:"not"`
Or bool `json:"or"`
Type string `json:"type"`
} `json:"rules"`
RulesSource string `json:"rulesSource"`
SsoEnabled bool `json:"ssoEnabled"`
Tags []interface{} `json:"tags"`
V4AssignMode struct {
Zt bool `json:"zt"`
} `json:"v4AssignMode"`
V6AssignMode struct {
SixPlane bool `json:"6plane"`
Rfc4193 bool `json:"rfc4193"`
Zt bool `json:"zt"`
} `json:"v6AssignMode"`
}
type MemberInfo struct {
ActiveBridge bool `json:"activeBridge"`
Address string `json:"address"`
AuthenticationExpiryTime int `json:"authenticationExpiryTime"`
Authorized bool `json:"authorized"`
Capabilities []interface{} `json:"capabilities"`
CreationTime int64 `json:"creationTime"`
ID string `json:"id"`
Identity string `json:"identity"`
IPAssignments []string `json:"ipAssignments"`
LastAuthorizedCredential interface{} `json:"lastAuthorizedCredential"`
LastAuthorizedCredentialType string `json:"lastAuthorizedCredentialType"`
LastAuthorizedTime int `json:"lastAuthorizedTime"`
LastDeauthorizedTime int `json:"lastDeauthorizedTime"`
NoAutoAssignIps bool `json:"noAutoAssignIps"`
Nwid string `json:"nwid"`
Objtype string `json:"objtype"`
RemoteTraceLevel int `json:"remoteTraceLevel"`
RemoteTraceTarget interface{} `json:"remoteTraceTarget"`
Revision int `json:"revision"`
SsoExempt bool `json:"ssoExempt"`
Tags []interface{} `json:"tags"`
VMajor int `json:"vMajor"`
VMinor int `json:"vMinor"`
VProto int `json:"vProto"`
VRev int `json:"vRev"`
}
// Get the zerotier node info from local service
func getControllerInfo(token string, apiPort int) (*NodeInfo, error) {
url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-ZT1-AUTH", token)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
//Read from zerotier service instance
defer resp.Body.Close()
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
//Parse the payload into struct
thisInstanceInfo := NodeInfo{}
err = json.Unmarshal(payload, &thisInstanceInfo)
if err != nil {
return nil, err
}
return &thisInstanceInfo, nil
}
/*
Network Functions
*/
//Create a zerotier network
func (m *NetworkManager) createNetwork() (*NetworkInfo, error) {
url := fmt.Sprintf("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/%s______", m.ControllerID)
data := []byte(`{}`)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
networkInfo := NetworkInfo{}
err = json.Unmarshal(payload, &networkInfo)
if err != nil {
return nil, err
}
return &networkInfo, nil
}
// List network details
func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) {
req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil)
if err != nil {
return nil, err
}
req.Header.Set("X-Zt1-Auth", m.authToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
thisNetworkInfo := NetworkInfo{}
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(payload, &thisNetworkInfo)
if err != nil {
return nil, err
}
return &thisNetworkInfo, nil
}
func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *NetworkInfo) error {
payloadBytes, err := json.Marshal(newNetworkInfo)
if err != nil {
return err
}
payloadBuffer := bytes.NewBuffer(payloadBytes)
// Create the HTTP request
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/"
req, err := http.NewRequest("POST", url, payloadBuffer)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", m.authToken)
req.Header.Set("Content-Type", "application/json")
// Send the HTTP request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// List network IDs
func (m *NetworkManager) listNetworkIds() ([]string, error) {
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
if err != nil {
return []string{}, err
}
req.Header.Set("X-Zt1-Auth", m.authToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return []string{}, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return []string{}, errors.New("network error")
}
networkIds := []string{}
payload, err := io.ReadAll(resp.Body)
if err != nil {
return []string{}, err
}
err = json.Unmarshal(payload, &networkIds)
if err != nil {
return []string{}, err
}
return networkIds, nil
}
// wrapper for checking if a network id exists
func (m *NetworkManager) networkExists(networkId string) bool {
networkIds, err := m.listNetworkIds()
if err != nil {
return false
}
for _, thisid := range networkIds {
if thisid == networkId {
return true
}
}
return false
}
// delete a network
func (m *NetworkManager) deleteNetwork(networkID string) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
client := &http.Client{}
// Create a new DELETE request
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
// Add the required authorization header
req.Header.Set("X-Zt1-Auth", m.authToken)
// Send the request and get the response
resp, err := client.Do(req)
if err != nil {
return err
}
// Close the response body when we're done
defer resp.Body.Close()
s, err := io.ReadAll(resp.Body)
fmt.Println(string(s), err, resp.StatusCode)
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// Configure network
// Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
data := map[string]interface{}{
"ipAssignmentPools": []map[string]string{
{
"ipRangeStart": ipRangeStart,
"ipRangeEnd": ipRangeEnd,
},
},
"routes": []map[string]interface{}{
{
"target": routeTarget,
"via": nil,
},
},
"v4AssignMode": "zt",
"private": true,
}
payload, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
func (m *NetworkManager) setAssignedIps(networkID string, memid string, newIps []string) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/member/" + memid
data := map[string]interface{}{
"ipAssignments": newIps,
}
payload, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
func (m *NetworkManager) setNetworkNameAndDescription(netid string, name string, desc string) error {
// Convert string to rune slice
r := []rune(name)
// Loop over runes and remove non-ASCII characters
for i, v := range r {
if v > 127 {
r[i] = ' '
}
}
// Convert back to string and trim whitespace
name = strings.TrimSpace(string(r))
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/"
data := map[string]interface{}{
"name": name,
}
payload, err := json.Marshal(data)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Print the response status code
if resp.StatusCode != 200 {
return errors.New("network error. status code: " + strconv.Itoa(resp.StatusCode))
}
meta := m.GetNetworkMetaData(netid)
if meta != nil {
meta.Desc = desc
m.WriteNetworkMetaData(netid, meta)
}
return nil
}
func (m *NetworkManager) getNetworkNameAndDescription(netid string) (string, string, error) {
//Get name from network info
netinfo, err := m.getNetworkInfoById(netid)
if err != nil {
return "", "", err
}
name := netinfo.Name
//Get description from meta
desc := ""
networkMeta := m.GetNetworkMetaData(netid)
if networkMeta != nil {
desc = networkMeta.Desc
}
return name, desc, nil
}
/*
Member functions
*/
func (m *NetworkManager) getNetworkMembers(networkId string) ([]string, error) {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkId + "/member"
reqBody := bytes.NewBuffer([]byte{})
req, err := http.NewRequest("GET", url, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("failed to get network members")
}
memberList := map[string]int{}
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(payload, &memberList)
if err != nil {
return nil, err
}
members := make([]string, 0, len(memberList))
for k := range memberList {
members = append(members, k)
}
return members, nil
}
func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool {
//Get a list of member
memberids, err := m.getNetworkMembers(netid)
if err != nil {
return false
}
for _, thisMemberId := range memberids {
if thisMemberId == memid {
return true
}
}
return false
}
// Get a network memeber info by netid and memberid
func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) {
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-Zt1-Auth", m.authToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
thisMemeberInfo := &MemberInfo{}
payload, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(payload, &thisMemeberInfo)
if err != nil {
return nil, err
}
return thisMemeberInfo, nil
}
// Set the authorization state of a member
func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
payload := []byte(`{"authorized": true}`)
if !setAuthorized {
payload = []byte(`{"authorized": false}`)
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("X-ZT1-AUTH", m.authToken)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// Delete a member from the network
func (m *NetworkManager) deleteMember(netid string, memid string) error {
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// Make the host to join a given network
func (m *NetworkManager) joinNetwork(netid string) error {
req, err := http.NewRequest("POST", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// Make the host to leave a given network
func (m *NetworkManager) leaveNetwork(netid string) error {
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}

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,128 @@
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
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, "/") {
// Redirect to the index.html
http.Redirect(w, r, r.URL.Path+"index.html", http.StatusFound)
return
}
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)
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(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
rewrittenURL := r.RequestURI
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
r.URL, _ = url.Parse(rewrittenURL)
r.RequestURI = rewrittenURL
//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)
}()
})
}

View File

@ -0,0 +1,174 @@
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 CaptureRule struct {
CapturePath string `json:"capture_path"`
IncludeSubPaths bool `json:"include_sub_paths"`
}
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"`
}
/*
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
*/
/*
Global Capture Settings
Once plugin is enabled these rules always applies, no matter which HTTP Proxy rule it is enabled on
This captures the whole traffic of Zoraxy
*/
GlobalCapturePaths []CaptureRule `json:"global_capture_path"` //Global traffic capture path of your plugin
GlobalCaptureIngress string `json:"global_capture_ingress"` //Global traffic capture ingress path of your plugin (e.g. /g_handler)
/*
Always Capture Settings
Once the plugin is enabled on a given HTTP Proxy rule,
these always applies
*/
AlwaysCapturePaths []CaptureRule `json:"always_capture_path"` //Always capture path of your plugin when enabled on a HTTP Proxy rule (e.g. /myapp)
AlwaysCaptureIngress string `json:"always_capture_ingress"` //Always capture ingress path of your plugin when enabled on a HTTP Proxy rule (e.g. /a_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

@ -0,0 +1,747 @@
<!-- 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;">
<button onclick='$("#gannetDetailEdit").slideToggle("fast");' class="ui mini basic right floated circular icon button" style="display: inline-block; margin-top: 2.5em;"><i class="ui edit icon"></i></button>
<h1 class="ui header">
<span class="ganetID"></span>
<div class="sub header ganetName"></div>
</h1>
<div class="ui divider"></div>
<p><span class="ganetDesc"></span></p>
</div>
<div id="gannetDetailEdit" class="ui form" style="margin-top: 1em; display:none;">
<div class="ui divider"></div>
<p>You can change the network name and description below. <br>The name and description is only for easy management purpose and will not effect the network operation.</p>
<div class="field">
<label>Network Name</label>
<input type="text" id="gaNetNameInput" placeholder="">
</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>
</div>
<br><br>
</div>
<div class="ui divider"></div>
<h2>Settings</h2>
<div class="" style="overflow-x: auto;">
<table class="ui basic celled unstackable table" style="min-width: 560px;">
<thead>
<tr>
<th colspan="4">IPv4 Auto-Assign</th>
</tr>
</thead>
<tbody id="ganetRangeTable">
</tbody>
</table>
</div>
<br>
<div class="ui form">
<h3>Custom IP Range</h3>
<p>Manual IP Range Configuration. The IP range must be within the selected CIDR range.
<br>Use <code>Utilities > IP to CIDR tool</code> if you are not too familiar with CIDR notations.</p>
<div class="two fields">
<div class="field">
<label>IP Start</label>
<input type="text" class="ganIpStart" placeholder="">
</div>
<div class="field">
<label>IP End</label>
<input type="text" class="ganIpEnd" placeholder="">
</div>
</div>
</div>
<button onclick="setNetworkRange();" class="ui basic button"><i class="ui blue save icon"></i> Save Settings</button>
<div class="ui divider"></div>
<h2>Members</h2>
<p>To join this network using command line, type <code>sudo zerotier-cli join <span class="ganetID"></span></code> on your device terminal</p>
<div class="ui checkbox" style="margin-bottom: 1em;">
<input id="showUnauthorizedMembers" type="checkbox" onchange="changeUnauthorizedVisibility(this.checked);" checked>
<label>Show Unauthorized Members</label>
</div>
<div class="" style="overflow-x: auto;">
<table class="ui celled unstackable table">
<thead>
<tr>
<th>Auth</th>
<th>Address</th>
<th>Name</th>
<th>Managed IP</th>
<th>Authorized Since</th>
<th>Version</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="networkMemeberTable">
<tr>
</tr>
</tbody>
</table>
</div>
<div class="ui divider"></div>
<h4>Add Controller as Member</h4>
<p>Optionally you can add the network controller (ZeroTier running on the Zoraxy node) as member for cross GAN reverse proxy to bypass NAT limitations.</p>
<button class="ui basic small button addControllerToNetworkBtn" onclick="ganAddControllerToNetwork(this);"><i class="green add icon"></i> Add Controller as Member</button>
<button class="ui basic small button removeControllerFromNetworkBtn" onclick="ganRemoveControllerFromNetwork(this);"><i class="red sign-out icon"></i> Remove Controller from Member</button>
<br><br>
</div>
<script>
$(".checkbox").checkbox();
var currentGANetID = "";
var currentGANNetMemeberListener = undefined;
var currentGaNetDetails = {};
var currentGANMemberList = [];
var netRanges = {
"10.147.17.*": "10.147.17.0/24",
"10.147.18.*": "10.147.18.0/24",
"10.147.19.*": "10.147.19.0/24",
"10.147.20.*": "10.147.20.0/24",
"10.144.*.*": "10.144.0.0/16",
"10.241.*.*": "10.241.0.0/16",
"10.242.*.*": "10.242.0.0/16",
"10.243.*.*": "10.243.0.0/16",
"10.244.*.*": "10.244.0.0/16",
"172.22.*.*": "172.22.0.0/15",
"172.23.*.*": "172.23.0.0/16",
"172.24.*.*": "172.24.0.0/14",
"172.25.*.*": "172.25.0.0/16",
"172.26.*.*": "172.26.0.0/15",
"172.27.*.*": "172.27.0.0/16",
"172.28.*.*": "172.28.0.0/15",
"172.29.*.*": "172.29.0.0/16",
"172.30.*.*": "172.30.0.0/15",
"192.168.191.*": "192.168.191.0/24",
"192.168.192.*": "192.168.192.0/24",
"192.168.193.*": "192.168.193.0/24",
"192.168.194.*": "192.168.194.0/24",
"192.168.195.*": "192.168.195.0/24",
"192.168.196.*": "192.168.196.0/24"
}
function generateIPRangeTable(netRanges) {
$("#ganetRangeTable").empty();
const tableBody = document.getElementById('ganetRangeTable');
const cidrs = Object.values(netRanges);
// Set the number of rows and columns to display in the table
const numRows = 6;
const numCols = 4;
let row = document.createElement('tr');
let col = 0;
for (let i = 0; i < cidrs.length; i++) {
if (col >= numCols) {
tableBody.appendChild(row);
row = document.createElement('tr');
col = 0;
}
const td = document.createElement('td');
td.setAttribute('class', `clickable iprange`);
td.setAttribute('CIDR', cidrs[i]);
td.innerHTML = cidrs[i];
let thisCidr = cidrs[i];
td.onclick = function(){
selectNetworkRange(thisCidr, td);
};
row.appendChild(td);
col++;
}
// Add any remaining cells to the table
if (col > 0) {
for (let i = col; i < numCols; i++) {
row.appendChild(document.createElement('td'));
}
tableBody.appendChild(row);
}
}
function highlightCurrentGANetCIDR(){
var currentCIDR = currentGaNetDetails.routes[0].target;
$(".iprange").each(function(){
if ($(this).attr("CIDR") == currentCIDR){
$(this).addClass("active");
populateStartEndIpByCidr(currentCIDR);
}
})
}
function populateStartEndIpByCidr(cidr){
function cidrToRange(cidr) {
var range = [2];
cidr = cidr.split('/');
var start = ip2long(cidr[0]);
range[0] = long2ip(start);
range[1] = long2ip(Math.pow(2, 32 - cidr[1]) + start - 1);
return range;
}
var cidrRange = cidrToRange(cidr);
$(".ganIpStart").val(cidrRange[0]);
$(".ganIpEnd").val(cidrRange[1]);
}
function selectNetworkRange(cidr, object){
populateStartEndIpByCidr(cidr);
$(".iprange.active").removeClass("active");
$(object).addClass("active");
}
function setNetworkRange(){
var ipstart = $(".ganIpStart").val().trim();
var ipend = $(".ganIpEnd").val().trim();
if (ipstart == ""){
$(".ganIpStart").parent().addClass("error");
}else{
$(".ganIpStart").parent().removeClass("error");
}
if (ipend == ""){
$(".ganIpEnd").parent().addClass("error");
}else{
$(".ganIpEnd").parent().removeClass("error");
}
//Get CIDR from selected range group
var cidr = $(".iprange.active").attr("cidr");
$.cjax({
url: "./api/gan/network/setRange",
metohd: "POST",
data:{
netid: currentGANetID,
cidr: cidr,
ipstart: ipstart,
ipend: ipend
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000)
}else{
msgbox("Network Range Updated")
}
}
})
}
function saveNameAndDesc(object=undefined){
var name = $("#gaNetNameInput").val();
var desc = $("#gaNetDescInput").val();
if (object != undefined){
$(object).addClass("loading");
}
$.cjax({
url: "./api/gan/network/name",
method: "POST",
data: {
netid: currentGANetID,
name: name,
desc: desc,
},
success: function(data){
initNetNameAndDesc();
if (object != undefined){
$(object).removeClass("loading");
msgbox("Network Metadata Updated");
}
$("#gannetDetailEdit").slideUp("fast");
}
});
}
function initNetNameAndDesc(){
//Get the details of the net
$.get("./api/gan/network/name?netid=" + currentGANetID, function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
}else{
$("#gaNetNameInput").val(data[0]);
$(".ganetName").html(data[0]);
$("#gaNetDescInput").val(data[1]);
$(".ganetDesc").text(data[1]);
}
});
}
function initNetDetails(){
//Get the details of the net
$.get("./api/gan/network/list?netid=" + currentGANetID, function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
}else{
currentGaNetDetails = data;
highlightCurrentGANetCIDR();
}
});
}
//Handle delete IP from memeber
function deleteIpFromMemeber(memberid, ip){
$.cjax({
url: "./api/gan/members/ip",
metohd: "POST",
data: {
netid: currentGANetID,
memid: memberid,
opr: "del",
ip: ip,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("IP removed from member " + memberid)
}
renderMemeberTable();
}
});
}
function addIpToMemeberFromInput(memberid, newip){
function isValidIPv4Address(address) {
// Split the address into its 4 components
const parts = address.split('.');
// Check that there are 4 components
if (parts.length !== 4) {
return false;
}
// Check that each component is a number between 0 and 255
for (let i = 0; i < 4; i++) {
const part = parseInt(parts[i], 10);
if (isNaN(part) || part < 0 || part > 255) {
return false;
}
}
// The address is valid
return true;
}
if (!isValidIPv4Address(newip)){
msgbox(newip + " is not a valid IPv4 address", false, 5000)
return
}
$.cjax({
url: "./api/gan/members/ip",
metohd: "POST",
data: {
netid: currentGANetID,
memid: memberid,
opr: "add",
ip: newip,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("IP added to member " + memberid)
}
renderMemeberTable();
}
})
}
//Member table populate
function renderMemeberTable(forceUpdate = false) {
$.ajax({
url: './api/gan/members/list?netid=' + currentGANetID + '&detail=true',
type: 'GET',
success: function(data) {
let tableBody = $('#networkMemeberTable');
if (tableBody.length == 0){
return;
}
data.sort((a, b) => a.address.localeCompare(b.address));
//Check if the new object equal to the old one
if (objectEqual(currentGANMemberList, data) && !forceUpdate){
//Do not need to update it
return;
}
tableBody.empty();
currentGANMemberList = data;
var authroziedCount = 0;
data.forEach((member) => {
let lastAuthTime = new Date(member.lastAuthorizedTime).toLocaleString();
if (member.lastAuthorizedTime == 0){
lastAuthTime = "Never";
}
let version = `${member.vMajor}.${member.vMinor}.${member.vProto}.${member.vRev}`;
if (member.vMajor == -1){
version = "Unknown";
}
let authorizedCheckbox = `<div class="ui fitted checkbox">
<input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);">
<label></label>
</div>`;
if (member.authorized){
authorizedCheckbox = `<div class="ui fitted checkbox">
<input type="checkbox" addr="${member.address}" name="isAuthrozied" onchange="handleMemberAuth(this);" checked="">
<label></label>
</div>`
}
let rowClass = "authorized";
let unauthorizedStyle = "";
if (!$("#showUnauthorizedMembers")[0].checked && !member.authorized){
unauthorizedStyle = "display:none;";
}
if (!member.authorized){
rowClass = "unauthorized";
}else{
authroziedCount++;
}
let assignedIp = "";
if (member.ipAssignments.length == 0){
assignedIp = "Not assigned"
}else{
assignedIp = `<div class="ui list">`
member.ipAssignments.forEach(function(thisIp){
assignedIp += `<div class="item" style="width: 100%;">${thisIp} <a style="cursor:pointer; float: right;" title="Remove IP" onclick="deleteIpFromMemeber('${member.address}','${thisIp}');"><i class="red remove icon"></i></a></div>`;
})
assignedIp += `</div>`
}
const row = $(`<tr class="GANetMemberEntity ${rowClass}" style="${unauthorizedStyle}">`);
row.append($(`<td class="GANetMember ${rowClass}" style="text-align: center;">`).html(authorizedCheckbox));
row.append($('<td>').text(member.address));
row.append($('<td>').html(`<span class="memberName" addr="${member.address}"></span> <a style="cursor:pointer; float: right;" title="Edit Memeber Name" onclick="renameMember('${member.address}');"><i class="grey edit icon"></i></a>`));
row.append($('<td>').html(`${assignedIp}
<div class="ui action mini fluid input" style="min-width: 200px;">
<input type="text" placeholder="IPv4" onchange="$(this).val($(this).val().trim());">
<button onclick="addIpToMemeberFromInput('${member.address}',$(this).parent().find('input').val());" class="ui basic icon button">
<i class="add icon"></i>
</button>
</div>`));
row.append($('<td>').text(lastAuthTime));
row.append($('<td>').text(version));
row.append($(`<td title="Deauthorize & Delete Memeber" style="text-align: center;" onclick="handleMemberDelete('${member.address}');">`).html(`<button class="ui basic mini icon button"><i class="red remove icon"></i></button>`));
tableBody.append(row);
});
if (data.length == 0){
tableBody.append(`<tr>
<td colspan="7"><i class="green check circle icon"></i> No member has joined this network yet.</td>
</tr>`);
}
if (data.length > 0 && authroziedCount == 0 && !$("#showUnauthorizedMembers")[0].checked){
//All nodes are unauthorized. Show tips to enable unauthorize display
tableBody.append(`<tr>
<td colspan="7"><i class="yellow exclamation circle icon"></i> Unauthorized nodes detected. Enable "Show Unauthorized Member" to change member access permission.</td>
</tr>`);
}
initNameForMembers();
},
error: function(xhr, status, error) {
console.log('Error:', error);
}
});
}
function initNameForMembers(){
$(".memberName").each(function(){
let addr = $(this).attr("addr");
let targetDOM = $(this);
$.cjax({
url: "./api/gan/members/name",
method: "POST",
data: {
netid: currentGANetID,
memid: addr,
},
success: function(data){
if (data.error != undefined){
$(targetDOM).text("N/A");
}else{
$(targetDOM).text(data.Name);
}
}
});
})
}
function renameMember(targetMemberAddr){
if (targetMemberAddr == ""){
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",
method: "POST",
data: {
netid: currentGANetID,
memid: targetMemberAddr,
name: newname
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Member Name Updated");
}
renderMemeberTable(true);
}
})
}
}
//Helper function to check if two objects are equal recursively
function objectEqual(obj1, obj2) {
// compare types
if (typeof obj1 !== typeof obj2) {
return false;
}
// compare values
if (typeof obj1 !== 'object' || obj1 === null) {
return obj1 === obj2;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
// compare keys
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (!keys2.includes(key)) {
return false;
}
// recursively compare values
if (!objectEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
}
function changeUnauthorizedVisibility(visable){
if(visable){
$(".GANetMemberEntity.unauthorized").show();
}else{
$(".GANetMemberEntity.unauthorized").hide();
}
}
function handleMemberAuth(object){
let targetMemberAddr = $(object).attr("addr");
let isAuthed = object.checked;
$.cjax({
url: "./api/gan/members/authorize",
method: "POST",
data: {
netid:currentGANetID,
memid: targetMemberAddr,
auth: isAuthed
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
if (isAuthed){
msgbox("Member Authorized");
}else{
msgbox("Member Deauthorized");
}
}
renderMemeberTable(true);
}
})
}
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");
}
renderMemeberTable(true);
}
});
}
}
//Add and remove this controller node to network as member
function ganAddControllerToNetwork(){
$(".addControllerToNetworkBtn").addClass("disabled");
$(".addControllerToNetworkBtn").addClass("loading");
$.cjax({
url: "./api/gan/network/join",
method: "POST",
data: {
netid:currentGANetID,
},
success: function(data){
$(".addControllerToNetworkBtn").removeClass("disabled");
$(".addControllerToNetworkBtn").removeClass("loading");
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Controller joint " + currentGANetID);
}
setTimeout(function(){
renderMemeberTable(true);
}, 3000)
}
});
}
function ganRemoveControllerFromNetwork(){
$(".removeControllerFromNetworkBtn").addClass("disabled");
$(".removeControllerFromNetworkBtn").addClass("loading");
$.cjax({
url: "./api/gan/network/leave",
method: "POST",
data: {
netid:currentGANetID,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Controller left " + currentGANetID);
}
renderMemeberTable(true);
$(".removeControllerFromNetworkBtn").removeClass("disabled");
$(".removeControllerFromNetworkBtn").removeClass("loading");
}
});
}
//Entry points
function initGanetDetails(ganetId){
currentGANetID = ganetId;
$(".ganetID").text(ganetId);
initNetNameAndDesc(ganetId);
generateIPRangeTable(netRanges);msgbox
initNetDetails();
renderMemeberTable(true);
//Setup a listener to listen for member list change
if (currentGANNetMemeberListener == undefined){
currentGANNetMemeberListener = setInterval(function(){
if ($('#networkMemeberTable').length > 0 && currentGANetID){
renderMemeberTable();
}
}, 3000);
}
}
//Exit point
function exitToGanList(){
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>

View File

@ -0,0 +1,262 @@
<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 == ""){
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){
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){
msgbox(response.error, false, 5000);
}else{
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);
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){
if (confirm("Confirm remove Network " + netid + " PERMANENTLY ?"))
$.cjax({
url: "./api/gan/network/remove",
type: "POST",
dataType: "json",
data: {
id: netid,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
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

@ -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,39 @@ func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
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

@ -41,6 +41,20 @@ func initACME() *acme.ACMEHandler {
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
func acmeRegisterSpecialRoutingRule() {
SystemWideLogger.Println("Assigned temporary port:" + acmeHandler.Getport())
@ -82,6 +96,12 @@ 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

View File

@ -8,6 +8,7 @@ import (
"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"
@ -18,34 +19,11 @@ 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)
@ -56,24 +34,24 @@ func initAPIs(targetMux *http.ServeMux) {
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)
@ -81,12 +59,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)
@ -95,42 +76,38 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
}
//SSO and Oauth
authRouter.HandleFunc("/api/sso/status", ssoHandler.HandleSSOStatus)
authRouter.HandleFunc("/api/sso/enable", ssoHandler.HandleSSOEnable)
authRouter.HandleFunc("/api/sso/setPort", ssoHandler.HandlePortChange)
authRouter.HandleFunc("/api/sso/setAuthURL", ssoHandler.HandleSetAuthURL)
// 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/app/register", ssoHandler.HandleRegisterApp)
//authRouter.HandleFunc("/api/sso/app/list", ssoHandler.HandleListApp)
//authRouter.HandleFunc("/api/sso/app/remove", ssoHandler.HandleRemoveApp)
authRouter.HandleFunc("/api/sso/user/list", ssoHandler.HandleListUser)
authRouter.HandleFunc("/api/sso/user/add", ssoHandler.HandleAddUser)
authRouter.HandleFunc("/api/sso/user/edit", ssoHandler.HandleEditUser)
authRouter.HandleFunc("/api/sso/user/remove", ssoHandler.HandleRemoveUser)
//Redirection config
// 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)
@ -138,20 +115,37 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
//Path Blocker APIs
/* Quick Ban List */
authRouter.HandleFunc("/api/quickban/list", handleListQuickBan)
}
// 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
// Register the APIs for Global Area Network management functions, Will be moving to plugin soon
func RegisterGANAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
@ -166,8 +160,10 @@ func initAPIs(targetMux *http.ServeMux) {
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)
@ -175,19 +171,57 @@ 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
// 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)
@ -202,66 +236,17 @@ 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)
}
// 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) {
@ -277,21 +262,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")
@ -332,5 +313,61 @@ 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
fs := http.FileServer(http.FS(webres))
if DEVELOPMENT_BUILD {
fs = http.FileServer(http.Dir("web/"))
}
//Add a layer of middleware for advance control
advHandler := FSHandler(fs)
targetMux.Handle("/", advHandler)
//Register the APIs
RegisterAuthAPIs(requireAuth, targetMux)
RegisterHTTPProxyAPIs(authRouter)
RegisterTLSAPIs(authRouter)
RegisterAuthenticationHandlerAPIs(authRouter)
RegisterRedirectionAPIs(authRouter)
RegisterAccessRuleAPIs(authRouter)
RegisterPathRuleAPIs(authRouter)
RegisterStatisticalAPIs(authRouter)
RegisterGANAPIs(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())))
}

154
src/def.go Normal file
View File

@ -0,0 +1,154 @@
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/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/ganserv"
"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.1.9"
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
/* 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"
/* 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"
)
/* 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)")
ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
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")
/* Maintaince Function Flags */
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
)
/* 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
ganManager *ganserv.NetworkManager //Global Area Network Manager
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
//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

@ -7,7 +7,7 @@ toolchain go1.22.2
require (
github.com/boltdb/bolt v1.3.1
github.com/docker/docker v27.0.0+incompatible
github.com/go-acme/lego/v4 v4.19.2
github.com/go-acme/lego/v4 v4.21.0
github.com/go-ping/ping v1.1.0
github.com/go-session/session v3.1.2+incompatible
github.com/google/uuid v1.6.0
@ -16,20 +16,27 @@ 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.29.0
golang.org/x/sys v0.25.0
golang.org/x/text v0.18.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/sys v0.28.0
golang.org/x/text v0.21.0
)
require (
cloud.google.com/go/auth v0.9.3 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // 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 v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // 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/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
github.com/tidwall/buntdb v1.1.2 // indirect
@ -41,18 +48,19 @@ require (
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // 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
)
require (
cloud.google.com/go/compute/metadata v0.5.1 // indirect
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.14.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.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.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
@ -65,28 +73,28 @@ require (
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.63.15 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // 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.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
github.com/aws/smithy-go v1.20.4 // 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.104.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.2-0.20180830191138-d8f796af33cc // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
@ -96,25 +104,24 @@ require (
github.com/docker/go-units v0.5.0 // 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.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-oauth2/oauth2/v4 v4.5.2
github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
github.com/goccy/go-json v0.10.3 // 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/gofrs/uuid v4.4.0+incompatible
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-jwt/jwt/v4 v4.5.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gophercloud/gophercloud v1.14.0 // 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
@ -131,7 +138,7 @@ 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.40.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
@ -147,9 +154,9 @@ require (
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-20240207213615-dde5bf4577a3 // indirect
github.com/nrdcg/desec v0.8.0 // 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
@ -165,24 +172,23 @@ require (
github.com/pquerna/otp v1.4.0 // indirect
github.com/sacloud/api-client-go v0.2.10 // indirect
github.com/sacloud/go-http v0.1.8 // indirect
github.com/sacloud/iaas-api-go v1.12.0 // 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.5 // indirect
github.com/softlayer/softlayer-go v1.1.7 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // 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.7.0-20240913052650-970ca9a // indirect
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/xlzd/gotp v0.1.0
github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 // indirect
go.opencensus.io v0.24.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
@ -190,20 +196,20 @@ require (
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.27.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.25.0 // indirect
google.golang.org/api v0.197.0 // indirect
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.66.1 // indirect
google.golang.org/protobuf v1.34.2 // 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.12.0 // 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

View File

@ -5,13 +5,13 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U=
cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs=
cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@ -21,22 +21,24 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 h1:9Eih8XcEeQnFD0ntMlUDleKMzfeCeUfa+VbnDCI4AZs=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0/go.mod h1:wGPyTi+aURdqPAGMZDQqnNs9IrShADF8w2WZb6bKeq0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
@ -62,6 +64,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -77,44 +81,43 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 h1:r2uwBUQhLhcPzaWz9tRJqc8MjYwHb+oF2+Q6467BF14=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72 h1:HvFZUzEbNvfe8F2Mg0wBGv90bPhWDxgVtDHR5zoBOU0=
github.com/aliyun/alibaba-cloud-sdk-go v1.63.72/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU=
github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks=
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I=
github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU=
github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw=
github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/config v1.28.7 h1:GduUnoTXlhkgnxTD93g1nv4tVPILbdNQOzav+Wpg7AE=
github.com/aws/aws-sdk-go-v2/config v1.28.7/go.mod h1:vZGX6GVkIE8uECSUHB6MWAUsd4ZcG2Yq/dMa4refR3M=
github.com/aws/aws-sdk-go-v2/credentials v1.17.48 h1:IYdLD1qTJ0zanRavulofmqut4afs45mOWEI+MzZtTfQ=
github.com/aws/aws-sdk-go-v2/credentials v1.17.48/go.mod h1:tOscxHN3CGmuX9idQ3+qbkzrjVIx32lqDSU1/0d/qXs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 h1:kqOrpojG71DxJm/KDPO+Z/y1phm1JlC8/iT+5XRmAn8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22/go.mod h1:NtSFajXVVL8TA2QNngagVZmUtXciyrHOt7xgz4faS/M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 h1:ea6TO3HgVeVTB2Ie1djyBFWBOc9CohpKbo/QZbGTCJQ=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6/go.mod h1:D2TUTD3v6AWmE5LzdCXLWNFtoYbSf6IEjKh1ggbuVdw=
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 h1:957e1/SwXIfPi/0OUJkH9YnPZRe9G6Kisd/xUhF7AUE=
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2/go.mod h1:343vcjcyOTuHTBBgUrOxPM36/jE96qLZnGL447ldrB0=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc=
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE=
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o=
github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4=
github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8 h1:+lmJoqxuUoPlSfGk5JYQQivd9YFjUvRZR6RPY+Wcx48=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.8/go.mod h1:Gg8/myP4+rgRi4+j9gQdbOEnMtwMAUUIeXo+nKCFVj8=
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4 h1:0jMtawybbfpFEIMy4wvfyW2Z4YLr7mnuzT0fhR67Nrc=
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.4/go.mod h1:xlMODgumb0Pp8bzfpojqelDrf8SL9rb5ovwmwKJl+oU=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
@ -129,26 +132,24 @@ github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx2
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM=
github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.104.0 h1:R/lB0dZupaZbOgibAH/BRrkFbZ6Acn/WsKg2iX2xXuY=
github.com/cloudflare/cloudflare-go v0.104.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
github.com/cloudflare/cloudflare-go v0.112.0 h1:caFwqXdGJCl3rjVMgbPEn8iCYAg9JsRYV3dIVQE5d7g=
github.com/cloudflare/cloudflare-go v0.112.0/go.mod h1:QB55kuJ5ZTeLNFcLJePfMuBilhu/LDKpLBmKFQIoSZ0=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@ -163,6 +164,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
@ -176,12 +179,10 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@ -197,14 +198,14 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-acme/lego/v4 v4.19.2 h1:Y8hrmMvWETdqzzkRly7m98xtPJJivWFsgWi8fcvZo+Y=
github.com/go-acme/lego/v4 v4.19.2/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ=
github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o=
github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI=
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@ -221,22 +222,24 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQKqBQ=
github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w=
github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -248,18 +251,15 @@ github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -272,11 +272,11 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -284,9 +284,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
@ -308,7 +306,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -317,10 +314,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/gophercloud/gophercloud v1.14.0 h1:Bt9zQDhPrbd4qX7EILGmy+i7GP35cc+AAL2+wIJpUE8=
github.com/gophercloud/gophercloud v1.14.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o=
github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=
github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
@ -339,9 +336,8 @@ github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0 h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
@ -374,8 +370,8 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 h1:X3E16S6AUZsQKhJIQ5kNnylnp0GtSy2YhIbxfvDavtU=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 h1:kQ2Agpfy7Ze1ajn9xCQG9G6T7XIbqv+FBDS/U98W9Mk=
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E=
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
@ -404,6 +400,8 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@ -432,8 +430,8 @@ github.com/likexian/gokit v0.25.13 h1:p2Uw3+6fGG53CwdU2Dz0T6bOycdb2+bAFAa3ymwWVk
github.com/likexian/gokit v0.25.13/go.mod h1:qQhEWFBEfqLCO3/vOEo2EDKd+EycekVtUK4tex+l2H4=
github.com/likexian/whois v1.15.1 h1:6vTMI8n9s1eJdmcO4R9h1x99aQWIZZX1CD3am68gApU=
github.com/likexian/whois v1.15.1/go.mod h1:/nxmQ6YXvLz+qTxC/QFtEJNAt0zLuRxJrKiWpBJX8X0=
github.com/linode/linodego v1.40.0 h1:7ESY0PwK94hoggoCtIroT1Xk6b1flrFBNZ6KwqbTqlI=
github.com/linode/linodego v1.40.0/go.mod h1:NsUw4l8QrLdIofRg1NYFBbW5ZERnmbZykVBszPZLORM=
github.com/linode/linodego v1.44.0 h1:JZLLWzCAx3CmHSV9NmCoXisuqKtrmPhfY9MrgvaHMUY=
github.com/linode/linodego v1.44.0/go.mod h1:umdoNOmtbqAdGQbmQnPFZ2YS4US+/mU/1bA7MjoKAvg=
github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
github.com/liquidweb/liquidweb-cli v0.6.9 h1:acbIvdRauiwbxIsOCEMXGwF75aSJDbDiyAWPjVnwoYM=
github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
@ -472,8 +470,9 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -505,12 +504,12 @@ github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo
github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk=
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 h1:ouZ2JWDl8IW5k1qugYbmpbmW8hn85Ig6buSMBRlz3KI=
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3/go.mod h1:ZwadWt7mVhMHMbAQ1w8IhDqtWO3eWqWq72W7trnaiE8=
github.com/nrdcg/desec v0.8.0 h1:FJbRWUAluTCUi9nHFnhqPhLSIHiNnB9elZVWYgFtIqA=
github.com/nrdcg/desec v0.8.0/go.mod h1:BsnYPtSlBttJL3Gyzv0kDH7zkk60obwThlnqiiKzn+o=
github.com/nrdcg/desec v0.10.0 h1:qrEDiqnsvNU9QE7lXIXi/tIHAfyaFXKxF2/8/52O8uM=
github.com/nrdcg/desec v0.10.0/go.mod h1:5+4vyhMRTs49V9CNoODF/HwT8Mwxv9DJ6j+7NekUnBs=
github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
github.com/nrdcg/freemyip v0.2.0 h1:/GscavT4GVqAY13HExl5UyoB4wlchv6Cg5NYDGsUoJ8=
github.com/nrdcg/freemyip v0.2.0/go.mod h1:HjF0Yz0lSb37HD2ihIyGz9esyGcxbCrrGFLPpKevbx4=
github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc=
github.com/nrdcg/freemyip v0.3.0/go.mod h1:c1PscDvA0ukBF0dwelU/IwOakNKnVxetpAQ863RMJoM=
github.com/nrdcg/goinwx v0.10.0 h1:6W630bjDxQD6OuXKqrFRYVpTt0G/9GXXm3CeOrN0zJM=
github.com/nrdcg/goinwx v0.10.0/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4=
github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk=
@ -528,6 +527,7 @@ github.com/nzdjb/go-metaname v1.0.0 h1:sNASlZC1RM3nSudtBTE1a3ZVTDyTpjqI5WXRPrdZ9
github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
@ -536,6 +536,7 @@ github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
@ -554,6 +555,8 @@ github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c=
github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -566,6 +569,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@ -584,8 +589,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
@ -595,8 +601,8 @@ github.com/sacloud/api-client-go v0.2.10 h1:+rv3jDohD+pkdYwOTBiB+jZsM0xK3AxadXRz
github.com/sacloud/api-client-go v0.2.10/go.mod h1:Jj3CTy2+O4bcMedVDXlbHuqqche85HEPuVXoQFhLaRc=
github.com/sacloud/go-http v0.1.8 h1:ynreWA/vnM8G2ksbMlmefBHsXURKPz49qlPRqQ9IQdw=
github.com/sacloud/go-http v0.1.8/go.mod h1:7TL7TN1fnPKHsMifIqURDkGujnKViCgEz5Ei/LQdFK8=
github.com/sacloud/iaas-api-go v1.12.0 h1:kqXFn3HzCiawlX6hVJb1GVqcSJqcmiGHB4Zp14sxiI8=
github.com/sacloud/iaas-api-go v1.12.0/go.mod h1:SZLXeWOdXk3WReIS557sbU1gkOgrE4rseIBQV1B3b7o=
github.com/sacloud/iaas-api-go v1.14.0 h1:xjkFWqdo4ilTrKPNNYBNWR/CZ/kVRsJrdAHAad6J/AQ=
github.com/sacloud/iaas-api-go v1.14.0/go.mod h1:C8os2Mnj0TOmMdSllwhaDWKMVG2ysFnpe69kyA4M3V0=
github.com/sacloud/packages-go v0.0.10 h1:UiQGjy8LretewkRhsuna1TBM9Vz/l9FoYpQx+D+AOck=
github.com/sacloud/packages-go v0.0.10/go.mod h1:f8QITBh9z4IZc4yE9j21Q8b0sXEMwRlRmhhjWeDVTYs=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 h1:yoKAVkEVwAqbGbR8n87rHQ1dulL25rKloGadb3vm770=
@ -605,6 +611,8 @@ github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZ
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@ -622,8 +630,8 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA=
github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
github.com/softlayer/softlayer-go v1.1.5 h1:UFFtgKxiw0yIuUw93XBCFIiIMYR5eLgmm4a5DqMHXGg=
github.com/softlayer/softlayer-go v1.1.5/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw=
github.com/softlayer/softlayer-go v1.1.7 h1:SgTL+pQZt1h+5QkAhVmHORM/7N9c1X0sljJhuOIHxWE=
github.com/softlayer/softlayer-go v1.1.7/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw=
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ=
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -655,15 +663,16 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 h1:RE84sHFFx6t24DJvSnF9fS1DzBNv9OpctzHK3t7AY+I=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065 h1:krcqtAmexnHHBm/4ge4tr2b1cn/a7JGBESVGoZYXQAE=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 h1:aEFtLD1ceyeljQXB1S2BjN0zjTkf0X3XmpuxFIiC29w=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065/go.mod h1:HWvwy09hFSMXrj9SMvVRWV4U7rZO3l+WuogyNuxiT3M=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo=
@ -692,8 +701,8 @@ github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaO
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a h1:R6IR+Vj/RnGZLnX8PpPQsbbQthctO7Ah2q4tj5eoe2o=
github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI=
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
@ -717,10 +726,10 @@ github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 h1:WgeEP+8WizCQyccJNHOMLONq23qVAzYHtyg5qTdUWmg=
github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 h1:Q4LvUMF4kzaGtopoIdXReL9/qGtmzOewBhF3dQvuHMU=
github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5/go.mod h1:9dt2V80cfJGRZA+5SKP3Ky+R/DxH02XfKObi2Uy2uPc=
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c h1:Rnr+lDYXVkP+3eT8/d68iq4G/UeIhyCQk+HKa8toTvg=
github.com/yandex-cloud/go-genproto v0.0.0-20241220122821-aeb3b05efd1c/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134 h1:qmpz0Kvr9GAng8LAhRcKIpY71CEAcL3EBkftVlsP5Cw=
github.com/yandex-cloud/go-sdk v0.0.0-20241220131134-2393e243c134/go.mod h1:KgZCJrxdhdw/sKhTQ/M3S9WOLri2PCnBlc4C3s+PfKY=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
@ -731,13 +740,13 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
@ -752,7 +761,6 @@ go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBq
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@ -781,8 +789,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -811,8 +819,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -833,10 +841,8 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
@ -848,15 +854,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -867,8 +872,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -887,6 +892,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -901,6 +907,7 @@ golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -921,8 +928,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -930,14 +937,13 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
@ -945,14 +951,13 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -981,8 +986,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -996,8 +1001,8 @@ google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ=
google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw=
google.golang.org/api v0.214.0 h1:h2Gkq07OYi6kusGOaT/9rnNljuXmqPnaig7WGPmKbwA=
google.golang.org/api v0.214.0/go.mod h1:bYPpLG8AyeMWwDU6NXoB00xC0DFkikVvd5MfwoxjLqE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -1012,43 +1017,31 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -1062,15 +1055,14 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ns1/ns1-go.v2 v2.12.0 h1:cqdqQoTx17JmTusfxh5m3e2b36jfUzFAZedv89pFX18=
gopkg.in/ns1/ns1-go.v2 v2.12.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
gopkg.in/ns1/ns1-go.v2 v2.13.0 h1:I5NNqI9Bi1SGK92TVkOvLTwux5LNrix/99H2datVh48=
gopkg.in/ns1/ns1-go.v2 v2.13.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
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.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,7 +1,36 @@
package main
/*
______
|___ /
/ / ___ _ __ __ ___ ___ _
/ / / _ \| '__/ _` \ \/ / | | |
/ /_| (_) | | | (_| |> <| |_| |
/_____\___/|_| \__,_/_/\_\\__, |
__/ |
|___/
Zoraxy - A general purpose HTTP reverse proxy and forwarding tool
Author: tobychui
License: AGPLv3
--------------------------------------------
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License or any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"embed"
"flag"
"fmt"
"log"
@ -13,103 +42,15 @@ import (
"github.com/google/uuid"
"github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso"
"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/ganserv"
"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/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/update"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv"
)
// General flags
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
var showver = flag.Bool("version", false, "Show version of this server")
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
var mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
var acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
var (
name = "Zoraxy"
version = "3.1.2"
nodeUUID = "generic" //System uuid, in uuidv4 format
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
/*
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
ganManager *ganserv.NetworkManager //Global Area Network Manager
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
ssoHandler *sso.SSOHandler //Single Sign On handler
//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
)
// Kill signal handler. Do something before the system the core terminate.
/* SIGTERM handler, do shutdown sequences before closing */
func SetupCloseHandler() {
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-c
ShutdownSeq()
@ -117,45 +58,21 @@ func SetupCloseHandler() {
}()
}
func ShutdownSeq() {
SystemWideLogger.Println("Shutting down " + name)
//SystemWideLogger.Println("Closing GeoDB")
//geodbStore.Close()
SystemWideLogger.Println("Closing Netstats Listener")
netstatBuffers.Close()
SystemWideLogger.Println("Closing Statistic Collector")
statisticCollector.Close()
if mdnsTickerStop != nil {
SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
// Stop the mdns service
mdnsTickerStop <- true
}
mdnsScanner.Close()
SystemWideLogger.Println("Shutting down load balancer")
loadBalancer.Close()
SystemWideLogger.Println("Closing Certificates Auto Renewer")
acmeAutoRenewer.Close()
//Remove the tmp folder
SystemWideLogger.Println("Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database
SystemWideLogger.Println("Stopping system database")
sysdb.Close()
//Close logger
SystemWideLogger.Println("Closing system wide logger")
SystemWideLogger.Close()
}
func main() {
//Parse startup flags
flag.Parse()
/* Maintaince Function Modes */
if *showver {
fmt.Println(name + " - Version " + version)
fmt.Println(SYSTEM_NAME + " - Version " + SYSTEM_VERSION)
os.Exit(0)
}
if *geoDbUpdate {
geodb.DownloadGeoDBUpdate("./conf/geodb")
os.Exit(0)
}
/* Main Zoraxy Routines */
if !utils.ValidateListeningAddress(*webUIPort) {
fmt.Println("Malformed -port (listening address) paramter. Do you mean -port=:" + *webUIPort + "?")
os.Exit(0)
@ -163,13 +80,13 @@ func main() {
if *enableAutoUpdate {
fmt.Println("Checking required config update")
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(version))
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(SYSTEM_VERSION))
}
SetupCloseHandler()
//Read or create the system uuid
uuidRecord := "./sys.uuid"
uuidRecord := *path_uuid
if !utils.FileExists(uuidRecord) {
newSystemUUID := uuid.New().String()
os.WriteFile(uuidRecord, []byte(newSystemUUID), 0775)
@ -185,13 +102,13 @@ func main() {
webminPanelMux = http.NewServeMux()
csrfMiddleware = csrf.Protect(
[]byte(nodeUUID),
csrf.CookieName("zoraxy-csrf"),
csrf.CookieName(CSRF_COOKIENAME),
csrf.Secure(false),
csrf.Path("/"),
csrf.SameSite(csrf.SameSiteLaxMode),
)
//Startup all modules
//Startup all modules, see start.go
startupSequence()
//Initiate management interface APIs
@ -208,11 +125,10 @@ func main() {
//Start the finalize sequences
finalSequence()
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://localhost" + *webUIPort)
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
if err != nil {
log.Fatal(err)
}
}

View File

@ -21,6 +21,7 @@ import (
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
@ -29,12 +30,20 @@ import (
"imuslab.com/zoraxy/mod/utils"
)
var defaultNameservers = []string{
"8.8.8.8:53", // Google DNS
"8.8.4.4:53", // Google DNS
"1.1.1.1:53", // Cloudflare DNS
"1.0.0.1:53", // Cloudflare DNS
}
type CertificateInfoJSON struct {
AcmeName string `json:"acme_name"` //ACME provider name
AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
UseDNS bool `json:"dns"` //Use DNS challenge
PropTimeout int `json:"prop_time"` //Propagation timeout
AcmeName string `json:"acme_name"` //ACME provider name
AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
UseDNS bool `json:"dns"` //Use DNS challenge
PropTimeout int `json:"prop_time"` //Propagation timeout
DNSServers []string `json:"dnsServers"` // DNS servers
}
// ACMEUser represents a user in the ACME system.
@ -86,8 +95,15 @@ func (a *ACMEHandler) Logf(message string, err error) {
a.Logger.PrintAndLog("ACME", message, err)
}
// Close closes the ACMEHandler.
// ACME Handler does not need to close anything
// Function defined for future compatibility
func (a *ACMEHandler) Close() error {
return nil
}
// ObtainCert obtains a certificate for the specified domains.
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int) (bool, error) {
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int, dnsServers string) (bool, error) {
a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
// generate private key
@ -157,15 +173,31 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
return false, err
}
// Load certificate info from JSON file
certInfo, err := LoadCertInfoJSON(fmt.Sprintf("./conf/certs/%s.json", certificateName))
if err == nil {
useDNS = certInfo.UseDNS
if dnsServers == "" && certInfo.DNSServers != nil && len(certInfo.DNSServers) > 0 {
dnsServers = strings.Join(certInfo.DNSServers, ",")
}
propagationTimeout = certInfo.PropTimeout
}
// Clean DNS servers
dnsNameservers := strings.Split(dnsServers, ",")
for i := range dnsNameservers {
dnsNameservers[i] = strings.TrimSpace(dnsNameservers[i])
}
// setup how to receive challenge
if useDNS {
if !a.Database.TableExists("acme") {
a.Database.NewTable("acme")
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -1)")
return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -1)")
}
if !a.Database.KeyExists("acme", certificateName+"_dns_provider") || !a.Database.KeyExists("acme", certificateName+"_dns_credentials") {
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -2)")
return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -2)")
}
var dnsCredentials string
@ -188,7 +220,13 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
return false, err
}
err = client.Challenge.SetDNS01Provider(provider)
if len(dnsNameservers) > 0 && dnsNameservers[0] != "" {
a.Logf("Using DNS servers: "+strings.Join(dnsNameservers, ", "), nil)
err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(dnsNameservers))
} else {
// Use default DNS-01 nameservers if dnsServers is empty
err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(defaultNameservers))
}
if err != nil {
a.Logf("Failed to resolve DNS01 Provider", err)
return false, err
@ -285,12 +323,13 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
}
// Save certificate's ACME info for renew usage
certInfo := &CertificateInfoJSON{
certInfo = &CertificateInfoJSON{
AcmeName: caName,
AcmeUrl: caUrl,
SkipTLS: skipTLS,
UseDNS: useDNS,
PropTimeout: propagationTimeout,
DNSServers: dnsNameservers,
}
certInfoBytes, err := json.Marshal(certInfo)
@ -477,7 +516,21 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
for _, domain := range domains {
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
}
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout)
// Extract DNS servers from the request
var dnsServers []string
dnsServersPara, err := utils.PostPara(r, "dnsServers")
if err == nil && dnsServersPara != "" {
dnsServers = strings.Split(dnsServersPara, ",")
for i := range dnsServers {
dnsServers[i] = strings.TrimSpace(dnsServers[i])
}
}
// Convert DNS servers slice to a single string
dnsServersString := strings.Join(dnsServers, ",")
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout, dnsServersString)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
@ -520,5 +573,10 @@ func LoadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
return nil, err
}
// Clean DNS servers
for i := range certInfo.DNSServers {
certInfo.DNSServers[i] = strings.TrimSpace(certInfo.DNSServers[i])
}
return certInfo, nil
}

View File

@ -26,6 +26,7 @@ type AutoRenewConfig struct {
Email string //Email for acme
RenewAll bool //Renew all or selective renew with the slice below
FilesToRenew []string //If RenewAll is false, renew these certificate files
DNSServers string // DNS servers
}
type AutoRenewer struct {
@ -308,7 +309,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
}
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
//This cert is expired
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
@ -355,6 +355,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
return a.renewExpiredDomains(expiredCertList)
}
// Close the auto renewer
func (a *AutoRenewer) Close() {
if a.TickerstopChan != nil {
a.TickerstopChan <- true
@ -390,7 +391,13 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
certInfo.PropTimeout = 300
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout)
// Extract DNS servers from the certificate info if available
var dnsServers string
if len(certInfo.DNSServers) > 0 {
dnsServers = strings.Join(certInfo.DNSServers, ",")
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout, dnsServers)
if err != nil {
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
} else {
@ -440,7 +447,7 @@ func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
}
// Handle update auto renew DNS configuration
func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) {
func (a *AutoRenewer) HandleSetDNS(w http.ResponseWriter, r *http.Request) {
dnsProvider, err := utils.PostPara(r, "dnsProvider")
if err != nil {
utils.SendErrorResponse(w, "dnsProvider not set")
@ -459,12 +466,18 @@ func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) {
return
}
dnsServers, err := utils.PostPara(r, "dnsServers")
if err != nil {
dnsServers = ""
}
if !a.AcmeHandler.Database.TableExists("acme") {
a.AcmeHandler.Database.NewTable("acme")
}
a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
a.AcmeHandler.Database.Write("acme", filename+"_dns_servers", dnsServers)
utils.SendOK(w)

View File

@ -3,7 +3,7 @@ package acme
/*
CA.go
This script load CA defination from embedded ca.json
This script load CA definition from embedded ca.json
*/
import (
_ "embed"
@ -13,7 +13,7 @@ import (
"strings"
)
// CA Defination, load from embeded json when startup
// CA definition, load from embeded json when startup
type CaDef struct {
Production map[string]string
Test map[string]string

View File

@ -210,8 +210,8 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
}
session.Values["authenticated"] = false
session.Values["username"] = nil
session.Save(r, w)
return nil
session.Options.MaxAge = -1
return session.Save(r, w)
}
// Get the current session username from request
@ -339,6 +339,7 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool {
if err != nil {
return false
}
// Check if user is authenticated
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
return false

View File

@ -1,34 +0,0 @@
package sso
/*
app.go
This file contains the app structure and app management
functions for the SSO module.
*/
// RegisteredUpstreamApp is a structure that contains the information of an
// upstream app that is registered with the SSO server
type RegisteredUpstreamApp struct {
ID string
Secret string
Domain []string
Scopes []string
SessionDuration int //in seconds, default to 1 hour
}
// RegisterUpstreamApp registers an upstream app with the SSO server
func (s *SSOHandler) ListRegisteredApps() []*RegisteredUpstreamApp {
apps := make([]*RegisteredUpstreamApp, 0)
for _, app := range s.Apps {
apps = append(apps, &app)
}
return apps
}
// RegisterUpstreamApp registers an upstream app with the SSO server
func (s *SSOHandler) GetAppByID(appID string) (*RegisteredUpstreamApp, bool) {
app, ok := s.Apps[appID]
return &app, ok
}

View File

@ -0,0 +1,136 @@
package authelia
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type AutheliaRouterOptions struct {
UseHTTPS bool //If the Authelia server is using HTTPS
AutheliaURL string //The URL of the Authelia server
Logger *logger.Logger
Database *database.Database
}
type AutheliaRouter struct {
options *AutheliaRouterOptions
}
// NewAutheliaRouter creates a new AutheliaRouter object
func NewAutheliaRouter(options *AutheliaRouterOptions) *AutheliaRouter {
options.Database.NewTable("authelia")
//Read settings from database, if exists
options.Database.Read("authelia", "autheliaURL", &options.AutheliaURL)
options.Database.Read("authelia", "useHTTPS", &options.UseHTTPS)
return &AutheliaRouter{
options: options,
}
}
// HandleSetAutheliaURLAndHTTPS is the internal handler for setting the Authelia URL and HTTPS
func (ar *AutheliaRouter) HandleSetAutheliaURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current settings
js, _ := json.Marshal(map[string]interface{}{
"useHTTPS": ar.options.UseHTTPS,
"autheliaURL": ar.options.AutheliaURL,
})
utils.SendJSONResponse(w, string(js))
return
} else if r.Method == http.MethodPost {
//Update the settings
autheliaURL, err := utils.PostPara(r, "autheliaURL")
if err != nil {
utils.SendErrorResponse(w, "autheliaURL not found")
return
}
useHTTPS, err := utils.PostBool(r, "useHTTPS")
if err != nil {
useHTTPS = false
}
//Write changes to runtime
ar.options.AutheliaURL = autheliaURL
ar.options.UseHTTPS = useHTTPS
//Write changes to database
ar.options.Database.Write("authelia", "autheliaURL", autheliaURL)
ar.options.Database.Write("authelia", "useHTTPS", useHTTPS)
utils.SendOK(w)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
// handleAutheliaAuth is the internal handler for Authelia authentication
// Set useHTTPS to true if your authelia server is using HTTPS
// Set autheliaURL to the URL of the Authelia server, e.g. authelia.example.com
func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
client := &http.Client{}
if ar.options.AutheliaURL == "" {
ar.options.Logger.PrintAndLog("Authelia", "Authelia URL not set", nil)
w.WriteHeader(500)
w.Write([]byte("500 - Internal Server Error"))
return errors.New("authelia URL not set")
}
protocol := "http"
if ar.options.UseHTTPS {
protocol = "https"
}
autheliaBaseURL := protocol + "://" + ar.options.AutheliaURL
//Remove tailing slash if any
if autheliaBaseURL[len(autheliaBaseURL)-1] == '/' {
autheliaBaseURL = autheliaBaseURL[:len(autheliaBaseURL)-1]
}
//Make a request to Authelia to verify the request
req, err := http.NewRequest("POST", autheliaBaseURL+"/api/verify", nil)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
req.Header.Add("X-Original-URL", fmt.Sprintf("%s://%s", scheme, r.Host))
// Copy cookies from the incoming request
for _, cookie := range r.Cookies() {
req.AddCookie(cookie)
}
// Making the verification request
resp, err := client.Do(req)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to verify", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
if resp.StatusCode != 200 {
redirectURL := autheliaBaseURL + "/?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String()) + "&rm=" + r.Method
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return errors.New("unauthorized")
}
return nil
}

View File

@ -1,271 +0,0 @@
package sso
/*
handlers.go
This file contains the handlers for the SSO module.
If you are looking for handlers for SSO user management,
please refer to userHandlers.go.
*/
import (
"encoding/json"
"net/http"
"strings"
"github.com/gofrs/uuid"
"imuslab.com/zoraxy/mod/utils"
)
// HandleSSOStatus handle the request to get the status of the SSO portal server
func (s *SSOHandler) HandleSSOStatus(w http.ResponseWriter, r *http.Request) {
type SSOStatus struct {
Enabled bool
SSOInterceptEnabled bool
ListeningPort int
AuthURL string
}
status := SSOStatus{
Enabled: s.ssoPortalServer != nil,
//SSOInterceptEnabled: s.ssoInterceptEnabled,
ListeningPort: s.Config.PortalServerPort,
AuthURL: s.Config.AuthURL,
}
js, _ := json.Marshal(status)
utils.SendJSONResponse(w, string(js))
}
// Wrapper for starting and stopping the SSO portal server
// require POST request with key "enable" and value "true" or "false"
func (s *SSOHandler) HandleSSOEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostBool(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "invalid enable value")
return
}
if enable {
s.HandleStartSSOPortal(w, r)
} else {
s.HandleStopSSOPortal(w, r)
}
}
// HandleStartSSOPortal handle the request to start the SSO portal server
func (s *SSOHandler) HandleStartSSOPortal(w http.ResponseWriter, r *http.Request) {
if s.ssoPortalServer != nil {
//Already enabled. Do restart instead.
err := s.RestartSSOServer()
if err != nil {
utils.SendErrorResponse(w, "failed to start SSO server")
return
}
utils.SendOK(w)
return
}
//Check if the authURL is set correctly. If not, return error
if s.Config.AuthURL == "" {
utils.SendErrorResponse(w, "auth URL not set")
return
}
//Start the SSO portal server in go routine
go s.StartSSOPortal()
//Write current state to database
err := s.Config.Database.Write("sso_conf", "enabled", true)
if err != nil {
utils.SendErrorResponse(w, "failed to update SSO state")
return
}
utils.SendOK(w)
}
// HandleStopSSOPortal handle the request to stop the SSO portal server
func (s *SSOHandler) HandleStopSSOPortal(w http.ResponseWriter, r *http.Request) {
if s.ssoPortalServer == nil {
//Already disabled
utils.SendOK(w)
return
}
err := s.ssoPortalServer.Close()
if err != nil {
s.Log("Failed to stop SSO portal server", err)
utils.SendErrorResponse(w, "failed to stop SSO portal server")
return
}
s.ssoPortalServer = nil
//Write current state to database
err = s.Config.Database.Write("sso_conf", "enabled", false)
if err != nil {
utils.SendErrorResponse(w, "failed to update SSO state")
return
}
utils.SendOK(w)
}
// HandlePortChange handle the request to change the SSO portal server port
func (s *SSOHandler) HandlePortChange(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current port
js, _ := json.Marshal(s.Config.PortalServerPort)
utils.SendJSONResponse(w, string(js))
return
}
port, err := utils.PostInt(r, "port")
if err != nil {
utils.SendErrorResponse(w, "invalid port given")
return
}
s.Config.PortalServerPort = port
//Write to the database
err = s.Config.Database.Write("sso_conf", "port", port)
if err != nil {
utils.SendErrorResponse(w, "failed to update port")
return
}
if s.IsRunning() {
//Restart the server if it is running
err = s.RestartSSOServer()
if err != nil {
utils.SendErrorResponse(w, "failed to restart SSO server")
return
}
}
utils.SendOK(w)
}
// HandleSetAuthURL handle the request to change the SSO auth URL
// This is the URL that the SSO portal server will redirect to for authentication
// e.g. auth.yourdomain.com
func (s *SSOHandler) HandleSetAuthURL(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current auth URL
js, _ := json.Marshal(s.Config.AuthURL)
utils.SendJSONResponse(w, string(js))
return
}
//Get the auth URL
authURL, err := utils.PostPara(r, "auth_url")
if err != nil {
utils.SendErrorResponse(w, "invalid auth URL given")
return
}
s.Config.AuthURL = authURL
//Write to the database
err = s.Config.Database.Write("sso_conf", "authurl", authURL)
if err != nil {
utils.SendErrorResponse(w, "failed to update auth URL")
return
}
//Clear the cookie store and restart the server
err = s.RestartSSOServer()
if err != nil {
utils.SendErrorResponse(w, "failed to restart SSO server")
return
}
utils.SendOK(w)
}
// HandleRegisterApp handle the request to register a new app to the SSO portal
func (s *SSOHandler) HandleRegisterApp(w http.ResponseWriter, r *http.Request) {
appName, err := utils.PostPara(r, "app_name")
if err != nil {
utils.SendErrorResponse(w, "invalid app name given")
return
}
id, err := utils.PostPara(r, "app_id")
if err != nil {
//If id is not given, use the app name with a random UUID
newID, err := uuid.NewV4()
if err != nil {
utils.SendErrorResponse(w, "failed to generate new app ID")
return
}
id = strings.ReplaceAll(appName, " ", "") + "-" + newID.String()
}
//Check if the given appid is already in use
if _, ok := s.Apps[id]; ok {
utils.SendErrorResponse(w, "app ID already in use")
return
}
/*
Process the app domain
An app can have multiple domains, separated by commas
Usually the app domain is the proxy rule that points to the app
For example, if the app is hosted at app.yourdomain.com, the app domain is app.yourdomain.com
*/
appDomain, err := utils.PostPara(r, "app_domain")
if err != nil {
utils.SendErrorResponse(w, "invalid app URL given")
return
}
appURLs := strings.Split(appDomain, ",")
//Remove padding and trailing spaces in each URL
for i := range appURLs {
appURLs[i] = strings.TrimSpace(appURLs[i])
}
//Create a new app entry
thisAppEntry := RegisteredUpstreamApp{
ID: id,
Secret: "",
Domain: appURLs,
Scopes: []string{},
SessionDuration: 3600,
}
js, _ := json.Marshal(thisAppEntry)
//Create a new app in the database
err = s.Config.Database.Write("sso_apps", appName, string(js))
if err != nil {
utils.SendErrorResponse(w, "failed to create new app")
return
}
//Also add the app to runtime config
s.Apps[appName] = thisAppEntry
utils.SendOK(w)
}
// HandleAppRemove handle the request to remove an app from the SSO portal
func (s *SSOHandler) HandleAppRemove(w http.ResponseWriter, r *http.Request) {
appID, err := utils.PostPara(r, "app_id")
if err != nil {
utils.SendErrorResponse(w, "invalid app ID given")
return
}
//Check if the app actually exists
if _, ok := s.Apps[appID]; !ok {
utils.SendErrorResponse(w, "app not found")
return
}
delete(s.Apps, appID)
//Also remove it from the database
err = s.Config.Database.Delete("sso_apps", appID)
if err != nil {
s.Log("Failed to remove app from database", err)
}
}

View File

@ -1,295 +0,0 @@
package sso
import (
"context"
_ "embed"
"encoding/json"
"log"
"net/http"
"net/url"
"time"
"github.com/go-oauth2/oauth2/v4/errors"
"github.com/go-oauth2/oauth2/v4/generates"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store"
"github.com/go-session/session"
"imuslab.com/zoraxy/mod/utils"
)
const (
SSO_SESSION_NAME = "ZoraxySSO"
)
type OAuth2Server struct {
srv *server.Server //oAuth server instance
config *SSOConfig
parent *SSOHandler
}
//go:embed static/auth.html
var authHtml []byte
//go:embed static/login.html
var loginHtml []byte
// NewOAuth2Server creates a new OAuth2 server instance
func NewOAuth2Server(config *SSOConfig, parent *SSOHandler) (*OAuth2Server, error) {
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
// token store
manager.MustTokenStorage(store.NewFileTokenStore("./conf/sso.db"))
// generate jwt access token
manager.MapAccessGenerate(generates.NewAccessGenerate())
//Load the information of registered app within the OAuth2 server
clientStore := store.NewClientStore()
clientStore.Set("myapp", &models.Client{
ID: "myapp",
Secret: "verysecurepassword",
Domain: "localhost:9094",
})
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
manager.MapClientStorage(clientStore)
thisServer := OAuth2Server{
config: config,
parent: parent,
}
//Create a new oauth server
srv := server.NewServer(server.NewConfig(), manager)
srv.SetPasswordAuthorizationHandler(thisServer.PasswordAuthorizationHandler)
srv.SetUserAuthorizationHandler(thisServer.UserAuthorizeHandler)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
//Set the access scope handler
srv.SetAuthorizeScopeHandler(thisServer.AuthorizationScopeHandler)
//Set the access token expiration handler based on requesting domain / hostname
srv.SetAccessTokenExpHandler(thisServer.ExpireHandler)
thisServer.srv = srv
return &thisServer, nil
}
// Password handler, validate if the given username and password are correct
func (oas *OAuth2Server) PasswordAuthorizationHandler(ctx context.Context, clientID, username, password string) (userID string, err error) {
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
if username == "test" && password == "test" {
userID = "test"
}
return
}
// User Authorization Handler, handle auth request from user
func (oas *OAuth2Server) UserAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
return
}
uid, ok := store.Get(SSO_SESSION_NAME)
if !ok {
if r.Form == nil {
r.ParseForm()
}
store.Set("ReturnUri", r.Form)
store.Save()
w.Header().Set("Location", "/oauth2/login")
w.WriteHeader(http.StatusFound)
return
}
userID = uid.(string)
store.Delete(SSO_SESSION_NAME)
store.Save()
return
}
// AccessTokenExpHandler, set the SSO session length default value
func (oas *OAuth2Server) ExpireHandler(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) {
requestHostname := r.Host
if requestHostname == "" {
//Use default value
return time.Hour, nil
}
//Get the Registered App Config from parent
appConfig, ok := oas.parent.Apps[requestHostname]
if !ok {
//Use default value
return time.Hour, nil
}
//Use the app's session length
return time.Second * time.Duration(appConfig.SessionDuration), nil
}
// AuthorizationScopeHandler, handle the scope of the request
func (oas *OAuth2Server) AuthorizationScopeHandler(w http.ResponseWriter, r *http.Request) (scope string, err error) {
//Get the scope from post or GEt request
if r.Form == nil {
if err := r.ParseForm(); err != nil {
return "none", err
}
}
//Get the hostname of the request
requestHostname := r.Host
if requestHostname == "" {
//No rule set. Use default
return "none", nil
}
//Get the Registered App Config from parent
appConfig, ok := oas.parent.Apps[requestHostname]
if !ok {
//No rule set. Use default
return "none", nil
}
//Check if the scope is set in the request
if v, ok := r.Form["scope"]; ok {
//Check if the requested scope is in the appConfig scope
if utils.StringInArray(appConfig.Scopes, v[0]) {
return v[0], nil
}
return "none", nil
}
return "none", nil
}
/* SSO Web Server Toggle Functions */
func (oas *OAuth2Server) RegisterOauthEndpoints(primaryMux *http.ServeMux) {
primaryMux.HandleFunc("/oauth2/login", oas.loginHandler)
primaryMux.HandleFunc("/oauth2/auth", oas.authHandler)
primaryMux.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var form url.Values
if v, ok := store.Get("ReturnUri"); ok {
form = v.(url.Values)
}
r.Form = form
store.Delete("ReturnUri")
store.Save()
err = oas.srv.HandleAuthorizeRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
})
primaryMux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
err := oas.srv.HandleTokenRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
primaryMux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
token, err := oas.srv.ValidationBearerToken(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
data := map[string]interface{}{
"expires_in": int64(time.Until(token.GetAccessCreateAt().Add(token.GetAccessExpiresIn())).Seconds()),
"client_id": token.GetClientID(),
"user_id": token.GetUserID(),
}
e := json.NewEncoder(w)
e.SetIndent("", " ")
e.Encode(data)
})
}
func (oas *OAuth2Server) loginHandler(w http.ResponseWriter, r *http.Request) {
store, err := session.Start(r.Context(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if r.Method == "POST" {
if r.Form == nil {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
//Load username and password from form post
username, err := utils.PostPara(r, "username")
if err != nil {
w.Write([]byte("invalid username or password"))
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
w.Write([]byte("invalid username or password"))
return
}
//Validate the user
if !oas.parent.ValidateUsernameAndPassword(username, password) {
//Wrong password
w.Write([]byte("invalid username or password"))
return
}
store.Set(SSO_SESSION_NAME, r.Form.Get("username"))
store.Save()
w.Header().Set("Location", "/oauth2/auth")
w.WriteHeader(http.StatusFound)
return
} else if r.Method == "GET" {
//Check if the user is logged in
if _, ok := store.Get(SSO_SESSION_NAME); ok {
w.Header().Set("Location", "/oauth2/auth")
w.WriteHeader(http.StatusFound)
return
}
}
//User not logged in. Show login page
w.Write(loginHtml)
}
func (oas *OAuth2Server) authHandler(w http.ResponseWriter, r *http.Request) {
store, err := session.Start(context.TODO(), w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, ok := store.Get(SSO_SESSION_NAME); !ok {
w.Header().Set("Location", "/oauth2/login")
w.WriteHeader(http.StatusFound)
return
}
//User logged in. Check if this user have previously authorized the app
//TODO: Check if the user have previously authorized the app
//User have not authorized the app. Show the authorization page
w.Write(authHtml)
}

View File

@ -1 +0,0 @@
package sso

View File

@ -1,58 +0,0 @@
package sso
import (
"encoding/json"
"net/http"
"strings"
)
type OpenIDConfiguration struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
JwksUri string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
ClaimsSupported []string `json:"claims_supported"`
}
func (h *SSOHandler) HandleDiscoveryRequest(w http.ResponseWriter, r *http.Request) {
//Prepend https:// if not present
authBaseURL := h.Config.AuthURL
if !strings.HasPrefix(authBaseURL, "http://") && !strings.HasPrefix(authBaseURL, "https://") {
authBaseURL = "https://" + authBaseURL
}
//Handle the discovery request
discovery := OpenIDConfiguration{
Issuer: authBaseURL,
AuthorizationEndpoint: authBaseURL + "/oauth2/authorize",
TokenEndpoint: authBaseURL + "/oauth2/token",
JwksUri: authBaseURL + "/jwks.json",
ResponseTypesSupported: []string{"code", "token"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{
"RS256",
},
ClaimsSupported: []string{
"sub", //Subject, usually the user ID
"iss", //Issuer, usually the server URL
"aud", //Audience, usually the client ID
"exp", //Expiration Time
"iat", //Issued At
"email", //Email
"locale", //Locale
"name", //Full Name
"nickname", //Nickname
"preferred_username", //Preferred Username
"website", //Website
},
}
//Write the response
js, _ := json.Marshal(discovery)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}

View File

@ -1,132 +0,0 @@
package sso
import (
"context"
"net/http"
"strconv"
"time"
"github.com/go-oauth2/oauth2/v4/errors"
"imuslab.com/zoraxy/mod/utils"
)
/*
server.go
This is the web server for the SSO portal. It contains the
HTTP server and the handlers for the SSO portal.
If you are looking for handlers that changes the settings
of the SSO portale or user management, please refer to
handlers.go.
*/
func (h *SSOHandler) InitSSOPortal(portalServerPort int) {
//Create a new web server for the SSO portal
pmux := http.NewServeMux()
fs := http.FileServer(http.FS(staticFiles))
pmux.Handle("/", fs)
//Register API endpoint for the SSO portal
pmux.HandleFunc("/sso/login", h.HandleLogin)
//Register API endpoint for autodiscovery
pmux.HandleFunc("/.well-known/openid-configuration", h.HandleDiscoveryRequest)
//Register OAuth2 endpoints
h.Oauth2Server.RegisterOauthEndpoints(pmux)
h.ssoPortalMux = pmux
}
// StartSSOPortal start the SSO portal server
// This function will block the main thread, call it in a goroutine
func (h *SSOHandler) StartSSOPortal() error {
if h.ssoPortalServer != nil {
return errors.New("SSO portal server already running")
}
h.ssoPortalServer = &http.Server{
Addr: ":" + strconv.Itoa(h.Config.PortalServerPort),
Handler: h.ssoPortalMux,
}
err := h.ssoPortalServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
h.Log("Failed to start SSO portal server", err)
}
return err
}
// StopSSOPortal stop the SSO portal server
func (h *SSOHandler) StopSSOPortal() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := h.ssoPortalServer.Shutdown(ctx)
if err != nil {
h.Log("Failed to stop SSO portal server", err)
return err
}
h.ssoPortalServer = nil
return nil
}
// StartSSOPortal start the SSO portal server
func (h *SSOHandler) RestartSSOServer() error {
if h.ssoPortalServer != nil {
err := h.StopSSOPortal()
if err != nil {
return err
}
}
go h.StartSSOPortal()
return nil
}
func (h *SSOHandler) IsRunning() bool {
return h.ssoPortalServer != nil
}
// HandleLogin handle the login request
func (h *SSOHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
//Handle the login request
username, err := utils.PostPara(r, "username")
if err != nil {
utils.SendErrorResponse(w, "invalid username or password")
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
utils.SendErrorResponse(w, "invalid username or password")
return
}
rememberMe, err := utils.PostBool(r, "remember_me")
if err != nil {
rememberMe = false
}
//Check if the user exists
userEntry, err := h.GetSSOUser(username)
if err != nil {
utils.SendErrorResponse(w, "user not found")
return
}
//Check if the password is correct
if !userEntry.VerifyPassword(password) {
utils.SendErrorResponse(w, "incorrect password")
return
}
//Create a new session for the user
session, _ := h.cookieStore.Get(r, "Zoraxy-SSO")
session.Values["username"] = username
if rememberMe {
session.Options.MaxAge = 86400 * 15 //15 days
} else {
session.Options.MaxAge = 3600 //1 hour
}
session.Save(r, w) //Save the session
utils.SendOK(w)
}

View File

@ -1,158 +0,0 @@
package sso
import (
"embed"
"net/http"
"github.com/gorilla/sessions"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
)
/*
sso.go
This file contains the main SSO handler and the SSO configuration
structure. It also contains the main SSO handler functions.
SSO web interface are stored in the static folder, which is embedded
into the binary.
*/
//go:embed static/*
var staticFiles embed.FS //Static files for the SSO portal
type SSOConfig struct {
SystemUUID string //System UUID, should be passed in from main scope
AuthURL string //Authentication subdomain URL, e.g. auth.example.com
PortalServerPort int //SSO portal server port
Database *database.Database //System master key-value database
Logger *logger.Logger
}
// SSOHandler is the main SSO handler structure
type SSOHandler struct {
cookieStore *sessions.CookieStore
ssoPortalServer *http.Server
ssoPortalMux *http.ServeMux
Oauth2Server *OAuth2Server
Config *SSOConfig
Apps map[string]RegisteredUpstreamApp
}
// Create a new Zoraxy SSO handler
func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) {
//Create a cookie store for the SSO handler
cookieStore := sessions.NewCookieStore([]byte(config.SystemUUID))
cookieStore.Options = &sessions.Options{
Path: "",
Domain: "",
MaxAge: 0,
Secure: false,
HttpOnly: false,
SameSite: 0,
}
config.Database.NewTable("sso_users") //For storing user information
config.Database.NewTable("sso_conf") //For storing SSO configuration
config.Database.NewTable("sso_apps") //For storing registered apps
//Create the SSO Handler
thisHandler := SSOHandler{
cookieStore: cookieStore,
Config: config,
}
//Read the app info from database
thisHandler.Apps = make(map[string]RegisteredUpstreamApp)
//Create an oauth2 server
oauth2Server, err := NewOAuth2Server(config, &thisHandler)
if err != nil {
return nil, err
}
//Register endpoints
thisHandler.Oauth2Server = oauth2Server
thisHandler.InitSSOPortal(config.PortalServerPort)
return &thisHandler, nil
}
func (h *SSOHandler) RestorePreviousRunningState() {
//Load the previous SSO state
ssoEnabled := false
ssoPort := 5488
ssoAuthURL := ""
h.Config.Database.Read("sso_conf", "enabled", &ssoEnabled)
h.Config.Database.Read("sso_conf", "port", &ssoPort)
h.Config.Database.Read("sso_conf", "authurl", &ssoAuthURL)
if ssoAuthURL == "" {
//Cannot enable SSO without auth URL
ssoEnabled = false
}
h.Config.PortalServerPort = ssoPort
h.Config.AuthURL = ssoAuthURL
if ssoEnabled {
go h.StartSSOPortal()
}
}
// ServeForwardAuth handle the SSO request in interception mode
// Suppose to be called in dynamicproxy.
// Return true if the request is allowed to pass, false if the request is blocked and shall not be further processed
func (h *SSOHandler) ServeForwardAuth(w http.ResponseWriter, r *http.Request) bool {
//Get the current uri for appending to the auth subdomain
originalRequestURL := r.RequestURI
redirectAuthURL := h.Config.AuthURL
if redirectAuthURL == "" || !h.IsRunning() {
//Redirect not set or auth server is offlined
w.Write([]byte("SSO auth URL not set or SSO server offline."))
//TODO: Use better looking template if exists
return false
}
//Check if the user have the cookie "Zoraxy-SSO" set
session, err := h.cookieStore.Get(r, "Zoraxy-SSO")
if err != nil {
//Redirect to auth subdomain
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=new&t="+originalRequestURL, http.StatusFound)
return false
}
//Check if the user is logged in
if session.Values["username"] != true {
//Redirect to auth subdomain
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=expired&t="+originalRequestURL, http.StatusFound)
return false
}
//Check if the current request subdomain is allowed
userName := session.Values["username"].(string)
user, err := h.GetSSOUser(userName)
if err != nil {
//User might have been removed from SSO. Redirect to auth subdomain
http.Redirect(w, r, redirectAuthURL, http.StatusFound)
return false
}
//Check if the user have access to the current subdomain
if !user.Subdomains[r.Host].AllowAccess {
//User is not allowed to access the current subdomain. Sent 403
http.Error(w, "Forbidden", http.StatusForbidden)
//TODO: Use better looking template if exists
return false
}
//User is logged in, continue to the next handler
return true
}
// Log a message with the SSO module tag
func (h *SSOHandler) Log(message string, err error) {
h.Config.Logger.PrintAndLog("SSO", message, err)
}

View File

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Auth</title>
<link
rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
/>
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="jumbotron">
<form action="/oauth2/authorize" method="POST">
<h1>Authorize</h1>
<p>The client would like to perform actions on your behalf.</p>
<p>
<button
type="submit"
class="btn btn-primary btn-lg"
style="width:200px;"
>
Allow
</button>
</p>
</form>
</div>
</div>
</body>
</html>

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Login Page</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>
<body>
<div class="ui container">
<div class="ui middle aligned center aligned grid">
<div class="column">
<h2 class="ui teal image header">
<div class="content">
Log in to your account
</div>
</h2>
<form class="ui large form">
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" name="username" placeholder="Username">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" name="password" placeholder="Password">
</div>
</div>
<div class="ui fluid large teal submit button">Login</div>
</div>
<div class="ui error message"></div>
</form>
<div class="ui message">
New to us? <a href="#">Sign Up</a>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
</body>
</html>

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<h1>Login In</h1>
<form action="/oauth2/login" method="POST">
<div class="form-group">
<label for="username">User Name</label>
<input type="text" class="form-control" name="username" required placeholder="Please enter your user name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" placeholder="Please enter your password">
</div>
<button type="submit" class="btn btn-success">Login</button>
</form>
</div>
</body>
</html>

View File

@ -1,309 +0,0 @@
package sso
/*
userHandlers.go
Handlers for SSO user management
If you are looking for handlers that changes the settings
of the SSO portal (e.g. authURL or port), please refer to
handlers.go.
*/
import (
"encoding/json"
"errors"
"net/http"
"github.com/gofrs/uuid"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/utils"
)
// HandleAddUser handle the request to add a new user to the SSO system
func (s *SSOHandler) HandleAddUser(w http.ResponseWriter, r *http.Request) {
username, err := utils.PostPara(r, "username")
if err != nil {
utils.SendErrorResponse(w, "invalid username given")
return
}
password, err := utils.PostPara(r, "password")
if err != nil {
utils.SendErrorResponse(w, "invalid password given")
return
}
newUserId, err := uuid.NewV4()
if err != nil {
utils.SendErrorResponse(w, "failed to generate new user ID")
return
}
//Create a new user entry
thisUserEntry := UserEntry{
UserID: newUserId.String(),
Username: username,
PasswordHash: auth.Hash(password),
TOTPCode: "",
Enable2FA: false,
}
js, _ := json.Marshal(thisUserEntry)
//Create a new user in the database
err = s.Config.Database.Write("sso_users", newUserId.String(), string(js))
if err != nil {
utils.SendErrorResponse(w, "failed to create new user")
return
}
utils.SendOK(w)
}
// Edit user information, only accept change of username, password and enabled subdomain filed
func (s *SSOHandler) HandleEditUser(w http.ResponseWriter, r *http.Request) {
userID, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userID)) {
utils.SendErrorResponse(w, "user not found")
return
}
//Load the user entry from database
userEntry, err := s.GetSSOUser(userID)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
//Update each of the fields if it is provided
username, err := utils.PostPara(r, "username")
if err == nil {
userEntry.Username = username
}
password, err := utils.PostPara(r, "password")
if err == nil {
userEntry.PasswordHash = auth.Hash(password)
}
//Update the user entry in the database
js, _ := json.Marshal(userEntry)
err = s.Config.Database.Write("sso_users", userID, string(js))
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleRemoveUser remove a user from the SSO system
func (s *SSOHandler) HandleRemoveUser(w http.ResponseWriter, r *http.Request) {
userID, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userID)) {
utils.SendErrorResponse(w, "user not found")
return
}
//Remove the user from the database
err = s.Config.Database.Delete("sso_users", userID)
if err != nil {
utils.SendErrorResponse(w, "failed to remove user")
return
}
utils.SendOK(w)
}
// HandleListUser list all users in the SSO system
func (s *SSOHandler) HandleListUser(w http.ResponseWriter, r *http.Request) {
ssoUsers, err := s.ListSSOUsers()
if err != nil {
utils.SendErrorResponse(w, "failed to list users")
return
}
js, _ := json.Marshal(ssoUsers)
utils.SendJSONResponse(w, string(js))
}
// HandleAddSubdomain add a subdomain to a user
func (s *SSOHandler) HandleAddSubdomain(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
subdomain, err := utils.PostPara(r, "subdomain")
if err != nil {
utils.SendErrorResponse(w, "invalid subdomain given")
return
}
allowAccess, err := utils.PostBool(r, "allow_access")
if err != nil {
utils.SendErrorResponse(w, "invalid allow access value given")
return
}
UserEntry.Subdomains[subdomain] = &SubdomainAccessRule{
Subdomain: subdomain,
AllowAccess: allowAccess,
}
err = UserEntry.Update()
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleRemoveSubdomain remove a subdomain from a user
func (s *SSOHandler) HandleRemoveSubdomain(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
subdomain, err := utils.PostPara(r, "subdomain")
if err != nil {
utils.SendErrorResponse(w, "invalid subdomain given")
return
}
delete(UserEntry.Subdomains, subdomain)
err = UserEntry.Update()
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleEnable2FA enable 2FA for a user
func (s *SSOHandler) HandleEnable2FA(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
UserEntry.Enable2FA = true
provisionUri, err := UserEntry.ResetTotp(UserEntry.UserID, "Zoraxy-SSO")
if err != nil {
utils.SendErrorResponse(w, "failed to reset TOTP")
return
}
//As the ResetTotp function will update the user entry in the database, no need to call Update here
js, _ := json.Marshal(provisionUri)
utils.SendJSONResponse(w, string(js))
}
// Handle Disable 2FA for a user
func (s *SSOHandler) HandleDisable2FA(w http.ResponseWriter, r *http.Request) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
utils.SendErrorResponse(w, "invalid user ID given")
return
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return
}
UserEntry.Enable2FA = false
UserEntry.TOTPCode = ""
err = UserEntry.Update()
if err != nil {
utils.SendErrorResponse(w, "failed to update user entry")
return
}
utils.SendOK(w)
}
// HandleVerify2FA verify the 2FA code for a user
func (s *SSOHandler) HandleVerify2FA(w http.ResponseWriter, r *http.Request) (bool, error) {
userid, err := utils.PostPara(r, "user_id")
if err != nil {
return false, errors.New("invalid user ID given")
}
if !(s.SSOUserExists(userid)) {
utils.SendErrorResponse(w, "user not found")
return false, errors.New("user not found")
}
UserEntry, err := s.GetSSOUser(userid)
if err != nil {
utils.SendErrorResponse(w, "failed to load user entry")
return false, errors.New("failed to load user entry")
}
totpCode, _ := utils.PostPara(r, "totp_code")
if !UserEntry.Enable2FA {
//If 2FA is not enabled, return true
return true, nil
}
if !UserEntry.VerifyTotp(totpCode) {
return false, nil
}
return true, nil
}

View File

@ -1,141 +0,0 @@
package sso
import (
"encoding/json"
"time"
"github.com/xlzd/gotp"
"imuslab.com/zoraxy/mod/auth"
)
/*
users.go
This file contains the user structure and user management
functions for the SSO module.
If you are looking for handlers, please refer to handlers.go.
*/
type SubdomainAccessRule struct {
Subdomain string
AllowAccess bool
}
type UserEntry struct {
UserID string `json:sub` //User ID
Username string `json:"name"` //Username
Email string `json:"email"` //Email
PasswordHash string `json:"passwordhash"` //Password hash
TOTPCode string `json:"totpcode"` //TOTP code
Enable2FA bool `json:"enable2fa"` //Enable 2FA
Subdomains map[string]*SubdomainAccessRule `json:"subdomains"` //Subdomain access rules
LastLogin int64 `json:"lastlogin"` //Last login time
LastLoginIP string `json:"lastloginip"` //Last login IP
LastLoginCountry string `json:"lastlogincountry"` //Last login country
parent *SSOHandler //Parent SSO handler
}
type ClientResponse struct {
Sub string `json:"sub"` //User ID
Name string `json:"name"` //Username
Nickname string `json:"nickname"` //Nickname
PreferredUsername string `json:"preferred_username"` //Preferred Username
Email string `json:"email"` //Email
Locale string `json:"locale"` //Locale
Website string `json:"website"` //Website
}
func (s *SSOHandler) SSOUserExists(userid string) bool {
//Check if the user exists in the database
var userEntry UserEntry
err := s.Config.Database.Read("sso_users", userid, &userEntry)
return err == nil
}
func (s *SSOHandler) GetSSOUser(userid string) (UserEntry, error) {
//Load the user entry from database
var userEntry UserEntry
err := s.Config.Database.Read("sso_users", userid, &userEntry)
if err != nil {
return UserEntry{}, err
}
userEntry.parent = s
return userEntry, nil
}
func (s *SSOHandler) ListSSOUsers() ([]*UserEntry, error) {
entries, err := s.Config.Database.ListTable("sso_users")
if err != nil {
return nil, err
}
ssoUsers := []*UserEntry{}
for _, keypairs := range entries {
group := new(UserEntry)
json.Unmarshal(keypairs[1], &group)
group.parent = s
ssoUsers = append(ssoUsers, group)
}
return ssoUsers, nil
}
// Validate the username and password
func (s *SSOHandler) ValidateUsernameAndPassword(username string, password string) bool {
//Validate the username and password
var userEntry UserEntry
err := s.Config.Database.Read("sso_users", username, &userEntry)
if err != nil {
return false
}
//TODO: Remove after testing
if (username == "test") && (password == "test") {
return true
}
return userEntry.VerifyPassword(password)
}
func (s *UserEntry) VerifyPassword(password string) bool {
return s.PasswordHash == auth.Hash(password)
}
// Write changes in the user entry back to the database
func (u *UserEntry) Update() error {
js, _ := json.Marshal(u)
err := u.parent.Config.Database.Write("sso_users", u.UserID, string(js))
if err != nil {
return err
}
return nil
}
// Reset and update the TOTP code for the current user
// Return the provision uri of the new TOTP code for Google Authenticator
func (u *UserEntry) ResetTotp(accountName string, issuerName string) (string, error) {
u.TOTPCode = gotp.RandomSecret(16)
totp := gotp.NewDefaultTOTP(u.TOTPCode)
err := u.Update()
if err != nil {
return "", err
}
return totp.ProvisioningUri(accountName, issuerName), nil
}
// Verify the TOTP code at current time
func (u *UserEntry) VerifyTotp(enteredCode string) bool {
totp := gotp.NewDefaultTOTP(u.TOTPCode)
return totp.Verify(enteredCode, time.Now().Unix())
}
func (u *UserEntry) GetClientResponse() ClientResponse {
return ClientResponse{
Sub: u.UserID,
Name: u.Username,
Nickname: u.Username,
PreferredUsername: u.Username,
Email: u.Email,
Locale: "en",
Website: "",
}
}

View File

@ -9,17 +9,39 @@ package database
*/
import (
"sync"
"log"
"runtime"
"imuslab.com/zoraxy/mod/database/dbinc"
)
type Database struct {
Db interface{} //This will be nil on openwrt and *bolt.DB in the rest of the systems
Tables sync.Map
ReadOnly bool
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, readOnlyMode bool) (*Database, error) {
return newDatabase(dbfile, readOnlyMode)
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
}
/*
@ -29,39 +51,33 @@ func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
err := sysdb.DropTable("MyTable")
*/
func (d *Database) UpdateReadWriteMode(readOnly bool) {
d.ReadOnly = readOnly
}
//Dump the whole db into a log file
func (d *Database) Dump(filename string) ([]string, error) {
return d.dump(filename)
}
//Create a new table
// Create a new table
func (d *Database) NewTable(tableName string) error {
return d.newTable(tableName)
}
//Check is table exists
// Check is table exists
func (d *Database) TableExists(tableName string) bool {
return d.tableExists(tableName)
}
//Drop the given table
// 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:
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);
err := sysdb.Write("MyTable", "username/message",thisDemo);
*/
func (d *Database) Write(tableName string, key string, value interface{}) error {
return d.write(tableName, key, value)
@ -81,14 +97,21 @@ func (d *Database) Read(tableName string, key string, assignee interface{}) erro
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
Delete a value from the database table given tablename and key
err := sysdb.Delete("MyTable", "username/message");
err := sysdb.Delete("MyTable", "username/message");
*/
func (d *Database) Delete(tableName string, key string) error {
return d.delete(tableName, key)
@ -115,6 +138,9 @@ func (d *Database) ListTable(tableName string) ([][][]byte, error) {
return d.listTable(tableName)
}
/*
Close the database connection
*/
func (d *Database) Close() {
d.close()
}

View File

@ -4,183 +4,67 @@
package database
import (
"encoding/json"
"errors"
"log"
"sync"
"github.com/boltdb/bolt"
"imuslab.com/zoraxy/mod/database/dbbolt"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/database/dbleveldb"
)
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
db, err := bolt.Open(dbfile, 0600, nil)
if err != nil {
return nil, err
func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
if backendType == dbinc.BackendFSOnly {
return nil, errors.New("Unsupported backend type for this platform")
}
tableMap := sync.Map{}
//Build the table list from database
err = db.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(name []byte, _ *bolt.Bucket) error {
tableMap.Store(string(name), "")
return nil
})
})
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: db,
Tables: tableMap,
ReadOnly: readOnlyMode,
Db: nil,
BackendType: backendType,
Backend: db,
}, err
}
//Dump the whole db into a log file
func (d *Database) dump(filename string) ([]string, error) {
results := []string{}
d.Tables.Range(func(tableName, v interface{}) bool {
entries, err := d.ListTable(tableName.(string))
if err != nil {
log.Println("Reading table " + tableName.(string) + " failed: " + err.Error())
return false
}
for _, keypairs := range entries {
results = append(results, string(keypairs[0])+":"+string(keypairs[1])+"\n")
}
return true
})
return results, nil
}
//Create a new table
func (d *Database) newTable(tableName string) error {
if d.ReadOnly == true {
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
return nil
})
d.Tables.Store(tableName, "")
return err
return d.Backend.NewTable(tableName)
}
//Check is table exists
func (d *Database) tableExists(tableName string) bool {
if _, ok := d.Tables.Load(tableName); ok {
return true
}
return false
return d.Backend.TableExists(tableName)
}
//Drop the given table
func (d *Database) dropTable(tableName string) error {
if d.ReadOnly == true {
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
return d.Backend.DropTable(tableName)
}
//Write to table
func (d *Database) write(tableName string, key string, value interface{}) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
jsonString, err := json.Marshal(value)
if err != nil {
return err
}
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
b := tx.Bucket([]byte(tableName))
err = b.Put([]byte(key), jsonString)
return err
})
return err
return d.Backend.Write(tableName, key, value)
}
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
return d.Backend.Read(tableName, key, assignee)
}
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
}
}
return d.Backend.KeyExists(tableName, key)
}
func (d *Database) delete(tableName string, key string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte(tableName)).Delete([]byte(key))
return nil
})
return err
return d.Backend.Delete(tableName, key)
}
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
return d.Backend.ListTable(tableName)
}
func (d *Database) close() {
d.Db.(*bolt.DB).Close()
d.Backend.Close()
}

View File

@ -10,10 +10,19 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"imuslab.com/zoraxy/mod/database/dbinc"
)
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
/*
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)
@ -21,24 +30,11 @@ func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
return nil, err
}
tableMap := sync.Map{}
//build the table list from file system
files, err := filepath.Glob(filepath.Join(dbRootPath, "/*"))
if err != nil {
return nil, err
}
for _, file := range files {
if isDirectory(file) {
tableMap.Store(filepath.Base(file), "")
}
}
log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
return &Database{
Db: dbRootPath,
Tables: tableMap,
ReadOnly: readOnlyMode,
Db: dbRootPath,
BackendType: dbinc.BackendFSOnly,
Backend: nil,
}, nil
}
@ -61,9 +57,7 @@ func (d *Database) dump(filename string) ([]string, error) {
}
func (d *Database) newTable(tableName string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if !fileExists(tablePath) {
return os.MkdirAll(tablePath, 0755)
@ -85,9 +79,7 @@ func (d *Database) tableExists(tableName string) bool {
}
func (d *Database) dropTable(tableName string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if d.tableExists(tableName) {
return os.RemoveAll(tablePath)
@ -98,9 +90,7 @@ func (d *Database) dropTable(tableName string) error {
}
func (d *Database) write(tableName string, key string, value interface{}) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
js, err := json.Marshal(value)
if err != nil {
@ -138,9 +128,7 @@ func (d *Database) keyExists(tableName string, key string) bool {
}
func (d *Database) delete(tableName string, key string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
if !d.keyExists(tableName, key) {
return errors.New("key not exists")
}

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"
"imuslab.com/zoraxy/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"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"imuslab.com/zoraxy/mod/database/dbinc"
)
// 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"
"imuslab.com/zoraxy/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

@ -83,22 +83,11 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
//SSO Interception Mode
if sep.UseSSOIntercept {
allowPass := h.Parent.Option.SSOHandler.ServeForwardAuth(w, r)
if !allowPass {
h.Parent.Option.Logger.LogHTTPRequest(r, "sso-x", 307)
return
}
}
//Validate basic auth
if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return
}
respWritten := handleAuthProviderRouting(sep, w, r, h)
if respWritten {
//Request handled by subroute
return
}
//Check if any virtual directory rules matches
@ -108,7 +97,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyTypeRoot {
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint
@ -153,7 +142,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
handleRootRouting
This function handle root routing situations where there are no subdomain
This function handle root routing (aka default sites) situations where there are no subdomain
, vdir or special routing rule matches the requested URI.
Once entered this routing segment, the root routing options will take over
@ -180,7 +169,7 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
//Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyTypeRoot {
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint
@ -220,13 +209,40 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
case DefaultSite_NotFoundPage:
//Serve the not found page, use template if exists
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
if err != nil {
w.Write(page_hosterror)
} else {
w.Write(template)
h.serve404PageWithTemplate(w, r)
case DefaultSite_NoResponse:
//No response. Just close the connection
h.Parent.logRequest(r, false, 444, "root-no_resp", domainOnly)
hijacker, ok := w.(http.Hijacker)
if !ok {
w.WriteHeader(http.StatusNoContent)
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
w.WriteHeader(http.StatusNoContent)
return
}
conn.Close()
case DefaultSite_TeaPot:
//I'm a teapot
h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly)
http.Error(w, "I'm a teapot", http.StatusTeapot)
default:
//Unknown routing option. Send empty response
h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly)
http.Error(w, "544 - No Route Defined", 544)
}
}
// Serve 404 page with template if exists
func (h *ProxyHandler) serve404PageWithTemplate(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
if err != nil {
w.Write(page_hosterror)
} else {
w.Write(template)
}
}

View File

@ -1,7 +1,6 @@
package dynamicproxy
import (
"log"
"net/http"
"os"
"path/filepath"
@ -16,7 +15,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
if err != nil {
//Unable to load access rule. Target rule not found?
log.Println("[Proxy] Unable to load access rule: " + ruleID)
h.Parent.Option.Logger.PrintAndLog("proxy-access", "Unable to load access rule: "+ruleID, err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 - Internal Server Error"))
return true

View File

@ -0,0 +1,108 @@
package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
/*
authProviders.go
This script handle authentication providers
*/
/*
Central Authentication Provider Router
This function will route the request to the correct authentication provider
if the return value is true, do not continue to the next handler
handleAuthProviderRouting takes in 4 parameters:
- sep: the ProxyEndpoint object
- w: the http.ResponseWriter object
- r: the http.Request object
- h: the ProxyHandler object
and return a boolean indicate if the request is written to http.ResponseWriter
- true: the request is handled, do not write to http.ResponseWriter
- false: the request is not handled (usually means auth ok), continue to the next handler
*/
func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool {
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return true
}
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia {
err := h.handleAutheliaAuth(w, r)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return true
}
}
//No authentication provider, do not need to handle
return false
}
/* Basic Auth */
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.AuthenticationProvider.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.AuthenticationProvider.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
for _, cred := range pe.AuthenticationProvider.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true
//Set the X-Remote-User header
r.Header.Set("X-Remote-User", u)
break
}
}
if !matchingFound {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}
/* Authelia */
// Handle authelia auth routing
func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r)
}

View File

@ -1,66 +0,0 @@
package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
/*
BasicAuth.go
This file handles the basic auth on proxy endpoints
if RequireBasicAuth is set to true
*/
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
for _, cred := range pe.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true
//Set the X-Remote-User header
r.Header.Set("X-Remote-User", u)
break
}
}
if !matchingFound {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}

View File

@ -0,0 +1,65 @@
package dynamicproxy
import (
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
/*
Default Provider
This script provide the default options for all datatype
provided by dynamicproxy module
*/
// GetDefaultAuthenticationProvider return a default authentication provider
func GetDefaultAuthenticationProvider() *AuthenticationProvider {
return &AuthenticationProvider{
AuthMethod: AuthMethodNone,
BasicAuthCredentials: []*BasicAuthCredentials{},
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
BasicAuthGroupIDs: []string{},
AutheliaURL: "",
UseHTTPS: false,
}
}
// GetDefaultHeaderRewriteRules return a default header rewrite rules
func GetDefaultHeaderRewriteRules() *HeaderRewriteRules {
return &HeaderRewriteRules{
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
RequestHostOverwrite: "",
HSTSMaxAge: 0,
EnablePermissionPolicyHeader: false,
PermissionPolicy: nil,
DisableHopByHopHeaderRemoval: false,
}
}
// GetDefaultProxyEndpoint return a default proxy endpoint
func GetDefaultProxyEndpoint() ProxyEndpoint {
randomPrefix := uuid.New().String()
return ProxyEndpoint{
ProxyType: ProxyTypeHost,
RootOrMatchingDomain: randomPrefix + ".internal",
MatchingDomainAlias: []string{},
ActiveOrigins: []*loadbalance.Upstream{},
InactiveOrigins: []*loadbalance.Upstream{},
UseStickySession: false,
UseActiveLoadBalance: false,
Disabled: false,
BypassGlobalTLS: false,
VirtualDirectories: []*VirtualDirectoryEndpoint{},
HeaderRewriteRules: GetDefaultHeaderRewriteRules(),
EnableWebsocketCustomHeaders: false,
AuthenticationProvider: GetDefaultAuthenticationProvider(),
RequireRateLimit: false,
RateLimit: 0,
DisableUptimeMonitor: false,
AccessFilterUUID: "default",
DefaultSiteOption: DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
}
}

View File

@ -9,8 +9,15 @@ package domainsniff
*/
import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
// Check if the domain is reachable and return err if not reachable
@ -25,7 +32,132 @@ func DomainReachableWithError(domain string) error {
return nil
}
// Check if a domain have TLS but it is self-signed or expired
// Return false if sniff error
func DomainIsSelfSigned(domain string) bool {
//Extract the domain from URl in case the user input the full URL
host, port, err := net.SplitHostPort(domain)
if err != nil {
host = domain
} else {
domain = host + ":" + port
}
if !strings.Contains(domain, ":") {
domain = domain + ":443"
}
//Get the certificate
conn, err := net.Dial("tcp", domain)
if err != nil {
return false
}
defer conn.Close()
//Connect with TLS using secure verify
tlsConn := tls.Client(conn, nil)
err = tlsConn.Handshake()
if err == nil {
//This is a valid certificate
fmt.Println()
return false
}
//Connect with TLS using insecure skip verify
config := &tls.Config{
InsecureSkipVerify: true,
}
tlsConn = tls.Client(conn, config)
err = tlsConn.Handshake()
//If the handshake is successful, this is a self-signed certificate
return err == nil
}
// Check if domain reachable
func DomainReachable(domain string) bool {
return DomainReachableWithError(domain) == nil
}
// Check if domain is served by a web server using HTTPS
func DomainUsesTLS(targetURL string) bool {
//Check if the site support https
httpsUrl := fmt.Sprintf("https://%s", targetURL)
httpUrl := fmt.Sprintf("http://%s", targetURL)
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Head(httpsUrl)
if err == nil && resp.StatusCode == http.StatusOK {
return true
}
resp, err = client.Head(httpUrl)
if err == nil && resp.StatusCode == http.StatusOK {
return false
}
//If the site is not reachable, return false
return false
}
/*
WebSocket Header Sniff
*/
// Check if the requst is a special case where
// user defined header shall not be passed over
func RequireWebsocketHeaderCopy(r *http.Request) bool {
//Return false for proxmox
if IsProxmox(r) {
return false
}
//Add more edge cases here
return true
}
/*
Request Handlers
*/
//Check if site support TLS
//Pass in ?selfsignchk=true to also check for self-signed certificate
func HandleCheckSiteSupportTLS(w http.ResponseWriter, r *http.Request) {
targetURL, err := utils.PostPara(r, "url")
if err != nil {
utils.SendErrorResponse(w, "invalid url given")
return
}
//If the selfsign flag is set, also chec for self-signed certificate
_, err = utils.PostBool(r, "selfsignchk")
if err == nil {
//Return the https and selfsign status
type result struct {
Protocol string `json:"protocol"`
SelfSign bool `json:"selfsign"`
}
scanResult := result{Protocol: "http", SelfSign: false}
if DomainUsesTLS(targetURL) {
scanResult.Protocol = "https"
if DomainIsSelfSigned(targetURL) {
scanResult.SelfSign = true
}
}
js, _ := json.Marshal(scanResult)
utils.SendJSONResponse(w, string(js))
return
}
if DomainUsesTLS(targetURL) {
js, _ := json.Marshal("https")
utils.SendJSONResponse(w, string(js))
return
} else {
js, _ := json.Marshal("http")
utils.SendJSONResponse(w, string(js))
return
}
}

View File

@ -17,5 +17,6 @@ func IsProxmox(r *http.Request) bool {
return true
}
}
return false
}

View File

@ -12,6 +12,7 @@ import (
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/dynamicproxy/modh2c"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
@ -82,8 +83,12 @@ type requestCanceler interface {
}
type DpcoreOptions struct {
IgnoreTLSVerification bool //Disable all TLS verification when request pass through this proxy router
FlushInterval time.Duration //Duration to flush in normal requests. Stream request or keep-alive request will always flush with interval of -1 (immediately)
IgnoreTLSVerification bool //Disable all TLS verification when request pass through this proxy router
FlushInterval time.Duration //Duration to flush in normal requests. Stream request or keep-alive request will always flush with interval of -1 (immediately)
MaxConcurrentConnection int //Maxmium concurrent requests to this server
ResponseHeaderTimeout int64 //Timeout for response header, set to 0 for default
IdleConnectionTimeout int64 //Idle connection timeout, set to 0 for default
UseH2CRoundTripper bool //Use H2C RoundTripper for HTTP/2.0 connection
}
func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
@ -100,20 +105,39 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
}
//Hack the default transporter to handle more connections
thisTransporter := http.DefaultTransport
//Hack the default transporter to handle more connections
optimalConcurrentConnection := 32
if dpcOptions.MaxConcurrentConnection > 0 {
optimalConcurrentConnection = dpcOptions.MaxConcurrentConnection
}
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).DisableCompression = true
if dpcOptions.ResponseHeaderTimeout > 0 {
//Set response header timeout
thisTransporter.(*http.Transport).ResponseHeaderTimeout = time.Duration(dpcOptions.ResponseHeaderTimeout) * time.Millisecond
}
if dpcOptions.IdleConnectionTimeout > 0 {
//Set idle connection timeout
thisTransporter.(*http.Transport).IdleConnTimeout = time.Duration(dpcOptions.IdleConnectionTimeout) * time.Millisecond
}
if dpcOptions.IgnoreTLSVerification {
//Ignore TLS certificate validation error
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
}
if dpcOptions.UseH2CRoundTripper {
//Use H2C RoundTripper for HTTP/2.0 connection
thisTransporter = modh2c.NewH2CRoundTripper()
}
return &ReverseProxy{
Director: director,
Prepender: prepender,

View File

@ -144,7 +144,7 @@ func (router *Router) StartProxyService() error {
}
//Validate basic auth
if sep.RequireBasicAuth {
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
err := handleBasicAuth(w, r, sep)
if err != nil {
return
@ -157,12 +157,18 @@ func (router *Router) StartProxyService() error {
router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
router.logRequest(r, false, 404, "vdir-http", r.Host)
}
endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
if sep.HeaderRewriteRules != nil {
endpointProxyRewriteRules = sep.HeaderRewriteRules
}
selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: selectedUpstream.OriginIpOrDomain,
OriginalHost: originalHostHeader,
UseTLS: selectedUpstream.RequireTLS,
HostHeaderOverwrite: sep.RequestHostOverwrite,
NoRemoveHopByHop: sep.DisableHopByHopHeaderRemoval,
HostHeaderOverwrite: endpointProxyRewriteRules.RequestHostOverwrite,
NoRemoveHopByHop: endpointProxyRewriteRules.DisableHopByHopHeaderRemoval,
PathPrefix: "",
Version: sep.parent.Option.HostVersion,
})
@ -185,7 +191,24 @@ func (router *Router) StartProxyService() error {
w.Write([]byte("400 - Bad Request"))
} else {
//No defined sub-domain
http.NotFound(w, r)
if router.Root.DefaultSiteOption == DefaultSite_NoResponse {
//No response. Just close the connection
hijacker, ok := w.(http.Hijacker)
if !ok {
w.Header().Set("Connection", "close")
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
w.Header().Set("Connection", "close")
return
}
conn.Close()
} else {
//Default behavior
http.NotFound(w, r)
}
}
}
@ -291,7 +314,7 @@ func (router *Router) Restart() error {
return err
}
time.Sleep(300 * time.Millisecond)
time.Sleep(100 * time.Millisecond)
// Start the server
err = router.StartProxyService()
if err != nil {
@ -331,7 +354,7 @@ func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
return true
}
if key == matchingDomain {
if key == strings.ToLower(matchingDomain) {
targetProxyEndpoint = v
}
return true

View File

@ -27,7 +27,12 @@ import (
// Check if a user define header exists in this endpoint, ignore case
func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
for _, header := range ep.UserDefinedHeaders {
endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
if ep.HeaderRewriteRules != nil {
endpointProxyRewriteRules = ep.HeaderRewriteRules
}
for _, header := range endpointProxyRewriteRules.UserDefinedHeaders {
if strings.EqualFold(header.Key, key) {
return true
}
@ -38,13 +43,16 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
// Remvoe a user defined header from the list
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
newHeaderList := []*rewrite.UserDefinedHeader{}
for _, header := range ep.UserDefinedHeaders {
if ep.HeaderRewriteRules == nil {
ep.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
for _, header := range ep.HeaderRewriteRules.UserDefinedHeaders {
if !strings.EqualFold(header.Key, key) {
newHeaderList = append(newHeaderList, header)
}
}
ep.UserDefinedHeaders = newHeaderList
ep.HeaderRewriteRules.UserDefinedHeaders = newHeaderList
return nil
}
@ -55,8 +63,11 @@ func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefined
ep.RemoveUserDefinedHeader(newHeaderRule.Key)
}
if ep.HeaderRewriteRules == nil {
ep.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
newHeaderRule.Key = cases.Title(language.Und, cases.NoLower).String(newHeaderRule.Key)
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, newHeaderRule)
ep.HeaderRewriteRules.UserDefinedHeaders = append(ep.HeaderRewriteRules.UserDefinedHeaders, newHeaderRule)
return nil
}
@ -106,7 +117,7 @@ func (ep *ProxyEndpoint) RemoveVirtualDirectoryRuleByMatchingPath(matchingPath s
return errors.New("target virtual directory routing rule not found")
}
// Delete a vdir rule by its matching path
// Add a vdir rule by its matching path
func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint) (*ProxyEndpoint, error) {
//Check for matching path duplicate
if ep.GetVirtualDirectoryRuleByMatchingPath(vdir.MatchingPath) != nil {
@ -123,9 +134,9 @@ func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint)
return nil, err
}
if ep.ProxyType == ProxyType_Root {
if ep.ProxyType == ProxyTypeRoot {
parentRouter.Root = readyRoutingRule
} else if ep.ProxyType == ProxyType_Host {
} else if ep.ProxyType == ProxyTypeHost {
ep.Remove()
parentRouter.AddProxyRouteToRuntime(readyRoutingRule)
} else {
@ -256,7 +267,8 @@ func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
// Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
lookupHostname := strings.ToLower(ep.RootOrMatchingDomain)
ep.parent.ProxyEndpoints.Delete(lookupHostname)
return nil
}
@ -264,5 +276,6 @@ func (ep *ProxyEndpoint) Remove() error {
// use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest
func (ep *ProxyEndpoint) UpdateToRuntime() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
lookupHostname := strings.ToLower(ep.RootOrMatchingDomain)
ep.parent.ProxyEndpoints.Store(lookupHostname, ep)
}

View File

@ -3,6 +3,7 @@ package loadbalance
import (
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/sessions"
@ -25,11 +26,12 @@ type Options struct {
}
type RouteManager struct {
SessionStore *sessions.CookieStore
LoadBalanceMap sync.Map //Sync map to store the last load balance state of a given node
OnlineStatusMap sync.Map //Sync map to store the online status of a given ip address or domain name
onlineStatusTickerStop chan bool //Stopping channel for the online status pinger
Options Options //Options for the load balancer
SessionStore *sessions.CookieStore
OnlineStatus sync.Map //Store the online status notify by uptime monitor
Options Options //Options for the load balancer
cacheTicker *time.Ticker //Ticker for cache cleanup
cacheTickerStop chan bool //Stop the cache cleanup
}
/* Upstream or Origin Server */
@ -41,8 +43,12 @@ type Upstream struct {
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
//Load balancing configs
Weight int //Random weight for round robin, 0 for fallback only
MaxConn int //TODO: Maxmium connection to this server, 0 for unlimited
Weight int //Random weight for round robin, 0 for fallback only
//HTTP Transport Config
MaxConn int //Maxmium concurrent requests to this upstream dpcore instance
RespTimeout int64 //Response header timeout in milliseconds
IdleTimeout int64 //Idle connection timeout in milliseconds
//currentConnectionCounts atomic.Uint64 //Counter for number of client currently connected
proxy *dpcore.ReverseProxy
@ -55,14 +61,31 @@ func NewLoadBalancer(options *Options) *RouteManager {
options.SystemUUID = uuid.New().String()
}
//Create a ticker for cache cleanup every 12 hours
cacheTicker := time.NewTicker(12 * time.Hour)
cacheTickerStop := make(chan bool)
go func() {
options.Logger.PrintAndLog("LoadBalancer", "Upstream state cache ticker started", nil)
for {
select {
case <-cacheTickerStop:
return
case <-cacheTicker.C:
//Clean up the cache
options.Logger.PrintAndLog("LoadBalancer", "Cleaning up upstream state cache", nil)
}
}
}()
//Generate a session store for stickySession
store := sessions.NewCookieStore([]byte(options.SystemUUID))
return &RouteManager{
SessionStore: store,
LoadBalanceMap: sync.Map{},
OnlineStatusMap: sync.Map{},
onlineStatusTickerStop: nil,
Options: *options,
SessionStore: store,
OnlineStatus: sync.Map{},
Options: *options,
cacheTicker: cacheTicker,
cacheTickerStop: cacheTickerStop,
}
}
@ -90,11 +113,20 @@ func GetUpstreamsAsString(upstreams []*Upstream) string {
return strings.Join(targets, ", ")
}
func (m *RouteManager) Close() {
if m.onlineStatusTickerStop != nil {
m.onlineStatusTickerStop <- true
}
// Reset the current session store and clear all previous sessions
func (m *RouteManager) ResetSessions() {
m.SessionStore = sessions.NewCookieStore([]byte(m.Options.SystemUUID))
}
func (m *RouteManager) Close() {
//Close the session store
m.SessionStore.MaxAge(0)
//Stop the cache cleanup
if m.cacheTicker != nil {
m.cacheTicker.Stop()
}
close(m.cacheTickerStop)
}
// Log Println, replace all log.Println or fmt.Println with this

View File

@ -1,39 +1,73 @@
package loadbalance
import (
"net/http"
"time"
"strconv"
"strings"
)
// Return the last ping status to see if the target is online
func (m *RouteManager) IsTargetOnline(matchingDomainOrIp string) bool {
value, ok := m.LoadBalanceMap.Load(matchingDomainOrIp)
// Return if the target host is online
func (m *RouteManager) IsTargetOnline(upstreamIP string) bool {
value, ok := m.OnlineStatus.Load(upstreamIP)
if !ok {
return false
// Assume online if not found, also update the map
m.OnlineStatus.Store(upstreamIP, true)
return true
}
isOnline, ok := value.(bool)
return ok && isOnline
}
// Ping a target to see if it is online
func PingTarget(targetMatchingDomainOrIp string, requireTLS bool) bool {
client := &http.Client{
Timeout: 10 * time.Second,
// Notify the host online state, should be called from uptime monitor
func (m *RouteManager) NotifyHostOnlineState(upstreamIP string, isOnline bool) {
//if the upstream IP contains http or https, strip it
upstreamIP = strings.TrimPrefix(upstreamIP, "http://")
upstreamIP = strings.TrimPrefix(upstreamIP, "https://")
//Check previous state and update
if m.IsTargetOnline(upstreamIP) == isOnline {
return
}
url := targetMatchingDomainOrIp
if requireTLS {
url = "https://" + url
} else {
url = "http://" + url
}
resp, err := client.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode <= 600
m.OnlineStatus.Store(upstreamIP, isOnline)
m.println("Updating upstream "+upstreamIP+" online state to "+strconv.FormatBool(isOnline), nil)
}
// Set this host unreachable for a given amount of time defined in timeout
// this shall be used in passive fallback. The uptime monitor should call to NotifyHostOnlineState() instead
/*
func (m *RouteManager) NotifyHostUnreachableWithTimeout(upstreamIp string, timeout int64) {
//if the upstream IP contains http or https, strip it
upstreamIp = strings.TrimPrefix(upstreamIp, "http://")
upstreamIp = strings.TrimPrefix(upstreamIp, "https://")
if timeout <= 0 {
//Set to the default timeout
timeout = 60
}
if !m.IsTargetOnline(upstreamIp) {
//Already offline
return
}
m.OnlineStatus.Store(upstreamIp, false)
m.println("Setting upstream "+upstreamIp+" unreachable for "+strconv.FormatInt(timeout, 10)+"s", nil)
go func() {
//Set the upstream back to online after the timeout
<-time.After(time.Duration(timeout) * time.Second)
m.NotifyHostOnlineState(upstreamIp, true)
}()
}
*/
// FilterOfflineOrigins return only online origins from a list of origins
func (m *RouteManager) FilterOfflineOrigins(origins []*Upstream) []*Upstream {
var onlineOrigins []*Upstream
for _, origin := range origins {
if m.IsTargetOnline(origin.OriginIpOrDomain) {
onlineOrigins = append(onlineOrigins, origin)
}
}
return onlineOrigins
}

View File

@ -13,49 +13,75 @@ import (
by this request.
*/
const (
STICKY_SESSION_NAME = "zr_sticky_session"
)
// GetRequestUpstreamTarget return the upstream target where this
// request should be routed
func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.Request, origins []*Upstream, useStickySession bool) (*Upstream, error) {
if len(origins) == 0 {
return nil, errors.New("no upstream is defined for this host")
}
var targetOrigin = origins[0]
//Pick the origin
if useStickySession {
//Use stick session, check which origins this request previously used
targetOriginId, err := m.getSessionHandler(r, origins)
if err != nil {
//No valid session found. Assign a new upstream
// No valid session found or origin is offline
// Filter the offline origins
origins = m.FilterOfflineOrigins(origins)
if len(origins) == 0 {
return nil, errors.New("no online upstream is available for origin: " + r.Host)
}
//Get a random origin
targetOrigin, index, err := getRandomUpstreamByWeight(origins)
if err != nil {
m.println("Unable to get random upstream", err)
targetOrigin = origins[0]
index = 0
}
//fmt.Println("DEBUG: (Sticky Session) Registering session origin " + origins[index].OriginIpOrDomain)
m.setSessionHandler(w, r, targetOrigin.OriginIpOrDomain, index)
return targetOrigin, nil
}
//Valid session found. Resume the previous session
//Valid session found and origin is online
//fmt.Println("DEBUG: (Sticky Session) Picking origin " + origins[targetOriginId].OriginIpOrDomain)
return origins[targetOriginId], nil
} else {
//Do not use stick session. Get a random one
var err error
targetOrigin, _, err = getRandomUpstreamByWeight(origins)
if err != nil {
m.println("Failed to get next origin", err)
targetOrigin = origins[0]
}
}
//No sticky session, get a random origin
//Filter the offline origins
origins = m.FilterOfflineOrigins(origins)
if len(origins) == 0 {
return nil, errors.New("no online upstream is available for origin: " + r.Host)
}
//Get a random origin
targetOrigin, _, err := getRandomUpstreamByWeight(origins)
if err != nil {
m.println("Failed to get next origin", err)
targetOrigin = origins[0]
}
//fmt.Println("DEBUG: Picking origin " + targetOrigin.OriginIpOrDomain)
return targetOrigin, nil
}
// GetUsableUpstreamCounts return the number of usable upstreams
func (m *RouteManager) GetUsableUpstreamCounts(origins []*Upstream) int {
origins = m.FilterOfflineOrigins(origins)
return len(origins)
}
/* Features related to session access */
//Set a new origin for this connection by session
func (m *RouteManager) setSessionHandler(w http.ResponseWriter, r *http.Request, originIpOrDomain string, index int) error {
session, err := m.SessionStore.Get(r, "STICKYSESSION")
session, err := m.SessionStore.Get(r, STICKY_SESSION_NAME)
if err != nil {
return err
}
@ -73,7 +99,7 @@ func (m *RouteManager) setSessionHandler(w http.ResponseWriter, r *http.Request,
// Get the previous connected origin from session
func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream) (int, error) {
// Get existing session
session, err := m.SessionStore.Get(r, "STICKYSESSION")
session, err := m.SessionStore.Get(r, STICKY_SESSION_NAME)
if err != nil {
return -1, err
}
@ -82,19 +108,26 @@ func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream)
originDomainRaw := session.Values["zr_sid_origin"]
originIDRaw := session.Values["zr_sid_index"]
if originDomainRaw == nil || originIDRaw == nil {
if originDomainRaw == nil || originIDRaw == nil || originIDRaw == -1 {
return -1, errors.New("no session has been set")
}
originDomain := originDomainRaw.(string)
originID := originIDRaw.(int)
//originID := originIDRaw.(int)
//Check if it has been modified
if len(upstreams) < originID || upstreams[originID].OriginIpOrDomain != originDomain {
//Mismatch or upstreams has been updated
return -1, errors.New("upstreams has been changed")
//Check if the upstream still exists
for i, upstream := range upstreams {
if upstream.OriginIpOrDomain == originDomain {
if !m.IsTargetOnline(originDomain) {
//Origin is offline
return -1, errors.New("origin is offline")
}
//Ok, the origin is still online
return i, nil
}
}
return originID, nil
return -1, errors.New("origin is no longer exists")
}
/* Functions related to random upstream picking */
@ -157,21 +190,3 @@ func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) {
return nil, -1, errors.New("failed to pick an upstream origin server")
}
// IntRange returns a random integer in the range from min to max.
/*
func intRange(min, max int) (int, error) {
var result int
switch {
case min > max:
// Fail with error
return result, errors.New("min is greater than max")
case max == min:
result = max
case max > min:
b := rand.Intn(max-min) + min
result = min + int(b)
}
return result, nil
}
*/

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