mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-30 19:31:46 +02:00
Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
0e749e8a41 | |||
2c219eceef | |||
92a27cbeb8 | |||
b8a47dc620 | |||
c4266559be | |||
136989f2ea | |||
3e031605fc | |||
eb265e3e94 | |||
8504ff16cb | |||
b71437058f | |||
4d16758e0a | |||
f2b4c47805 | |||
7dff4f83b4 | |||
eb24bc0391 | |||
dac3e8c925 | |||
3f1c1f1395 | |||
cd15fdf3c1 | |||
0fdfda436b | |||
f8270e46c2 | |||
4a99afa2f0 | |||
dfd5ef5578 | |||
3e57a90bb6 | |||
23d4df1ed7 | |||
39d6d16c2a | |||
b7e3888513 | |||
fd41a1cb91 | |||
75c351e7e2 | |||
549e492ffd | |||
6351f25c00 |
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@ -24,6 +24,10 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Pull last image for layer reuse
|
||||
run: |
|
||||
docker pull docker.io/zoraxydocker/zoraxy:latest
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -49,3 +49,4 @@ src/log/
|
||||
# plugins
|
||||
example/plugins/ztnc/ztnc.db
|
||||
example/plugins/ztnc/authtoken.secret
|
||||
example/plugins/ztnc/ztnc.db.lock
|
||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,3 +1,14 @@
|
||||
# v3.1.9 1 Mar 2025
|
||||
|
||||
+ Fixed netstat underflow bug
|
||||
+ Fixed origin picker cookie bug [#550](https://github.com/tobychui/zoraxy/issues/550)
|
||||
+ Added prototype plugin system
|
||||
+ Added plugin examples
|
||||
+ Added notice for build-in Zerotier network controller deprecation (and will be moved to plugins)
|
||||
+ Added country code display for quickban list [#247](https://github.com/tobychui/zoraxy/issues/247)
|
||||
+ Removed passive load balancer and default to active lb only [#554](https://github.com/tobychui/zoraxy/issues/554)
|
||||
|
||||
|
||||
# v3.1.8 16 Feb 2025
|
||||
|
||||
+ Exposed timeout value from dpcore to UI
|
||||
|
@ -1,4 +1,5 @@
|
||||
FROM docker.io/golang:alpine AS build-zoraxy
|
||||
## Build Zoraxy
|
||||
FROM docker.io/golang:bookworm AS build-zoraxy
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
@ -12,7 +13,9 @@ RUN go mod tidy &&\
|
||||
go build -o /usr/local/bin/zoraxy &&\
|
||||
chmod 755 /usr/local/bin/zoraxy
|
||||
|
||||
FROM docker.io/ubuntu:latest AS build-zerotier
|
||||
|
||||
## Build ZeroTier
|
||||
FROM docker.io/golang:bookworm AS build-zerotier
|
||||
|
||||
RUN mkdir -p /opt/zerotier/source/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
@ -29,14 +32,20 @@ RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne
|
||||
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
|
||||
chmod 755 /usr/local/bin/zerotier-one
|
||||
|
||||
FROM docker.io/ubuntu:latest
|
||||
|
||||
FROM docker.io/golang:bookworm
|
||||
|
||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||
COPY --chmod=700 ./build_plugins.sh /usr/local/bin/build_plugins
|
||||
COPY --chmod=700 ./example/plugins/ztnc/mod/zoraxy_plugin/ /opt/zoraxy/zoraxy_plugin/
|
||||
|
||||
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
||||
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||
|
||||
RUN apt-get update -y &&\
|
||||
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates openssh-server
|
||||
|
||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
||||
RUN mkdir -p /opt/zoraxy/plugin/
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
|
||||
@ -51,14 +60,13 @@ ENV FASTGEOIP="false"
|
||||
ENV MDNS="true"
|
||||
ENV MDNSNAME="''"
|
||||
ENV NOAUTH="false"
|
||||
ENV PLUGIN="/opt/zoraxy/plugin/"
|
||||
ENV PORT="8000"
|
||||
ENV SSHLB="false"
|
||||
ENV UPDATE_GEOIP="false"
|
||||
ENV VERSION="false"
|
||||
ENV WEBFM="true"
|
||||
ENV WEBROOT="./www"
|
||||
ENV ZTAUTH=""
|
||||
ENV ZTPORT="9993"
|
||||
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
|
@ -23,6 +23,7 @@ docker run -d \
|
||||
-p 443:443 \
|
||||
-p 8000:8000 \
|
||||
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
|
||||
-v /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/ \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /etc/localtime:/etc/localtime \
|
||||
-e FASTGEOIP="true" \
|
||||
@ -43,6 +44,7 @@ services:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- /path/to/zoraxy/config/:/opt/zoraxy/config/
|
||||
- /path/to/zoraxy/plugin/:/opt/zoraxy/plugin/
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/localtime:/etc/localtime
|
||||
environment:
|
||||
@ -62,6 +64,7 @@ services:
|
||||
| Volume | Details |
|
||||
|:-|:-|
|
||||
| `/opt/zoraxy/config/` | Zoraxy configuration. |
|
||||
| `/opt/zoraxy/plugin/` | Zoraxy plugins. |
|
||||
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
|
||||
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
|
||||
|
||||
@ -80,6 +83,7 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
|
||||
| `MDNS` | `true` (Boolean) | Enable mDNS scanner and transponder. |
|
||||
| `MDNSNAME` | `''` (String) | mDNS name, leave empty to use default (zoraxy_{node-uuid}.local). |
|
||||
| `NOAUTH` | `false` (Boolean) | Disable authentication for management interface. |
|
||||
| `PLUGIN` | `/opt/zoraxy/plugin/` (String) | Set the path for Zoraxy plugins. Only change this if you know what you are doing. |
|
||||
| `PORT` | `8000` (Integer) | Management web interface listening port |
|
||||
| `SSHLB` | `false` (Boolean) | Allow loopback web ssh connection (DANGER). |
|
||||
| `UPDATE_GEOIP` | `false` (Boolean) | Download the latest GeoIP data and exit. |
|
||||
@ -87,17 +91,24 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
|
||||
| `WEBFM` | `true` (Boolean) | Enable web file manager for static web server root folder. |
|
||||
| `WEBROOT` | `./www` (String) | Static web server root folder. Only allow change in start parameters. |
|
||||
| `ZEROTIER` | `false` (Boolean) | Enable ZeroTier functionality for GAN. |
|
||||
| `ZTAUTH` | `""` (String) | ZeroTier authtoken for the local node. |
|
||||
| `ZTPORT` | `9993` (Integer) | ZeroTier controller API port. |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Contrary to the Zoraxy README, Docker usage of the port flag should NOT include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
|
||||
|
||||
### Plugins
|
||||
|
||||
You can find official plugins at https://github.com/aroz-online/zoraxy-official-plugins
|
||||
|
||||
Place your plugins inside the volume `/path/to/zoraxy/plugin/:/opt/zoraxy/plugin/` (Adjust to your actual install location). Any plugins you have added will then be built and used on the next restart.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Plugins are currently experimental.
|
||||
|
||||
### Building
|
||||
|
||||
To build the Docker image:
|
||||
- Check out the repository/branch.
|
||||
- Copy the Zoraxy `src/` directory into the `docker/` (here) directory.
|
||||
- Copy the Zoraxy `src/` and `example/` directory into the `docker/` (here) directory.
|
||||
- Run the build command with `docker build -t zoraxy_build .`
|
||||
- You can now use the image `zoraxy_build`
|
||||
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.
|
||||
|
19
docker/build_plugins.sh
Normal file
19
docker/build_plugins.sh
Normal file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Copying zoraxy_plugin to all mods..."
|
||||
for dir in "$1"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
cp -r "/opt/zoraxy/zoraxy_plugin/" "$dir/mod/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Running go mod tidy and go build for all directories..."
|
||||
for dir in "$1"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
cd "$dir" || exit 1
|
||||
go mod tidy
|
||||
go build
|
||||
cd "$1" || exit 1
|
||||
fi
|
||||
done
|
||||
|
@ -1,19 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
trap cleanup TERM INT
|
||||
|
||||
cleanup() {
|
||||
echo "Shutting down..."
|
||||
echo "Stop signal received. Shutting down..."
|
||||
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
|
||||
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
|
||||
unlink /var/lib/zerotier-one/zerotier/
|
||||
exit 0
|
||||
}
|
||||
|
||||
update-ca-certificates
|
||||
echo "CA certificates updated."
|
||||
trap cleanup SIGTERM SIGINT TERM INT
|
||||
|
||||
zoraxy -update_geoip=true
|
||||
echo "Updated GeoIP data."
|
||||
update-ca-certificates && echo "CA certificates updated."
|
||||
zoraxy -update_geoip=true && echo "GeoIP data updated ."
|
||||
|
||||
echo "Building plugins..."
|
||||
cd /opt/zoraxy/plugin/ || exit 1
|
||||
build_plugins "$PWD"
|
||||
echo "Plugins built."
|
||||
cd /opt/zoraxy/config/ || exit 1
|
||||
|
||||
if [ "$ZEROTIER" = "true" ]; then
|
||||
if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then
|
||||
@ -36,17 +40,16 @@ zoraxy \
|
||||
-mdns="$MDNS" \
|
||||
-mdnsname="$MDNSNAME" \
|
||||
-noauth="$NOAUTH" \
|
||||
-plugin="$PLUGIN" \
|
||||
-port=:"$PORT" \
|
||||
-sshlb="$SSHLB" \
|
||||
-update_geoip="$UPDATE_GEOIP" \
|
||||
-version="$VERSION" \
|
||||
-webfm="$WEBFM" \
|
||||
-webroot="$WEBROOT" \
|
||||
-ztauth="$ZTAUTH" \
|
||||
-ztport="$ZTPORT" \
|
||||
&
|
||||
|
||||
zoraxypid=$!
|
||||
wait $zoraxypid
|
||||
wait $zerotierpid
|
||||
wait "$zoraxypid"
|
||||
wait "$zerotierpid"
|
||||
|
||||
|
451
docs/GNU Free Documentation License.txt
Normal file
451
docs/GNU Free Documentation License.txt
Normal file
@ -0,0 +1,451 @@
|
||||
|
||||
GNU Free Documentation License
|
||||
Version 1.3, 3 November 2008
|
||||
|
||||
|
||||
Copyright (C) 2000, 2001, 2002, 2007, 2008 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
0. PREAMBLE
|
||||
|
||||
The purpose of this License is to make a manual, textbook, or other
|
||||
functional and useful document "free" in the sense of freedom: to
|
||||
assure everyone the effective freedom to copy and redistribute it,
|
||||
with or without modifying it, either commercially or noncommercially.
|
||||
Secondarily, this License preserves for the author and publisher a way
|
||||
to get credit for their work, while not being considered responsible
|
||||
for modifications made by others.
|
||||
|
||||
This License is a kind of "copyleft", which means that derivative
|
||||
works of the document must themselves be free in the same sense. It
|
||||
complements the GNU General Public License, which is a copyleft
|
||||
license designed for free software.
|
||||
|
||||
We have designed this License in order to use it for manuals for free
|
||||
software, because free software needs free documentation: a free
|
||||
program should come with manuals providing the same freedoms that the
|
||||
software does. But this License is not limited to software manuals;
|
||||
it can be used for any textual work, regardless of subject matter or
|
||||
whether it is published as a printed book. We recommend this License
|
||||
principally for works whose purpose is instruction or reference.
|
||||
|
||||
|
||||
1. APPLICABILITY AND DEFINITIONS
|
||||
|
||||
This License applies to any manual or other work, in any medium, that
|
||||
contains a notice placed by the copyright holder saying it can be
|
||||
distributed under the terms of this License. Such a notice grants a
|
||||
world-wide, royalty-free license, unlimited in duration, to use that
|
||||
work under the conditions stated herein. The "Document", below,
|
||||
refers to any such manual or work. Any member of the public is a
|
||||
licensee, and is addressed as "you". You accept the license if you
|
||||
copy, modify or distribute the work in a way requiring permission
|
||||
under copyright law.
|
||||
|
||||
A "Modified Version" of the Document means any work containing the
|
||||
Document or a portion of it, either copied verbatim, or with
|
||||
modifications and/or translated into another language.
|
||||
|
||||
A "Secondary Section" is a named appendix or a front-matter section of
|
||||
the Document that deals exclusively with the relationship of the
|
||||
publishers or authors of the Document to the Document's overall
|
||||
subject (or to related matters) and contains nothing that could fall
|
||||
directly within that overall subject. (Thus, if the Document is in
|
||||
part a textbook of mathematics, a Secondary Section may not explain
|
||||
any mathematics.) The relationship could be a matter of historical
|
||||
connection with the subject or with related matters, or of legal,
|
||||
commercial, philosophical, ethical or political position regarding
|
||||
them.
|
||||
|
||||
The "Invariant Sections" are certain Secondary Sections whose titles
|
||||
are designated, as being those of Invariant Sections, in the notice
|
||||
that says that the Document is released under this License. If a
|
||||
section does not fit the above definition of Secondary then it is not
|
||||
allowed to be designated as Invariant. The Document may contain zero
|
||||
Invariant Sections. If the Document does not identify any Invariant
|
||||
Sections then there are none.
|
||||
|
||||
The "Cover Texts" are certain short passages of text that are listed,
|
||||
as Front-Cover Texts or Back-Cover Texts, in the notice that says that
|
||||
the Document is released under this License. A Front-Cover Text may
|
||||
be at most 5 words, and a Back-Cover Text may be at most 25 words.
|
||||
|
||||
A "Transparent" copy of the Document means a machine-readable copy,
|
||||
represented in a format whose specification is available to the
|
||||
general public, that is suitable for revising the document
|
||||
straightforwardly with generic text editors or (for images composed of
|
||||
pixels) generic paint programs or (for drawings) some widely available
|
||||
drawing editor, and that is suitable for input to text formatters or
|
||||
for automatic translation to a variety of formats suitable for input
|
||||
to text formatters. A copy made in an otherwise Transparent file
|
||||
format whose markup, or absence of markup, has been arranged to thwart
|
||||
or discourage subsequent modification by readers is not Transparent.
|
||||
An image format is not Transparent if used for any substantial amount
|
||||
of text. A copy that is not "Transparent" is called "Opaque".
|
||||
|
||||
Examples of suitable formats for Transparent copies include plain
|
||||
ASCII without markup, Texinfo input format, LaTeX input format, SGML
|
||||
or XML using a publicly available DTD, and standard-conforming simple
|
||||
HTML, PostScript or PDF designed for human modification. Examples of
|
||||
transparent image formats include PNG, XCF and JPG. Opaque formats
|
||||
include proprietary formats that can be read and edited only by
|
||||
proprietary word processors, SGML or XML for which the DTD and/or
|
||||
processing tools are not generally available, and the
|
||||
machine-generated HTML, PostScript or PDF produced by some word
|
||||
processors for output purposes only.
|
||||
|
||||
The "Title Page" means, for a printed book, the title page itself,
|
||||
plus such following pages as are needed to hold, legibly, the material
|
||||
this License requires to appear in the title page. For works in
|
||||
formats which do not have any title page as such, "Title Page" means
|
||||
the text near the most prominent appearance of the work's title,
|
||||
preceding the beginning of the body of the text.
|
||||
|
||||
The "publisher" means any person or entity that distributes copies of
|
||||
the Document to the public.
|
||||
|
||||
A section "Entitled XYZ" means a named subunit of the Document whose
|
||||
title either is precisely XYZ or contains XYZ in parentheses following
|
||||
text that translates XYZ in another language. (Here XYZ stands for a
|
||||
specific section name mentioned below, such as "Acknowledgements",
|
||||
"Dedications", "Endorsements", or "History".) To "Preserve the Title"
|
||||
of such a section when you modify the Document means that it remains a
|
||||
section "Entitled XYZ" according to this definition.
|
||||
|
||||
The Document may include Warranty Disclaimers next to the notice which
|
||||
states that this License applies to the Document. These Warranty
|
||||
Disclaimers are considered to be included by reference in this
|
||||
License, but only as regards disclaiming warranties: any other
|
||||
implication that these Warranty Disclaimers may have is void and has
|
||||
no effect on the meaning of this License.
|
||||
|
||||
2. VERBATIM COPYING
|
||||
|
||||
You may copy and distribute the Document in any medium, either
|
||||
commercially or noncommercially, provided that this License, the
|
||||
copyright notices, and the license notice saying this License applies
|
||||
to the Document are reproduced in all copies, and that you add no
|
||||
other conditions whatsoever to those of this License. You may not use
|
||||
technical measures to obstruct or control the reading or further
|
||||
copying of the copies you make or distribute. However, you may accept
|
||||
compensation in exchange for copies. If you distribute a large enough
|
||||
number of copies you must also follow the conditions in section 3.
|
||||
|
||||
You may also lend copies, under the same conditions stated above, and
|
||||
you may publicly display copies.
|
||||
|
||||
|
||||
3. COPYING IN QUANTITY
|
||||
|
||||
If you publish printed copies (or copies in media that commonly have
|
||||
printed covers) of the Document, numbering more than 100, and the
|
||||
Document's license notice requires Cover Texts, you must enclose the
|
||||
copies in covers that carry, clearly and legibly, all these Cover
|
||||
Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
|
||||
the back cover. Both covers must also clearly and legibly identify
|
||||
you as the publisher of these copies. The front cover must present
|
||||
the full title with all words of the title equally prominent and
|
||||
visible. You may add other material on the covers in addition.
|
||||
Copying with changes limited to the covers, as long as they preserve
|
||||
the title of the Document and satisfy these conditions, can be treated
|
||||
as verbatim copying in other respects.
|
||||
|
||||
If the required texts for either cover are too voluminous to fit
|
||||
legibly, you should put the first ones listed (as many as fit
|
||||
reasonably) on the actual cover, and continue the rest onto adjacent
|
||||
pages.
|
||||
|
||||
If you publish or distribute Opaque copies of the Document numbering
|
||||
more than 100, you must either include a machine-readable Transparent
|
||||
copy along with each Opaque copy, or state in or with each Opaque copy
|
||||
a computer-network location from which the general network-using
|
||||
public has access to download using public-standard network protocols
|
||||
a complete Transparent copy of the Document, free of added material.
|
||||
If you use the latter option, you must take reasonably prudent steps,
|
||||
when you begin distribution of Opaque copies in quantity, to ensure
|
||||
that this Transparent copy will remain thus accessible at the stated
|
||||
location until at least one year after the last time you distribute an
|
||||
Opaque copy (directly or through your agents or retailers) of that
|
||||
edition to the public.
|
||||
|
||||
It is requested, but not required, that you contact the authors of the
|
||||
Document well before redistributing any large number of copies, to
|
||||
give them a chance to provide you with an updated version of the
|
||||
Document.
|
||||
|
||||
|
||||
4. MODIFICATIONS
|
||||
|
||||
You may copy and distribute a Modified Version of the Document under
|
||||
the conditions of sections 2 and 3 above, provided that you release
|
||||
the Modified Version under precisely this License, with the Modified
|
||||
Version filling the role of the Document, thus licensing distribution
|
||||
and modification of the Modified Version to whoever possesses a copy
|
||||
of it. In addition, you must do these things in the Modified Version:
|
||||
|
||||
A. Use in the Title Page (and on the covers, if any) a title distinct
|
||||
from that of the Document, and from those of previous versions
|
||||
(which should, if there were any, be listed in the History section
|
||||
of the Document). You may use the same title as a previous version
|
||||
if the original publisher of that version gives permission.
|
||||
B. List on the Title Page, as authors, one or more persons or entities
|
||||
responsible for authorship of the modifications in the Modified
|
||||
Version, together with at least five of the principal authors of the
|
||||
Document (all of its principal authors, if it has fewer than five),
|
||||
unless they release you from this requirement.
|
||||
C. State on the Title page the name of the publisher of the
|
||||
Modified Version, as the publisher.
|
||||
D. Preserve all the copyright notices of the Document.
|
||||
E. Add an appropriate copyright notice for your modifications
|
||||
adjacent to the other copyright notices.
|
||||
F. Include, immediately after the copyright notices, a license notice
|
||||
giving the public permission to use the Modified Version under the
|
||||
terms of this License, in the form shown in the Addendum below.
|
||||
G. Preserve in that license notice the full lists of Invariant Sections
|
||||
and required Cover Texts given in the Document's license notice.
|
||||
H. Include an unaltered copy of this License.
|
||||
I. Preserve the section Entitled "History", Preserve its Title, and add
|
||||
to it an item stating at least the title, year, new authors, and
|
||||
publisher of the Modified Version as given on the Title Page. If
|
||||
there is no section Entitled "History" in the Document, create one
|
||||
stating the title, year, authors, and publisher of the Document as
|
||||
given on its Title Page, then add an item describing the Modified
|
||||
Version as stated in the previous sentence.
|
||||
J. Preserve the network location, if any, given in the Document for
|
||||
public access to a Transparent copy of the Document, and likewise
|
||||
the network locations given in the Document for previous versions
|
||||
it was based on. These may be placed in the "History" section.
|
||||
You may omit a network location for a work that was published at
|
||||
least four years before the Document itself, or if the original
|
||||
publisher of the version it refers to gives permission.
|
||||
K. For any section Entitled "Acknowledgements" or "Dedications",
|
||||
Preserve the Title of the section, and preserve in the section all
|
||||
the substance and tone of each of the contributor acknowledgements
|
||||
and/or dedications given therein.
|
||||
L. Preserve all the Invariant Sections of the Document,
|
||||
unaltered in their text and in their titles. Section numbers
|
||||
or the equivalent are not considered part of the section titles.
|
||||
M. Delete any section Entitled "Endorsements". Such a section
|
||||
may not be included in the Modified Version.
|
||||
N. Do not retitle any existing section to be Entitled "Endorsements"
|
||||
or to conflict in title with any Invariant Section.
|
||||
O. Preserve any Warranty Disclaimers.
|
||||
|
||||
If the Modified Version includes new front-matter sections or
|
||||
appendices that qualify as Secondary Sections and contain no material
|
||||
copied from the Document, you may at your option designate some or all
|
||||
of these sections as invariant. To do this, add their titles to the
|
||||
list of Invariant Sections in the Modified Version's license notice.
|
||||
These titles must be distinct from any other section titles.
|
||||
|
||||
You may add a section Entitled "Endorsements", provided it contains
|
||||
nothing but endorsements of your Modified Version by various
|
||||
parties--for example, statements of peer review or that the text has
|
||||
been approved by an organization as the authoritative definition of a
|
||||
standard.
|
||||
|
||||
You may add a passage of up to five words as a Front-Cover Text, and a
|
||||
passage of up to 25 words as a Back-Cover Text, to the end of the list
|
||||
of Cover Texts in the Modified Version. Only one passage of
|
||||
Front-Cover Text and one of Back-Cover Text may be added by (or
|
||||
through arrangements made by) any one entity. If the Document already
|
||||
includes a cover text for the same cover, previously added by you or
|
||||
by arrangement made by the same entity you are acting on behalf of,
|
||||
you may not add another; but you may replace the old one, on explicit
|
||||
permission from the previous publisher that added the old one.
|
||||
|
||||
The author(s) and publisher(s) of the Document do not by this License
|
||||
give permission to use their names for publicity for or to assert or
|
||||
imply endorsement of any Modified Version.
|
||||
|
||||
|
||||
5. COMBINING DOCUMENTS
|
||||
|
||||
You may combine the Document with other documents released under this
|
||||
License, under the terms defined in section 4 above for modified
|
||||
versions, provided that you include in the combination all of the
|
||||
Invariant Sections of all of the original documents, unmodified, and
|
||||
list them all as Invariant Sections of your combined work in its
|
||||
license notice, and that you preserve all their Warranty Disclaimers.
|
||||
|
||||
The combined work need only contain one copy of this License, and
|
||||
multiple identical Invariant Sections may be replaced with a single
|
||||
copy. If there are multiple Invariant Sections with the same name but
|
||||
different contents, make the title of each such section unique by
|
||||
adding at the end of it, in parentheses, the name of the original
|
||||
author or publisher of that section if known, or else a unique number.
|
||||
Make the same adjustment to the section titles in the list of
|
||||
Invariant Sections in the license notice of the combined work.
|
||||
|
||||
In the combination, you must combine any sections Entitled "History"
|
||||
in the various original documents, forming one section Entitled
|
||||
"History"; likewise combine any sections Entitled "Acknowledgements",
|
||||
and any sections Entitled "Dedications". You must delete all sections
|
||||
Entitled "Endorsements".
|
||||
|
||||
|
||||
6. COLLECTIONS OF DOCUMENTS
|
||||
|
||||
You may make a collection consisting of the Document and other
|
||||
documents released under this License, and replace the individual
|
||||
copies of this License in the various documents with a single copy
|
||||
that is included in the collection, provided that you follow the rules
|
||||
of this License for verbatim copying of each of the documents in all
|
||||
other respects.
|
||||
|
||||
You may extract a single document from such a collection, and
|
||||
distribute it individually under this License, provided you insert a
|
||||
copy of this License into the extracted document, and follow this
|
||||
License in all other respects regarding verbatim copying of that
|
||||
document.
|
||||
|
||||
|
||||
7. AGGREGATION WITH INDEPENDENT WORKS
|
||||
|
||||
A compilation of the Document or its derivatives with other separate
|
||||
and independent documents or works, in or on a volume of a storage or
|
||||
distribution medium, is called an "aggregate" if the copyright
|
||||
resulting from the compilation is not used to limit the legal rights
|
||||
of the compilation's users beyond what the individual works permit.
|
||||
When the Document is included in an aggregate, this License does not
|
||||
apply to the other works in the aggregate which are not themselves
|
||||
derivative works of the Document.
|
||||
|
||||
If the Cover Text requirement of section 3 is applicable to these
|
||||
copies of the Document, then if the Document is less than one half of
|
||||
the entire aggregate, the Document's Cover Texts may be placed on
|
||||
covers that bracket the Document within the aggregate, or the
|
||||
electronic equivalent of covers if the Document is in electronic form.
|
||||
Otherwise they must appear on printed covers that bracket the whole
|
||||
aggregate.
|
||||
|
||||
|
||||
8. TRANSLATION
|
||||
|
||||
Translation is considered a kind of modification, so you may
|
||||
distribute translations of the Document under the terms of section 4.
|
||||
Replacing Invariant Sections with translations requires special
|
||||
permission from their copyright holders, but you may include
|
||||
translations of some or all Invariant Sections in addition to the
|
||||
original versions of these Invariant Sections. You may include a
|
||||
translation of this License, and all the license notices in the
|
||||
Document, and any Warranty Disclaimers, provided that you also include
|
||||
the original English version of this License and the original versions
|
||||
of those notices and disclaimers. In case of a disagreement between
|
||||
the translation and the original version of this License or a notice
|
||||
or disclaimer, the original version will prevail.
|
||||
|
||||
If a section in the Document is Entitled "Acknowledgements",
|
||||
"Dedications", or "History", the requirement (section 4) to Preserve
|
||||
its Title (section 1) will typically require changing the actual
|
||||
title.
|
||||
|
||||
|
||||
9. TERMINATION
|
||||
|
||||
You may not copy, modify, sublicense, or distribute the Document
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense, or distribute it is void, and
|
||||
will automatically terminate your rights under this License.
|
||||
|
||||
However, if you cease all violation of this License, then your license
|
||||
from a particular copyright holder is reinstated (a) provisionally,
|
||||
unless and until the copyright holder explicitly and finally
|
||||
terminates your license, and (b) permanently, if the copyright holder
|
||||
fails to notify you of the violation by some reasonable means prior to
|
||||
60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, receipt of a copy of some or all of the same material does
|
||||
not give you any rights to use it.
|
||||
|
||||
|
||||
10. FUTURE REVISIONS OF THIS LICENSE
|
||||
|
||||
The Free Software Foundation may publish new, revised versions of the
|
||||
GNU Free Documentation License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in
|
||||
detail to address new problems or concerns. See
|
||||
https://www.gnu.org/licenses/.
|
||||
|
||||
Each version of the License is given a distinguishing version number.
|
||||
If the Document specifies that a particular numbered version of this
|
||||
License "or any later version" applies to it, you have the option of
|
||||
following the terms and conditions either of that specified version or
|
||||
of any later version that has been published (not as a draft) by the
|
||||
Free Software Foundation. If the Document does not specify a version
|
||||
number of this License, you may choose any version ever published (not
|
||||
as a draft) by the Free Software Foundation. If the Document
|
||||
specifies that a proxy can decide which future versions of this
|
||||
License can be used, that proxy's public statement of acceptance of a
|
||||
version permanently authorizes you to choose that version for the
|
||||
Document.
|
||||
|
||||
11. RELICENSING
|
||||
|
||||
"Massive Multiauthor Collaboration Site" (or "MMC Site") means any
|
||||
World Wide Web server that publishes copyrightable works and also
|
||||
provides prominent facilities for anybody to edit those works. A
|
||||
public wiki that anybody can edit is an example of such a server. A
|
||||
"Massive Multiauthor Collaboration" (or "MMC") contained in the site
|
||||
means any set of copyrightable works thus published on the MMC site.
|
||||
|
||||
"CC-BY-SA" means the Creative Commons Attribution-Share Alike 3.0
|
||||
license published by Creative Commons Corporation, a not-for-profit
|
||||
corporation with a principal place of business in San Francisco,
|
||||
California, as well as future copyleft versions of that license
|
||||
published by that same organization.
|
||||
|
||||
"Incorporate" means to publish or republish a Document, in whole or in
|
||||
part, as part of another Document.
|
||||
|
||||
An MMC is "eligible for relicensing" if it is licensed under this
|
||||
License, and if all works that were first published under this License
|
||||
somewhere other than this MMC, and subsequently incorporated in whole or
|
||||
in part into the MMC, (1) had no cover texts or invariant sections, and
|
||||
(2) were thus incorporated prior to November 1, 2008.
|
||||
|
||||
The operator of an MMC Site may republish an MMC contained in the site
|
||||
under CC-BY-SA on the same site at any time before August 1, 2009,
|
||||
provided the MMC is eligible for relicensing.
|
||||
|
||||
|
||||
ADDENDUM: How to use this License for your documents
|
||||
|
||||
To use this License in a document you have written, include a copy of
|
||||
the License in the document and put the following copyright and
|
||||
license notices just after the title page:
|
||||
|
||||
Copyright (c) YEAR YOUR NAME.
|
||||
Permission is granted to copy, distribute and/or modify this document
|
||||
under the terms of the GNU Free Documentation License, Version 1.3
|
||||
or any later version published by the Free Software Foundation;
|
||||
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
|
||||
A copy of the license is included in the section entitled "GNU
|
||||
Free Documentation License".
|
||||
|
||||
If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
|
||||
replace the "with...Texts." line with this:
|
||||
|
||||
with the Invariant Sections being LIST THEIR TITLES, with the
|
||||
Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
|
||||
|
||||
If you have Invariant Sections without Cover Texts, or some other
|
||||
combination of the three, merge those two alternatives to suit the
|
||||
situation.
|
||||
|
||||
If your document contains nontrivial examples of program code, we
|
||||
recommend releasing these examples in parallel under your choice of
|
||||
free software license, such as the GNU General Public License,
|
||||
to permit their use in free software.
|
1
docs/dom-i18n.min.js
vendored
Normal file
1
docs/dom-i18n.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(a,b){"use strict";"function"==typeof define&&define.amd?define([],function(){return a.domI18n=b()}):"object"==typeof exports?module.exports=b():a.domI18n=b()}(this,function(){"use strict";return function(a){function b(a){return a||(a=window.navigator.languages?window.navigator.languages[0]:window.navigator.language||window.navigator.userLanguage),-1===q.indexOf(a)&&(r&&console.warn(a+" is not available on the list of languages provided"),a=a.indexOf("-")?a.split("-")[0]:a),-1===q.indexOf(a)&&(r&&console.error(a+" is not compatible with any language provided"),a=p),a}function c(a){v=b(a),l()}function d(){u={}}function e(a){var b=a.getAttribute("data-dom-i18n-id");return b&&u&&u[b]}function f(a,b){var c="i18n"+Date.now()+1e3*Math.random();a.setAttribute("data-dom-i18n-id",c),u[c]=b}function g(a){return u&&u[a.getAttribute("data-dom-i18n-id")]}function h(a,b){var c={},d=a.firstElementChild,e=!d&&a[b].split(o);return q.forEach(function(b,f){var g;d?(g=a.children[f],g&&g.cloneNode&&(c[b]=g.cloneNode(!0))):(g=e[f],g&&(c[b]=String(g)))}),c}function i(a){var b,c,d=a.getAttribute(t),i=null!==a.getAttribute(s),k=d?d:"textContent";!i&&e(a)?b=g(a):(b=h(a,k),i||f(a,b)),c=b[v],"string"==typeof c?a[k]=c:"object"==typeof c&&j(a,c)}function j(a,b){k(a),a.appendChild(b)}function k(a){for(;a.lastChild;)a.removeChild(a.lastChild)}function l(){for(var a="string"==typeof n||n instanceof String?m.querySelectorAll(n):n,b=0;b<a.length;++b)i(a[b])}a=a||{};var m=a.rootElement||window.document,n=a.selector||"[data-translatable]",o=a.separator||" // ",p=a.defaultLanguage||"en",q=a.languages||["en"],r=void 0!==a.enableLog?a.enableLog:!0,s="data-no-cache",t="data-translatable-attr",u={},v=b(a.currentLanguage);return l(n),{changeLanguage:c,clearCachedElements:d}}});
|
BIN
docs/img/logo_white.png
Normal file
BIN
docs/img/logo_white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
BIN
docs/img/preview-mobile.png
Normal file
BIN
docs/img/preview-mobile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 140 KiB |
BIN
docs/img/preview-mobile.psd
Normal file
BIN
docs/img/preview-mobile.psd
Normal file
Binary file not shown.
BIN
docs/img/preview-pc.png
Normal file
BIN
docs/img/preview-pc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 194 KiB |
874
docs/index.html
874
docs/index.html
@ -1,128 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="Reverse Proxy, Cluster, Gateway, Go, Homelab, Network Tools" name="keywords">
|
||||
<meta content="A reverse proxy server and cluster network gateway for noobs" name="description">
|
||||
<meta content="Reverse Proxy, Open Source, Aroz, Go, OS, NAS, Cloud" name="keywords">
|
||||
<meta content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox" name="description">
|
||||
<meta name="author" content="tobychui">
|
||||
|
||||
<!-- HTML Meta Tags -->
|
||||
<title>Reverse Proxy Server | Zoraxy</title>
|
||||
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<title>Homelab Gateway | Zoraxy</title>
|
||||
<meta name="description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://zoraxy.aroz.org/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta property="og:title" content="Hello Zoraxy">
|
||||
<meta property="og:description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
|
||||
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="aroz.org">
|
||||
<meta property="twitter:domain" content="os.aroz.org">
|
||||
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
|
||||
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta name="twitter:title" content="Hello Zoraxy">
|
||||
<meta name="twitter:description" content="Simplify your self-hosted services with Zoraxy, the ultimate homelab networking toolbox">
|
||||
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- JavaScript Libs-->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<script src="dom-i18n.min.js"></script>
|
||||
<link href="main.css" rel="stylesheet">
|
||||
<script src="main.js" defer></script>
|
||||
|
||||
<!-- Css stuffs-->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js" integrity="sha512-gnoBksrDbaMnlE0rhhkcx3iwzvgBGz6mOEj4/Y5ZY09n55dYddx6+WYc72A55qEesV8VX2iMomteIwobeGK1BQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.css" integrity="sha512-3quBdRGJyLy79hzhDDcBzANW+mVqPctrGCfIPosHQtMKb3rKsCxfyslzwlz2wj1dT8A7UX+sEvDjaUv+WExQrA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- Favicons -->
|
||||
<link href="favicon.png" rel="icon">
|
||||
<link href="img/apple-touch-icon.png" rel="apple-touch-icon">
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@100;300;400;600;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;600;700;900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@100;300;400&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=M+PLUS+1p&family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100;300;400&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Main Stylesheet File -->
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.7.0.min.js"
|
||||
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js" integrity="sha512-5cguXwRllb+6bcc2pogwIeQmQPXEzn2ddsqAexIBhh7FO1z5Hkek1J9mrK2+rmZCTU6b6pERxI7acnp1MpAg4Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css" integrity="sha512-n//BDM4vMPvyca4bJjZPDh7hlqsQ7hqbP9RH18GF2hTXBY5amBwM2501M0GPiwCU/v9Tor2m13GOTFjk00tkQA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<style>
|
||||
p,a,div,span,h1,h2,h3,h4,h5,h6{
|
||||
font-family: 'Source Sans Pro', sans-serif !important;
|
||||
color: #404040;
|
||||
}
|
||||
</style>
|
||||
<!-- AOS.js-->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js" integrity="sha512-A7AYk1fGKX6S2SsHywmPkrnzTZHrgiVT7GcQkLGDe2ev0aWb8zejytzS8wjo7PGEXKqJOrjQ4oORtnimIRZBtw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css" integrity="sha512-1cK78a1o+ht2JcaW6g8OXYwqpev9+6GqOkz9xmBN9iUUhIndKtxwILGWYOSibOKjLsEdjyjZvYDq/cZwNeak0w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="main section">
|
||||
<div class="left-menu">
|
||||
<div class="iconWrapper">
|
||||
<a href="index.html"><img class="ui fluid image" src="img/icon.png"></a>
|
||||
</div>
|
||||
<a href="#home" class="menu-item active" align="center">
|
||||
<img src="img/icons/home.svg">
|
||||
</a>
|
||||
<a href="#features" class="menu-item" align="center">
|
||||
<img src="img/icons/awesome.svg">
|
||||
</a>
|
||||
<a href="#screenshots" class="menu-item" align="center">
|
||||
<img src="img/icons/screenshots.svg">
|
||||
</a>
|
||||
<a href="#plugins" class="menu-item" align="center">
|
||||
<img src="img/icons/plugin.svg">
|
||||
</a>
|
||||
<a href="#source" class="menu-item" align="center">
|
||||
<img src="img/icons/code.svg">
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<!-- Hero Banner Section -->
|
||||
<div class="headbanner"></div>
|
||||
<div id="home" class="herotext">
|
||||
<div class="ui basic segment">
|
||||
<div class="bannerHeaderWrapper">
|
||||
<h1 class="bannerHeader">Zoraxy</h1>
|
||||
<div class="ui divider"></div><br>
|
||||
<p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
|
||||
<div id="backToTopBtn" class="ui big icon button" onclick="backToTop();">
|
||||
<i class="ui arrow up icon"></i>
|
||||
</div>
|
||||
<button id="rwdmenubtn" class="ui black big icon button"><i class="ui bars icon"></i></button>
|
||||
<div id="mainmenu" class="ui segment" style="background: transparent !important;">
|
||||
<div class="ui container">
|
||||
<div class="ui small stackable secondary menu">
|
||||
<div class="item">
|
||||
<img class="ui tiny image" src="img/logo.png">
|
||||
</div>
|
||||
<a class="item" href="#mainmenu" i18n>
|
||||
Home // 主頁 // Startseite
|
||||
</a>
|
||||
<a class="item" href="#about" i18n>
|
||||
About Zoraxy // 關於 Zoraxy // Über Zoraxy
|
||||
</a>
|
||||
<a class="item" href="#features" i18n>
|
||||
Screenshots // 系統截圖 // Bildschirmfotos
|
||||
</a>
|
||||
<a class="item" href="#techspec" i18n>
|
||||
Videos // 介紹影片 // Videos
|
||||
</a>
|
||||
<a class="item" href="#download" i18n>
|
||||
Download // 下載 // Herunterladen
|
||||
</a>
|
||||
<a class="item" href="#learnmore" i18n>
|
||||
Learn More // 了解更多 // Mehr erfahren
|
||||
</a>
|
||||
<a class="right floated item">
|
||||
<div class="ui small selection dropdown">
|
||||
<input type="hidden" id="language">
|
||||
<div class="default text" style="color: #6cacff;"><i class="language icon"></i> Default</div>
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="en">English</div>
|
||||
<div class="item" data-value="zh">中文(正體)</div>
|
||||
<div class="item" data-value="de">Deutsch</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<br><br>
|
||||
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
|
||||
<br><br>
|
||||
<table class="ui very basic collapsing unstackable celled table">
|
||||
<thead>
|
||||
<tr><th colspan="2">Quick Access</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h4 class="ui image header">
|
||||
<i class="ui download icon"></i>
|
||||
<div class="content">
|
||||
Download
|
||||
<div class="sub header">Prebuild Binary
|
||||
</div>
|
||||
</div>
|
||||
</h4></td>
|
||||
<td>
|
||||
<a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Open <i class="ui external icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h4 class="ui image header">
|
||||
<i class="ui github icon"></i>
|
||||
<div class="content">
|
||||
Github
|
||||
<div class="sub header">Source Code
|
||||
</div>
|
||||
</div>
|
||||
</h4></td>
|
||||
<td>
|
||||
<a href="https://github.com/tobychui/zoraxy" target="_blank">Open <i class="ui external icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="wavesWrapper">
|
||||
</div>
|
||||
</div>
|
||||
<div class="messageBanner">
|
||||
<div class="ui text container">
|
||||
<p i18n>This site is currently under development. Some information might not be ready.
|
||||
// 本網站目前仍在開發中,部分資訊可能尚未準備好。
|
||||
// Diese Seite ist in Entwicklung. Einige Informationen sind möglicherweise nicht verfügbar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="slideshowBanner">
|
||||
<div class="title">
|
||||
<h1 i18n>Zoraxy</h1>
|
||||
<div class="ui divider" style="border-top: 1px solid rgba(255,255,255,0.5); "></div>
|
||||
<p i18n>The ultimate homelab networking toolbox for self-hosted services
|
||||
// 簡化自家伺服器部署之事,初學者居家網絡必備良器
|
||||
// Das ultimative Homelab-Netzwerk-Toolbox für selbstgehostete Dienste
|
||||
</p>
|
||||
<a href="https://github.com/tobychui/zoraxy/releases" class="ui basic white button" target="_blank"><i style="color:white;" class="ui download icon"></i><span i18n>Download // 立即下載 // Herunterladen </span></a>
|
||||
<a href="https://github.com/tobychui/zoraxy" class="ui basic white button" target="_blank"><i style="color: white;" class="ui code icon"></i><span i18n>Source Code // 查看原始碼 // Quellcode</span></a>
|
||||
|
||||
</div>
|
||||
<div id="wavesWrapper">
|
||||
<!-- CSS waves-->
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||
@ -137,250 +131,468 @@
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div id="features" class="section">
|
||||
<div class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/awesome.svg">
|
||||
<div class="content">
|
||||
Features
|
||||
<div class="sub header">Highlighting a few important features of Zoraxy</div>
|
||||
</div>
|
||||
</h1>
|
||||
<br>
|
||||
<div class="ui stackable grid featureList">
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/proxy.svg">
|
||||
<div class="content">
|
||||
Reverse Proxy
|
||||
</div>
|
||||
</h3>
|
||||
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/redirect.svg">
|
||||
<div class="content">
|
||||
Redirection
|
||||
</div>
|
||||
</h3>
|
||||
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/blacklist.svg">
|
||||
<div class="content">
|
||||
Geo-IP & Blacklist
|
||||
</div>
|
||||
</h3>
|
||||
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/gan.svg">
|
||||
<div class="content">
|
||||
Global Area Network
|
||||
</div>
|
||||
</h3>
|
||||
<p>ZeroTier controller integrated GAN. Enable unlimited nodes in your network with a few clicks.</p>
|
||||
</div>
|
||||
|
||||
<!-- Row 2-->
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/terminal.svg">
|
||||
<div class="content">
|
||||
Web SSH
|
||||
</div>
|
||||
</h3>
|
||||
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/stats.svg">
|
||||
<div class="content">
|
||||
Real Time Statistics
|
||||
</div>
|
||||
</h3>
|
||||
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/scan.svg">
|
||||
<div class="content">
|
||||
Scanner & Utilities
|
||||
</div>
|
||||
</h3>
|
||||
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/code.svg">
|
||||
<div class="content">
|
||||
Open Source
|
||||
</div>
|
||||
</h3>
|
||||
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
<!-- About ArozOS-->
|
||||
<div id="about" class="ui text container">
|
||||
<div class="ui stackable grid" data-aos="fade-up">
|
||||
<div class="six wide column" align="right">
|
||||
<img class="ui medium image" src="img/preview-pc.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Screenshots -->
|
||||
<div id="screenshots" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/screenshots.svg">
|
||||
<div class="content">
|
||||
Screenshots
|
||||
<div class="sub header">A quick overview of the UI designs</div>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div class="ui three column stackable grid">
|
||||
<div class="column">
|
||||
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Developments -->
|
||||
<div id="plugins" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/plugin.svg">
|
||||
<div class="content">
|
||||
Plugins
|
||||
<div class="sub header">Add custom routing rules via simple scripts</div>
|
||||
</div>
|
||||
</h1>
|
||||
<div style="width: 100%; text-align: center;">
|
||||
<br>
|
||||
<p>Documentation work in progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source code -->
|
||||
<div id="source" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/code.svg">
|
||||
<div class="content">
|
||||
Source Code
|
||||
<div class="sub header">Feel free to give us a ⭐ star ⭐.</div>
|
||||
</div>
|
||||
</h1>
|
||||
<br>
|
||||
<div class="ui two column stackable grid">
|
||||
<div class="column">
|
||||
<h3 class="ui header">
|
||||
<i class="grey github icon"></i>
|
||||
<div class="content" style="text-align: left;">
|
||||
<a href="https://github.com/tobychui/zoraxy">
|
||||
Github
|
||||
<div class="sub header">https://github.com/tobychui/zoraxy</div>
|
||||
</a>
|
||||
<div class="ten wide column">
|
||||
<div class="about-text-wrapper">
|
||||
<p class="about-title"><b i18n>Reverse Proxy // 反向代理 // Reverse-Proxy</b></p>
|
||||
<p><span i18n>Easy setups with dynamic updates // 讓你想不到般簡單易用、迅速設定、動態更新 // Einfache Einrichtung mit dynamischen Updates</span></p>
|
||||
<p i18n>Access your reverse proxy and self-hosted services from any computer with a browser, anytime, anywhere.
|
||||
// 透過瀏覽器,隨時隨地在任何裝置上存取您的反向代理及自家伺服器服務。
|
||||
// Greifen Sie jederzeit und überall von jedem Gerät aus auf Ihren Reverse-Proxy und selbst gehostete
|
||||
</p>
|
||||
<div class="ui list">
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Simple setups with web UI
|
||||
// 透過網頁介面簡單設定即可使用
|
||||
// Einfache Einrichtung mit Web-UI
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Change settings on the fly without restarting
|
||||
// 即時更改設定,無需重新啟動
|
||||
// Einstellungen ohne Neustart ändern
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
One of the best reverse proxy manager for beginners
|
||||
// 可能是最適合初學者的反向代理管理器之一
|
||||
// Einer der besten Reverse-Proxy-Manager für Anfänger
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Easily install plugins and edit configurations
|
||||
// 輕鬆安裝插件並編輯設定
|
||||
// Plugins einfach installieren und Konfigurationen bearbeiten
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="ui header">
|
||||
<i class="blue mail icon"></i>
|
||||
<div class="content" style="text-align: left;">
|
||||
<a href="mailto:toby@imuslab.com">
|
||||
Email Contact
|
||||
<div class="sub header">toby@imuslab.com</div>
|
||||
</a>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<div class="ui container">
|
||||
<p style="color: #3a3a3a">CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
|
||||
<div class="ui stackable grid" data-aos="fade-up">
|
||||
<div class="six wide column" align="right">
|
||||
<img class="ui medium image" src="img/preview-mobile.png">
|
||||
</div>
|
||||
<div class="ten wide column">
|
||||
<div class="about-text-wrapper">
|
||||
<p class="about-title"><b i18n>Real-time Analytics // 即時流量分析 // Echtzeit-Analysen</b></p>
|
||||
<p><span i18n>Dynamic statistic and access control // 動態流量數據、權限與路由設定 // Dynamische Statistik und Zugriffskontrolle</span></p>
|
||||
<p i18n>Provide real time statistical overview, take advantage of the real time traffic and situations to make better decisions.
|
||||
// 提供即時統計概覽,利用即時流量和情況做出更好的決策。
|
||||
// Bietet eine Echtzeit-Übersicht über die Statistiken, um bessere Entscheidungen zu treffen.
|
||||
</p>
|
||||
<div class="ui list">
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Real time visitor statistic
|
||||
// 即時訪客統計概覽
|
||||
// Echtzeit-Besucherstatistik
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
Instant network utilitization overview
|
||||
// 即時網路使用率概覽
|
||||
// Sofortige Netzwerkübersicht
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
No-reload access control and settings
|
||||
// 即時生效存取控制和設定
|
||||
// Zugriffskontrolle und Einstellungen ohne Neustart
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<i class="caret right icon"></i>
|
||||
<div class="content" i18n>
|
||||
One-click setting change with no downtime
|
||||
// 一鍵設定更改,無需停機
|
||||
// Einstellungsänderung mit einem Klick ohne Ausfallzeiten
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
<!-- Features -->
|
||||
<div class="ui divider"></div>
|
||||
<div id="features" class="ui container">
|
||||
<div class="centered title">
|
||||
<h1 i18n>Screenshots
|
||||
// 系統截圖
|
||||
// Bildschirmfotos
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ui three column grid">
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/1.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/2.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/3.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/4.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/5.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/6.png">
|
||||
</div>
|
||||
<!-- <div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/7.png">
|
||||
</div> -->
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/8.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/9.png">
|
||||
</div>
|
||||
<div class="column">
|
||||
<img class="ui fluid image screenshot" src="img/screenshots/10.png">
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<!-- Spec -->
|
||||
<div id="techspec" class="blackbanner">
|
||||
<br><br>
|
||||
<div class="centered title">
|
||||
<h1 style="font-weight: 600;" i18n>
|
||||
Review Videos
|
||||
// 介紹影片
|
||||
// Videos
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div class="videoScrollBar">
|
||||
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/5-lps8DC6_Y?si=rkfePn9kiYKCvYUZ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
|
||||
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/49xQYLpmedE?si=fgba2iK55s1760Xr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
|
||||
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/I_F97he5F2A?si=qKEXwDcjkX1nPejq" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
|
||||
<div class="introvideo"><iframe width="560" height="315" src="https://www.youtube.com/embed/FNU08-ufByM?si=I2hq9vsapeXB2Oqb" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br>
|
||||
</div>
|
||||
|
||||
<!-- Download -->
|
||||
<div id="download" class="ui text container">
|
||||
<br><br>
|
||||
<div class="centered title">
|
||||
<h1 i18n>
|
||||
Download
|
||||
// 下載
|
||||
// Herunterladen
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="downloadTabWrapper">
|
||||
<div class="ui top attached fluid stackable tabular menu">
|
||||
<a class="active item" data-tab="linux"><i class="grey linux icon"></i> Linux</a>
|
||||
<a class="item" data-tab="windows"><i class="blue windows icon"></i> Windows</a>
|
||||
<a class="item" data-tab="rpi"><i class="red raspberry pi icon"></i><span i18n>SBCs // ARM 開發板 // SBCs</span></a>
|
||||
<a class="item" data-tab="build"><i class="code icon"></i> <span i18n>Build from source // 從原始碼建置 // Aus dem Quellcode erstellen</span> </a>
|
||||
</div>
|
||||
<div class="ui bottom attached active tab segment" data-tab="linux">
|
||||
<p i18n>
|
||||
Install with command line
|
||||
// 使用 CLI 下載並執行發行版本
|
||||
// Installieren Sie mit der Befehlszeile
|
||||
</p>
|
||||
<div class="ui black message">
|
||||
<code>
|
||||
wget https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64<br>
|
||||
chmod +x ./zoraxy_linux_amd64<br>
|
||||
sudo ./zoraxy_linux_amd64
|
||||
</code>
|
||||
</div>
|
||||
<br>
|
||||
<p i18n>
|
||||
Install with precompiled binary
|
||||
// 下載發行版本
|
||||
// Installieren Sie mit vorkompilierten Binärdateien
|
||||
</p>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_amd64");'>
|
||||
<i class="black linux icon"></i>
|
||||
<span i18n>Download x64
|
||||
// 下載 64位元 執行檔
|
||||
// Herunterladen x64
|
||||
</span>
|
||||
</button>
|
||||
<span style="font-size: 1.2em; font-weight: 600; margin-right: 0.4em">OR</span>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_386");'>
|
||||
<i class="black linux icon"></i>
|
||||
<span i18n>Download x32
|
||||
// 下載 32位元 執行檔
|
||||
// Herunterladen x32
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment" data-tab="windows">
|
||||
<p i18n>
|
||||
Install with precompiled binary
|
||||
// 下載發行版本
|
||||
// Installieren Sie mit vorkompilierten Binärdateien
|
||||
</p>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_windows_amd64.exe");'>
|
||||
<i class="blue windows icon"></i>
|
||||
<span i18n>
|
||||
Download Zoraxy for Windows
|
||||
// 下載 Windows 版 Zoraxy
|
||||
// Zoraxy für Windows herunterladen
|
||||
</span>
|
||||
</button>
|
||||
<br><br>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment" data-tab="rpi">
|
||||
<p i18n>Install with command line (armv6-7, arm64, x86)
|
||||
// 使用 CLI 下載並執行 (armv6-7, arm64, x86)
|
||||
// Installieren Sie mit der Befehlszeile (armv6-7, arm64, x86)
|
||||
</p>
|
||||
<div class="ui black message">
|
||||
<code>
|
||||
# Check your CPU architecture<br>
|
||||
uname -m <br>
|
||||
<br>
|
||||
# For arm64 (aarch64) CPU<br>
|
||||
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64<br>
|
||||
<br>
|
||||
# For armv6 (armv6l) / armv7 (armv7l) CPU<br>
|
||||
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm<br>
|
||||
<br>
|
||||
# For RISC-V (riscv64) CPU<br>
|
||||
wget -O zoraxy https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_riscv64<br>
|
||||
<br>
|
||||
|
||||
chmod +x ./zoraxy<br>
|
||||
sudo ./zoraxy <br>
|
||||
</code>
|
||||
</div>
|
||||
<br>
|
||||
<p i18n>Install with precompiled binary
|
||||
// 下載發行版本
|
||||
// Installieren Sie mit vorkompilierten Binärdateien
|
||||
</p>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_arm");'><i class="black download icon"></i> <span i18n></span>arm (v6, v7)</button>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_arm64");'><i class="black download icon"></i> <span i18n></span>arm64</button>
|
||||
<button class="ui basic downloadButton button" onclick='handleDownload("zoraxy_linux_riscv64");'><i class="grey download icon"></i> <span i18n></span>riscv64</button>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment" data-tab="build">
|
||||
<p i18n>Require Go (Golang) compiler. Details build from source instruction can be found on Zoraxy Github README file.
|
||||
// 需要 Go (Go 語言)編譯器。建置詳情可以在 Zoraxy Github README 檔案中找到。
|
||||
// Erfordert den Go (Golang) Compiler. Detaillierte Anweisungen zum Erstellen aus dem Quellcode finden Sie in der Zoraxy Github README-Datei.
|
||||
</p>
|
||||
<div class="ui black message">
|
||||
<code>
|
||||
git clone https://github.com/tobychui/zoraxy<br>
|
||||
cd ./zoraxy/src/<br>
|
||||
go mod tidy<br>
|
||||
go build<br>
|
||||
sudo ./zoraxy <br>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<span i18n>After Zoraxy is started, navigate to
|
||||
// 當 Zoraxy 執行檔 / 服務啟動後,使用瀏覽器開啟
|
||||
// Nachdem Zoraxy gestartet wurde, navigieren Sie zu
|
||||
</span>
|
||||
<a href="http://localhost:8000" target="_blank">http://localhost:8000</a>
|
||||
<span i18n>to continue account and system setup.
|
||||
// 以繼續帳戶和系統設定。
|
||||
// um die Konto- und Systemeinrichtung fortzusetzen.
|
||||
</span>
|
||||
</p>
|
||||
<br><br>
|
||||
</div>
|
||||
|
||||
<!-- Learn More -->
|
||||
<div class="ui divider"></div>
|
||||
<div id="learnmore" class="ui text container">
|
||||
<div class="centered title" style="margin-bottom: 0px;">
|
||||
<h1 i18n>Learn More
|
||||
// 了解更多
|
||||
// Mehr erfahren
|
||||
</h1>
|
||||
<p i18n>If you like this project, please feel free to give us a ⭐ star ⭐.
|
||||
// 如果您喜歡這個開源專案,歡迎來給我們一顆 ⭐星星⭐ 喔!!
|
||||
// Wenn Ihnen dieses Projekt gefällt, geben Sie uns bitte einen ⭐ Stern ⭐.
|
||||
</p>
|
||||
</div>
|
||||
<div class="ui basic segment linkicons">
|
||||
<div class="ui big breadcrumb">
|
||||
<a class="section externallink" href="https://github.com/tobychui/zoraxy" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="black github icon"></i>
|
||||
<div class="content" i18n>
|
||||
Github
|
||||
// 源碼
|
||||
// Quellcode
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<i class="divider"> </i>
|
||||
<a class="section externallink" href="" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="green code icon"></i>
|
||||
<div class="content" i18n>
|
||||
Plugin Devs
|
||||
// 插件開發
|
||||
// Plugin-Entwickler
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<i class=" divider"> </i>
|
||||
<a class="section externallink" href="mailto:toby@imuslab.com" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="yellow mail icon"></i>
|
||||
<div class="content" i18n>
|
||||
Email
|
||||
// 電子郵件
|
||||
// E-Mail
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<i class=" divider"> </i>
|
||||
<a class="section externallink" href="https://t.me/ArOZBeta" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="blue telegram icon"></i>
|
||||
<div class="content">
|
||||
Telegram
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div id="footer">
|
||||
<div class="ui container">
|
||||
<br><br>
|
||||
<div class="ui stackable grid" style="height: 100%;">
|
||||
<div class="six wide column" style="height: 100%;">
|
||||
<a href="https://zoraxy.aroz.org"><img src="img/logo_white.png" class="ui small image"></a>
|
||||
<p><span style="font-weight: 300;">The Zoraxy Project</span><br>
|
||||
© Toby Chui</p>
|
||||
|
||||
<div class="bottom-attach">
|
||||
<br><br>
|
||||
<div class="ui breadcrumb" style="margin-top: 0.4em;">
|
||||
<div class="section" i18n><a style="color: white;" href="https://zoraxy.aroz.org" target="_blank">zoraxy.aroz.org</a></div>
|
||||
<div class="divider"> / </div>
|
||||
<div class="section">2018 - <span class="year">now</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three wide column">
|
||||
<div class="ui list">
|
||||
<div class="item title" i18n>Developer Tools
|
||||
// 開發者工具
|
||||
// Entwicklerwerkzeuge
|
||||
</div>
|
||||
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki" target="_blank">Zoraxy Wiki</a></div>
|
||||
<div class="item"><a href="https://github.com/tobychui/zoraxy" target="_blank">Source Code</a></div>
|
||||
<div class="item"><a href="" target="_blank">Offical Plugin List</a></div>
|
||||
<div class="item"><a href="" target="_blank">Plugin Development Guide</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three wide column">
|
||||
<div class="ui list">
|
||||
<div class="item title" i18n>Project Spin-offs
|
||||
// 衍生開源計劃
|
||||
// Projekt-Ableger
|
||||
</div>
|
||||
<div class="item"><a href="https://os.aroz.org" target="_blank">ArozOS</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three wide column">
|
||||
<div class="ui list">
|
||||
<div class="item title" i18n>Related Links
|
||||
// 相關連接
|
||||
// Verwandte Links
|
||||
</div>
|
||||
<div class="item"><a href="https://github.com/tobychui/zoraxy/wiki/Getting-Started" target="_blank" i18n>Getting Started</a></div>
|
||||
<div class="item"><a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Zoraxy Release</a></div>
|
||||
<div class="item"><a href="https://hub.docker.com/r/zoraxydocker/zoraxy" target="_blank">Zoraxy Docker</a></div>
|
||||
<div class="item"><a href="https://imuslab.com" target="_blank">imuslab</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(".year").text(new Date().getFullYear() );
|
||||
AOS.init();
|
||||
$(".year").text(new Date().getFullYear());
|
||||
|
||||
$(".menu-item").on("click", function(){
|
||||
$(".menu-item.active").removeClass("active");
|
||||
$(this).addClass("active");
|
||||
});
|
||||
// Function to open the modal with the clicked image using jQuery
|
||||
function openModal(src) {
|
||||
// Remove the old modal if it exists
|
||||
$('#imageModal').remove();
|
||||
|
||||
$(".right-content").on("scroll", function() {
|
||||
var scrollPos = $(".right-content").scrollTop();
|
||||
if (scrollPos < 10){
|
||||
//Reaching the top
|
||||
$('.menu-item.active').removeClass("active");
|
||||
$($('.menu-item')[0]).addClass('active');
|
||||
return;
|
||||
}else if ($(".right-content")[0].scrollHeight == $(".right-content").scrollTop() + window.innerHeight ){
|
||||
//Reaching the bottom
|
||||
$('.menu-item.active').removeClass("active");
|
||||
$($('.menu-item').get().reverse()[0]).addClass('active');
|
||||
return
|
||||
const modal = $('<div style="display:none;">', { id: 'imageModal' }).css({
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: '1000'
|
||||
});
|
||||
|
||||
const img = $('<img>', { src: src }).css({
|
||||
maxWidth: '80%',
|
||||
maxHeight: '80%',
|
||||
boxShadow: '0 0 10px rgba(0, 0, 0, 0.5)'
|
||||
});
|
||||
|
||||
modal.append(img);
|
||||
$("body").css("overflow", "hidden");
|
||||
|
||||
modal.on('click', function() {
|
||||
modal.remove();
|
||||
$("body").css("overflow", "auto");
|
||||
});
|
||||
|
||||
$('body').append(modal);
|
||||
}
|
||||
$('.menu-item').each(function() {
|
||||
var currLink = $(this);
|
||||
var refElement = $(currLink.attr("href"));
|
||||
if (refElement.offset().top <= (window.innerHeight / 2)) {
|
||||
$('.menu-item.active').removeClass("active");
|
||||
currLink.addClass("active");
|
||||
console.log(currLink.attr("href"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add click event listener to all screenshot images using jQuery
|
||||
$('.screenshot').on('click', function() {
|
||||
openModal($(this).attr('src'));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
386
docs/index_legacy.html
Normal file
386
docs/index_legacy.html
Normal file
@ -0,0 +1,386 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<meta content="Reverse Proxy, Cluster, Gateway, Go, Homelab, Network Tools" name="keywords">
|
||||
<meta content="A reverse proxy server and cluster network gateway for noobs" name="description">
|
||||
<meta name="author" content="tobychui">
|
||||
|
||||
<!-- HTML Meta Tags -->
|
||||
<title>Reverse Proxy Server | Zoraxy</title>
|
||||
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://zoraxy.aroz.org/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="aroz.org">
|
||||
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
|
||||
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link href="favicon.png" rel="icon">
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@100;300;400;600;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
|
||||
|
||||
<!-- Main Stylesheet File -->
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script
|
||||
src="https://code.jquery.com/jquery-3.7.0.min.js"
|
||||
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js" integrity="sha512-5cguXwRllb+6bcc2pogwIeQmQPXEzn2ddsqAexIBhh7FO1z5Hkek1J9mrK2+rmZCTU6b6pERxI7acnp1MpAg4Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css" integrity="sha512-n//BDM4vMPvyca4bJjZPDh7hlqsQ7hqbP9RH18GF2hTXBY5amBwM2501M0GPiwCU/v9Tor2m13GOTFjk00tkQA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<style>
|
||||
p,a,div,span,h1,h2,h3,h4,h5,h6{
|
||||
font-family: 'Source Sans Pro', sans-serif !important;
|
||||
color: #404040;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main section">
|
||||
<div class="left-menu">
|
||||
<div class="iconWrapper">
|
||||
<a href="index.html"><img class="ui fluid image" src="img/icon.png"></a>
|
||||
</div>
|
||||
<a href="#home" class="menu-item active" align="center">
|
||||
<img src="img/icons/home.svg">
|
||||
</a>
|
||||
<a href="#features" class="menu-item" align="center">
|
||||
<img src="img/icons/awesome.svg">
|
||||
</a>
|
||||
<a href="#screenshots" class="menu-item" align="center">
|
||||
<img src="img/icons/screenshots.svg">
|
||||
</a>
|
||||
<a href="#plugins" class="menu-item" align="center">
|
||||
<img src="img/icons/plugin.svg">
|
||||
</a>
|
||||
<a href="#source" class="menu-item" align="center">
|
||||
<img src="img/icons/code.svg">
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<!-- Hero Banner Section -->
|
||||
<div class="headbanner"></div>
|
||||
<div id="home" class="herotext">
|
||||
<div class="ui basic segment">
|
||||
<div class="bannerHeaderWrapper">
|
||||
<h1 class="bannerHeader">Zoraxy</h1>
|
||||
<div class="ui divider"></div><br>
|
||||
<p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
|
||||
</div>
|
||||
<br><br>
|
||||
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
|
||||
<br><br>
|
||||
<table class="ui very basic collapsing unstackable celled table">
|
||||
<thead>
|
||||
<tr><th colspan="2">Quick Access</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h4 class="ui image header">
|
||||
<i class="ui download icon"></i>
|
||||
<div class="content">
|
||||
Download
|
||||
<div class="sub header">Prebuild Binary
|
||||
</div>
|
||||
</div>
|
||||
</h4></td>
|
||||
<td>
|
||||
<a href="https://github.com/tobychui/zoraxy/releases" target="_blank">Open <i class="ui external icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h4 class="ui image header">
|
||||
<i class="ui github icon"></i>
|
||||
<div class="content">
|
||||
Github
|
||||
<div class="sub header">Source Code
|
||||
</div>
|
||||
</div>
|
||||
</h4></td>
|
||||
<td>
|
||||
<a href="https://github.com/tobychui/zoraxy" target="_blank">Open <i class="ui external icon"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="wavesWrapper">
|
||||
<!-- CSS waves-->
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||
<defs>
|
||||
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
|
||||
</defs>
|
||||
<g class="parallax">
|
||||
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div id="features" class="section">
|
||||
<div class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/awesome.svg">
|
||||
<div class="content">
|
||||
Features
|
||||
<div class="sub header">Highlighting a few important features of Zoraxy</div>
|
||||
</div>
|
||||
</h1>
|
||||
<br>
|
||||
<div class="ui stackable grid featureList">
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/proxy.svg">
|
||||
<div class="content">
|
||||
Reverse Proxy
|
||||
</div>
|
||||
</h3>
|
||||
<p>Simple to use noob-friendly reverse proxy server that can be easily set up using a web form and a few toggle switches.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/redirect.svg">
|
||||
<div class="content">
|
||||
Redirection
|
||||
</div>
|
||||
</h3>
|
||||
<p>Direct and intuitive redirection rules with basic rewrite options. Suitable for most simple use cases.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/blacklist.svg">
|
||||
<div class="content">
|
||||
Geo-IP & Blacklist
|
||||
</div>
|
||||
</h3>
|
||||
<p>Blacklist with GeoIP support. Allows easy setup for regional services.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/gan.svg">
|
||||
<div class="content">
|
||||
Global Area Network
|
||||
</div>
|
||||
</h3>
|
||||
<p>ZeroTier controller integrated GAN. Enable unlimited nodes in your network with a few clicks.</p>
|
||||
</div>
|
||||
|
||||
<!-- Row 2-->
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/terminal.svg">
|
||||
<div class="content">
|
||||
Web SSH
|
||||
</div>
|
||||
</h3>
|
||||
<p>Integration with Gotty Web SSH terminal allows one-stop management of your nodes inside private LAN via gateway nodes.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/stats.svg">
|
||||
<div class="content">
|
||||
Real Time Statistics
|
||||
</div>
|
||||
</h3>
|
||||
<p>Traffic data collection and real-time analytic tools provide you the best insight of visitors data without cookies.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/scan.svg">
|
||||
<div class="content">
|
||||
Scanner & Utilities
|
||||
</div>
|
||||
</h3>
|
||||
<p>Build in IP scanner and mDNS discovery service to enable automatic service discovery within LAN.</p>
|
||||
</div>
|
||||
|
||||
<div class="four wide column featureItem">
|
||||
<h3 class="ui header featureHeader">
|
||||
<img class="ui image" src="img/icons/code.svg">
|
||||
<div class="content">
|
||||
Open Source
|
||||
</div>
|
||||
</h3>
|
||||
<p>Project is open-source under AGPL on Github. Feel free to contribute on missing functions you need! </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Screenshots -->
|
||||
<div id="screenshots" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/screenshots.svg">
|
||||
<div class="content">
|
||||
Screenshots
|
||||
<div class="sub header">A quick overview of the UI designs</div>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<div class="ui three column stackable grid">
|
||||
<div class="column">
|
||||
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Developments -->
|
||||
<div id="plugins" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/plugin.svg">
|
||||
<div class="content">
|
||||
Plugins
|
||||
<div class="sub header">Add custom routing rules via simple scripts</div>
|
||||
</div>
|
||||
</h1>
|
||||
<div style="width: 100%; text-align: center;">
|
||||
<br>
|
||||
<p>Documentation work in progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Source code -->
|
||||
<div id="source" class="ui container">
|
||||
<div class="ui basic segment">
|
||||
<br>
|
||||
<h1 class="ui header">
|
||||
<img class="ui small image" src="img/icons/code.svg">
|
||||
<div class="content">
|
||||
Source Code
|
||||
<div class="sub header">Feel free to give us a ⭐ star ⭐.</div>
|
||||
</div>
|
||||
</h1>
|
||||
<br>
|
||||
<div class="ui two column stackable grid">
|
||||
<div class="column">
|
||||
<h3 class="ui header">
|
||||
<i class="grey github icon"></i>
|
||||
<div class="content" style="text-align: left;">
|
||||
<a href="https://github.com/tobychui/zoraxy">
|
||||
Github
|
||||
<div class="sub header">https://github.com/tobychui/zoraxy</div>
|
||||
</a>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="ui header">
|
||||
<i class="blue mail icon"></i>
|
||||
<div class="content" style="text-align: left;">
|
||||
<a href="mailto:toby@imuslab.com">
|
||||
Email Contact
|
||||
<div class="sub header">toby@imuslab.com</div>
|
||||
</a>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<div class="ui container">
|
||||
<p style="color: #3a3a3a">CopyRight Zoraxy Project and its authors © 2021 - <span class="year"></span></p>
|
||||
</div>
|
||||
<br><br><br>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<script>
|
||||
$(".year").text(new Date().getFullYear() );
|
||||
|
||||
$(".menu-item").on("click", function(){
|
||||
$(".menu-item.active").removeClass("active");
|
||||
$(this).addClass("active");
|
||||
});
|
||||
|
||||
$(".right-content").on("scroll", function() {
|
||||
var scrollPos = $(".right-content").scrollTop();
|
||||
if (scrollPos < 10){
|
||||
//Reaching the top
|
||||
$('.menu-item.active').removeClass("active");
|
||||
$($('.menu-item')[0]).addClass('active');
|
||||
return;
|
||||
}else if ($(".right-content")[0].scrollHeight == $(".right-content").scrollTop() + window.innerHeight ){
|
||||
//Reaching the bottom
|
||||
$('.menu-item.active').removeClass("active");
|
||||
$($('.menu-item').get().reverse()[0]).addClass('active');
|
||||
return
|
||||
}
|
||||
$('.menu-item').each(function() {
|
||||
var currLink = $(this);
|
||||
var refElement = $(currLink.attr("href"));
|
||||
if (refElement.offset().top <= (window.innerHeight / 2)) {
|
||||
$('.menu-item.active').removeClass("active");
|
||||
currLink.addClass("active");
|
||||
console.log(currLink.attr("href"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
434
docs/main.css
Normal file
434
docs/main.css
Normal file
@ -0,0 +1,434 @@
|
||||
/* Global */
|
||||
|
||||
p,a,div,span,h1,h2,h3,h4,h5,h6{
|
||||
font-family: 'Source Sans Pro', sans-serif;
|
||||
}
|
||||
|
||||
body.en *:not(i){
|
||||
font-family: 'Source Sans Pro', 'Noto Sans TC',sans-serif !important;
|
||||
}
|
||||
|
||||
body.zh *:not(i){
|
||||
font-family: 'Noto Sans TC',sans-serif !important;
|
||||
}
|
||||
|
||||
body.jp *:not(i){
|
||||
font-family: "Noto Sans JP", sans-serif !important;
|
||||
}
|
||||
|
||||
body.zh-cn *:not(i){
|
||||
font-family: 'Noto Sans SC',sans-serif !important;
|
||||
}
|
||||
|
||||
|
||||
.centered.title{
|
||||
padding: 2em;
|
||||
margin-bottom: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
.centered.title h1{
|
||||
font-weight: 300 !important;
|
||||
}
|
||||
|
||||
.messageBanner{
|
||||
width: 100%;
|
||||
background: #6cacff;
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
.messageBanner .header{
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#backToTopBtn{
|
||||
position: fixed;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
display:none;
|
||||
z-index: 999;
|
||||
border: 1px solid white;
|
||||
background: #6cacff;
|
||||
}
|
||||
|
||||
#backToTopBtn:hover{
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#backToTopBtn i{
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Main Menu */
|
||||
#mainmenu{
|
||||
padding-top: 0.4em;
|
||||
padding-bottom: 0.4em;
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#slideshowBanner .ui.basic.white.button{
|
||||
color: white;
|
||||
box-shadow: 0 0 0 1px rgb(231, 231, 231) inset;
|
||||
border-radius: 0.4em;
|
||||
background: none !important;
|
||||
}
|
||||
#slideshowBanner .ui.basic.white.button:hover{
|
||||
background-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
#slideshowBanner .ui.basic.white.button:active{
|
||||
background: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
#rwdmenubtn{
|
||||
display:none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#mainmenu .ui.secondary.inverted.menu .link.item:not(.disabled), .ui.secondary.inverted.menu a.item:not(.disabled){
|
||||
font-size: 1.1em;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-bottom ease-in-out 0.1s;
|
||||
color: white !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#mainmenu #mainmenu .ui.secondary.inverted.menu .link.item:not(.disabled), .ui.secondary.inverted.menu a.item:not(.disabled):hover{
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid #82adfc;
|
||||
color: #82adfc !important;
|
||||
}
|
||||
|
||||
/* Image Sldiers */
|
||||
#slideshowBanner{
|
||||
background: rgb(108,172,255);
|
||||
background: linear-gradient(48deg, rgba(108,172,255,1) 8%, rgba(141,235,255,1) 65%);
|
||||
position: relative;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.slideshow {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.slideshow .slides {
|
||||
display: flex;
|
||||
transition: transform 1s ease-in-out;
|
||||
opacity: 0.6;
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.slideshow .slide {
|
||||
min-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.slideshow .slide img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.slideshow .dots{
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slideshow .dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 0 5px;
|
||||
background-color: #bebebe;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.6s ease;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#slideshowBanner .title{
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10%;
|
||||
transform: translateX(0%) translateY(-50%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#slideshowBanner .title .scrolldownTips{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#slideshowBanner .title h1{
|
||||
font-size: 4em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#slideshowBanner .title p{
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* About Zoraxy */
|
||||
.about-text-wrapper{
|
||||
margin-top: 3em;
|
||||
}
|
||||
.about-text-wrapper p, .about-text-wrapper .list .item{
|
||||
font-weight: 300;
|
||||
}
|
||||
.about-title{
|
||||
font-size: 2.4em;
|
||||
font-weight: 300;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
.about-title b{
|
||||
font-weight: 800;
|
||||
}
|
||||
.about-text-wrapper .ui.list .item{
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
.about-text-wrapper .ui.list .item .icon{
|
||||
padding-top: 0.15em;
|
||||
}
|
||||
|
||||
/* Screenshots */
|
||||
#features{
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
#features .screenshot{
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#features .screenshot:hover{
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Videos */
|
||||
#techspec .centered.title{
|
||||
color: white;
|
||||
}
|
||||
|
||||
#techspec p {
|
||||
color: white;
|
||||
}
|
||||
|
||||
#techspec .videoScrollBar{
|
||||
overflow-x: scroll;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
scrollbar-color: #e7e7e7 rgba(0, 0, 0, 0.1);
|
||||
padding-top: 2em;
|
||||
padding-bottom: 3em;
|
||||
}
|
||||
|
||||
.introvideo{
|
||||
display: inline-block !important;
|
||||
|
||||
}
|
||||
|
||||
.blackbanner{
|
||||
width: 100%;
|
||||
background: rgb(108,172,255);
|
||||
background: linear-gradient(48deg, rgba(108,172,255,1) 8%, rgba(141,235,255,1) 65%);
|
||||
min-height: 300px;
|
||||
|
||||
}
|
||||
|
||||
/* Download */
|
||||
.downloadButton {
|
||||
margin-top: 0.4em !important;
|
||||
}
|
||||
|
||||
.downloadTabWrapper{
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#download .ui.black.message{
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Learn More */
|
||||
#learnmore .linkicons{
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#learnmore .linkicons .divider{
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#learnmore .linkicons .externallink{
|
||||
margin-bottom: 0.6em;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
#learnmore .linkicons .externallink i{
|
||||
/* color: #1b1c1d; */
|
||||
font-weight: 300;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#learnmore .linkicons .externallink:hover{
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
#learnmore .linkicons .externallink .content{
|
||||
color: #1b1c1d;
|
||||
font-weight: 500;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
|
||||
/* Footer */
|
||||
#footer{
|
||||
background: rgb(85,131,238);
|
||||
background: linear-gradient(48deg, rgba(85,131,238,1) 21%, rgba(108,172,255,1) 73%);
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
#footer a {
|
||||
color: rgb(209, 224, 255);
|
||||
}
|
||||
|
||||
#footer a:hover{
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
#footer .bottom-attach .divider{
|
||||
color: rgb(212, 212, 212);
|
||||
}
|
||||
|
||||
#footer .ui.list .title{
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
/* RWD Rules */
|
||||
@media (max-width:960px) {
|
||||
/* Main menu */
|
||||
#mainmenu{
|
||||
display:none;
|
||||
z-index: 99;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#rwdmenubtn{
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0.4em;
|
||||
right: 0.4em;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Slideshows */
|
||||
.slideshow {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.slideshow .slide{
|
||||
height: 100% !important;
|
||||
min-width: none;
|
||||
}
|
||||
|
||||
.slideshow .slide img{
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#slideshowBanner .title .scrolldownTips{
|
||||
margin-top: 2em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#slideshowBanner .title .scrolldownTips img{
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
#download .stackable.tabular.menu .active.item{
|
||||
background-color: rgb(243, 243, 243);
|
||||
border-width: 0;
|
||||
border-radius: 0.4em !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Waves CSS
|
||||
*/
|
||||
|
||||
#wavesWrapper{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.waves {
|
||||
position:relative;
|
||||
width: 100%;
|
||||
height:15vh;
|
||||
margin-bottom:-7px; /*Fix for safari gap*/
|
||||
min-height:100px;
|
||||
max-height:150px;
|
||||
}
|
||||
|
||||
|
||||
.parallax > use {
|
||||
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
|
||||
}
|
||||
.parallax > use:nth-child(1) {
|
||||
animation-delay: -8s;
|
||||
animation-duration: 28s;
|
||||
}
|
||||
.parallax > use:nth-child(2) {
|
||||
animation-delay: -12s;
|
||||
animation-duration: 40s;
|
||||
}
|
||||
.parallax > use:nth-child(3) {
|
||||
animation-delay: -16s;
|
||||
animation-duration: 52s;
|
||||
}
|
||||
.parallax > use:nth-child(4) {
|
||||
animation-delay: -20s;
|
||||
animation-duration: 80s;
|
||||
}
|
||||
@keyframes move-forever {
|
||||
0% {
|
||||
transform: translate3d(-90px,0,0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(85px,0,0);
|
||||
}
|
||||
}
|
||||
/*Shrinking for mobile*/
|
||||
@media (max-width: 768px) {
|
||||
.waves {
|
||||
height:40px;
|
||||
min-height:40px;
|
||||
}
|
||||
}
|
81
docs/main.js
Normal file
81
docs/main.js
Normal file
@ -0,0 +1,81 @@
|
||||
/*
|
||||
Localization
|
||||
|
||||
To add more locales, add to the html file with // (translated text)
|
||||
after each DOM elements with attr i18n
|
||||
|
||||
And then add the language ISO key to the list below.
|
||||
*/
|
||||
let languages = ['en', 'zh', 'de'];
|
||||
|
||||
|
||||
//Bind language change dropdown events
|
||||
$(".dropdown").dropdown();
|
||||
$("#language").on("change",function(){
|
||||
let newLang = $("#language").parent().dropdown("get value");
|
||||
i18n.changeLanguage(newLang);
|
||||
$("body").attr("class", newLang);
|
||||
});
|
||||
|
||||
//Initialize the i18n dom library
|
||||
var i18n = domI18n({
|
||||
selector: '[i18n]',
|
||||
separator: ' // ',
|
||||
languages: languages,
|
||||
defaultLanguage: 'en'
|
||||
});
|
||||
|
||||
let userLang = navigator.language || navigator.userLanguage;
|
||||
console.log("User language: " + userLang);
|
||||
userLang = userLang.split("-")[0];
|
||||
if (!languages.includes(userLang)) {
|
||||
userLang = 'en';
|
||||
}
|
||||
i18n.changeLanguage(userLang);
|
||||
|
||||
|
||||
/* Main Menu */
|
||||
$("#rwdmenubtn").on("click", function(){
|
||||
$("#mainmenu").slideToggle("fast");
|
||||
})
|
||||
|
||||
//Handle resize
|
||||
$(window).on("resize", function(){
|
||||
if (window.innerWidth > 960){
|
||||
$("#mainmenu").show();
|
||||
}else{
|
||||
$("#mainmenu").hide();
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
Download
|
||||
*/
|
||||
|
||||
$('.menu .item').tab();
|
||||
|
||||
//Download webpack and binary at the same time
|
||||
function handleDownload(releasename){
|
||||
let binaryURL = "https://github.com/tobychui/zoraxy/releases/latest/download/" + releasename;
|
||||
window.open(binaryURL);
|
||||
}
|
||||
|
||||
/* RWD */
|
||||
window.addEventListener('scroll', function() {
|
||||
var scrollPosition = window.scrollY || window.pageYOffset;
|
||||
var windowHeight = window.innerHeight;
|
||||
var hiddenDiv = document.querySelector('#backToTopBtn');
|
||||
|
||||
if (scrollPosition > windowHeight / 2) {
|
||||
hiddenDiv.style.display = 'block';
|
||||
} else {
|
||||
hiddenDiv.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function backToTop(){
|
||||
$('html, body').animate({scrollTop : 0},800, function(){
|
||||
window.location.hash = "";
|
||||
});
|
||||
}
|
@ -1,6 +1,16 @@
|
||||
#!/bin/bash
|
||||
# This script builds all the plugins in the current directory
|
||||
|
||||
echo "Copying zoraxy_plugin to all mods"
|
||||
for dir in ./*; do
|
||||
if [ -d "$dir" ]; then
|
||||
cp -r ../mod/plugins/zoraxy_plugin "$dir/mod"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
# Iterate over all directories in the current directory
|
||||
echo "Running go mod tidy and go build for all directories"
|
||||
for dir in */; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "Processing directory: $dir"
|
||||
@ -19,4 +29,4 @@ for dir in */; do
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Build process completed for all directories."
|
||||
echo "Build process completed for all directories."
|
||||
|
@ -3,14 +3,17 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
plugin "aroz.org/zoraxy/debugger/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.debugger"
|
||||
UI_PATH = "/debug"
|
||||
PLUGIN_ID = "org.aroz.zoraxy.debugger"
|
||||
UI_PATH = "/debug"
|
||||
STATIC_CAPTURE_INGRESS = "/s_capture"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -28,15 +31,18 @@ func main() {
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
|
||||
GlobalCapturePaths: []plugin.CaptureRule{
|
||||
StaticCapturePaths: []plugin.StaticCaptureRule{
|
||||
{
|
||||
CapturePath: "/debug_test", //Capture all traffic of all HTTP proxy rule
|
||||
IncludeSubPaths: true,
|
||||
CapturePath: "/test_a",
|
||||
},
|
||||
{
|
||||
CapturePath: "/test_b",
|
||||
},
|
||||
},
|
||||
GlobalCaptureIngress: "",
|
||||
AlwaysCapturePaths: []plugin.CaptureRule{},
|
||||
AlwaysCaptureIngress: "",
|
||||
StaticCaptureIngress: "/s_capture",
|
||||
|
||||
DynamicCaptureSniff: "/d_sniff",
|
||||
DynamicCaptureIngress: "/d_capture",
|
||||
|
||||
UIPath: UI_PATH,
|
||||
|
||||
@ -50,21 +56,85 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Register the shutdown handler
|
||||
plugin.RegisterShutdownHandler(func() {
|
||||
// Do cleanup here if needed
|
||||
fmt.Println("Debugger Terminated")
|
||||
// Setup the path router
|
||||
pathRouter := plugin.NewPathRouter()
|
||||
pathRouter.SetDebugPrintMode(true)
|
||||
|
||||
/*
|
||||
Static Routers
|
||||
*/
|
||||
pathRouter.RegisterPathHandler("/test_a", http.HandlerFunc(HandleCaptureA))
|
||||
pathRouter.RegisterPathHandler("/test_b", http.HandlerFunc(HandleCaptureB))
|
||||
pathRouter.SetDefaultHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//In theory this should never be called
|
||||
//but just in case the request is not captured by the path handlers
|
||||
//this will be the fallback handler
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the default handler!<br>Request URI: " + r.URL.String()))
|
||||
}))
|
||||
pathRouter.RegisterStaticCaptureHandle(STATIC_CAPTURE_INGRESS, http.DefaultServeMux)
|
||||
|
||||
/*
|
||||
Dynamic Captures
|
||||
*/
|
||||
pathRouter.RegisterDynamicSniffHandler("/d_sniff", http.DefaultServeMux, func(dsfr *plugin.DynamicSniffForwardRequest) plugin.SniffResult {
|
||||
//fmt.Println("Dynamic Capture Sniffed Request:")
|
||||
//fmt.Println("Request URI: " + dsfr.RequestURI)
|
||||
|
||||
//In this example, we want to capture all URI
|
||||
//that start with /test_ and forward it to the dynamic capture handler
|
||||
if strings.HasPrefix(dsfr.RequestURI, "/test_") {
|
||||
reqUUID := dsfr.GetRequestUUID()
|
||||
fmt.Println("Accepting request with UUID: " + reqUUID)
|
||||
return plugin.SniffResultAccpet
|
||||
}
|
||||
|
||||
return plugin.SniffResultSkip
|
||||
})
|
||||
pathRouter.RegisterDynamicCaptureHandle("/d_capture", http.DefaultServeMux, func(w http.ResponseWriter, r *http.Request) {
|
||||
// This is the dynamic capture handler where it actually captures and handle the request
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Welcome to the dynamic capture handler!"))
|
||||
|
||||
// Print all the request info to the response writer
|
||||
w.Write([]byte("\n\nRequest Info:\n"))
|
||||
w.Write([]byte("Request URI: " + r.RequestURI + "\n"))
|
||||
w.Write([]byte("Request Method: " + r.Method + "\n"))
|
||||
w.Write([]byte("Request Headers:\n"))
|
||||
headers := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
headers = append(headers, key)
|
||||
}
|
||||
sort.Strings(headers)
|
||||
for _, key := range headers {
|
||||
for _, value := range r.Header[key] {
|
||||
w.Write([]byte(fmt.Sprintf("%s: %s\n", key, value)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc(UI_PATH+"/", RenderDebugUI)
|
||||
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")
|
||||
func HandleCaptureA(w http.ResponseWriter, r *http.Request) {
|
||||
/*for key, values := range r.Header {
|
||||
for _, value := range values {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}*/
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the debugger"))
|
||||
w.Write([]byte("This request is captured by A handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
|
||||
func HandleCaptureB(w http.ResponseWriter, r *http.Request) {
|
||||
/*for key, values := range r.Header {
|
||||
for _, value := range values {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}*/
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("This request is captured by the B handler!<br>Request URI: " + r.URL.String()))
|
||||
}
|
||||
|
145
example/plugins/debugger/mod/zoraxy_plugin/dev_webserver.go
Normal file
145
example/plugins/debugger/mod/zoraxy_plugin/dev_webserver.go
Normal file
@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PluginUiDebugRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetDir string //The directory where the UI files are stored
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
|
||||
// The targetDir is the directory where the UI files are stored (e.g. ./www)
|
||||
// The handlerPrefix is the prefix of the handler used to route this router
|
||||
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
|
||||
// All prefix should not end with a slash
|
||||
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
|
||||
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
|
||||
// if mux is nil, the handler will be registered to http.DefaultServeMux
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
162
example/plugins/debugger/mod/zoraxy_plugin/dynamic_router.go
Normal file
162
example/plugins/debugger/mod/zoraxy_plugin/dynamic_router.go
Normal file
@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
|
||||
You can decide to accept or skip the request based on the request header and paths
|
||||
*/
|
||||
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
Sniffing and forwarding
|
||||
|
||||
The following functions are here to help with
|
||||
sniffing and forwarding requests to the dynamic
|
||||
router.
|
||||
*/
|
||||
// A custom request object to be used in the dynamic sniffing
|
||||
type DynamicSniffForwardRequest struct {
|
||||
Method string `json:"method"`
|
||||
Hostname string `json:"hostname"`
|
||||
URL string `json:"url"`
|
||||
Header map[string][]string `json:"header"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Host string `json:"host"`
|
||||
RequestURI string `json:"request_uri"`
|
||||
Proto string `json:"proto"`
|
||||
ProtoMajor int `json:"proto_major"`
|
||||
ProtoMinor int `json:"proto_minor"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
@ -6,15 +6,18 @@ import (
|
||||
"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
|
||||
PluginID string //The ID of the plugin
|
||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
||||
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
|
||||
@ -55,11 +58,6 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
//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, "/")
|
||||
@ -72,8 +70,24 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body))
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
@ -86,11 +100,18 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
@ -104,3 +125,32 @@ func (p *PluginUiRouter) Handler() http.Handler {
|
||||
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
|
||||
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
|
||||
// if mux is nil, the handler will be registered to http.DefaultServeMux
|
||||
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
||||
|
105
example/plugins/debugger/mod/zoraxy_plugin/static_router.go
Normal file
105
example/plugins/debugger/mod/zoraxy_plugin/static_router.go
Normal file
@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
@ -4,9 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -24,9 +22,9 @@ const (
|
||||
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 StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
type ControlStatusCode int
|
||||
@ -44,8 +42,9 @@ type SubscriptionEvent struct {
|
||||
}
|
||||
|
||||
type RuntimeConstantValue struct {
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
/*
|
||||
@ -74,23 +73,24 @@ type IntroSpect struct {
|
||||
*/
|
||||
|
||||
/*
|
||||
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
|
||||
Static Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
This is faster than dynamic capture, but less flexible
|
||||
*/
|
||||
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)
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
/*
|
||||
Always Capture Settings
|
||||
Dynamic Capture Settings
|
||||
|
||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||
these always applies
|
||||
Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
if the plugin sniff returns 280, the traffic will be captured
|
||||
otherwise, the traffic will be forwarded to the next plugin
|
||||
This is slower than static capture, but more flexible
|
||||
*/
|
||||
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)
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
/* UI Path for your plugin */
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||
@ -174,25 +174,3 @@ 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)
|
||||
}()
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
plugin "example.com/zoraxy/helloworld/zoraxy_plugin"
|
||||
plugin "example.com/zoraxy/helloworld/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
|
145
example/plugins/helloworld/mod/zoraxy_plugin/dev_webserver.go
Normal file
145
example/plugins/helloworld/mod/zoraxy_plugin/dev_webserver.go
Normal file
@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PluginUiDebugRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetDir string //The directory where the UI files are stored
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
|
||||
// The targetDir is the directory where the UI files are stored (e.g. ./www)
|
||||
// The handlerPrefix is the prefix of the handler used to route this router
|
||||
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
|
||||
// All prefix should not end with a slash
|
||||
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
|
||||
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
|
||||
// if mux is nil, the handler will be registered to http.DefaultServeMux
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
162
example/plugins/helloworld/mod/zoraxy_plugin/dynamic_router.go
Normal file
162
example/plugins/helloworld/mod/zoraxy_plugin/dynamic_router.go
Normal file
@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
|
||||
You can decide to accept or skip the request based on the request header and paths
|
||||
*/
|
||||
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
Sniffing and forwarding
|
||||
|
||||
The following functions are here to help with
|
||||
sniffing and forwarding requests to the dynamic
|
||||
router.
|
||||
*/
|
||||
// A custom request object to be used in the dynamic sniffing
|
||||
type DynamicSniffForwardRequest struct {
|
||||
Method string `json:"method"`
|
||||
Hostname string `json:"hostname"`
|
||||
URL string `json:"url"`
|
||||
Header map[string][]string `json:"header"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Host string `json:"host"`
|
||||
RequestURI string `json:"request_uri"`
|
||||
Proto string `json:"proto"`
|
||||
ProtoMajor int `json:"proto_major"`
|
||||
ProtoMinor int `json:"proto_minor"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
@ -12,12 +12,12 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
PluginID string //The ID of the plugin
|
||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
||||
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
|
||||
@ -58,11 +58,6 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
//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, "/")
|
||||
@ -75,8 +70,24 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body))
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
@ -89,11 +100,18 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
@ -126,3 +144,13 @@ func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.Ser
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
105
example/plugins/helloworld/mod/zoraxy_plugin/static_router.go
Normal file
105
example/plugins/helloworld/mod/zoraxy_plugin/static_router.go
Normal file
@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
@ -22,9 +22,9 @@ const (
|
||||
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 StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
type ControlStatusCode int
|
||||
@ -42,8 +42,9 @@ type SubscriptionEvent struct {
|
||||
}
|
||||
|
||||
type RuntimeConstantValue struct {
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
/*
|
||||
@ -72,23 +73,24 @@ type IntroSpect struct {
|
||||
*/
|
||||
|
||||
/*
|
||||
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
|
||||
Static Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
This is faster than dynamic capture, but less flexible
|
||||
*/
|
||||
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)
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
/*
|
||||
Always Capture Settings
|
||||
Dynamic Capture Settings
|
||||
|
||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||
these always applies
|
||||
Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
if the plugin sniff returns 280, the traffic will be captured
|
||||
otherwise, the traffic will be forwarded to the next plugin
|
||||
This is slower than static capture, but more flexible
|
||||
*/
|
||||
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)
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
/* UI Path for your plugin */
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
327
example/plugins/upnp/api.go
Normal file
327
example/plugins/upnp/api.go
Normal file
@ -0,0 +1,327 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
API Handlers
|
||||
*/
|
||||
|
||||
func handleUsableState(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
js, _ := json.Marshal(upnpRouterExists)
|
||||
SendJSONResponse(w, string(js))
|
||||
} else if r.Method == "POST" {
|
||||
//Try to probe the UPnP router again
|
||||
TryStartUPnPClient()
|
||||
if upnpRouterExists {
|
||||
SendOK(w)
|
||||
} else {
|
||||
SendErrorResponse(w, "UPnP router not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get or set the enable state of the plugin
|
||||
func handleEnableState(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
js, _ := json.Marshal(upnpRuntimeConfig.Enabled)
|
||||
SendJSONResponse(w, string(js))
|
||||
} else if r.Method == "POST" {
|
||||
enable, err := PostBool(r, "enable")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !enable {
|
||||
//Close all the port forwards if UPnP client is available
|
||||
if upnpClient != nil {
|
||||
for _, record := range upnpRuntimeConfig.ForwardRules {
|
||||
err = upnpClient.ClosePort(record.PortNumber)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if upnpClient == nil {
|
||||
SendErrorResponse(w, "No UPnP router in network")
|
||||
return
|
||||
}
|
||||
|
||||
//Forward all the ports if UPnP client is available
|
||||
if upnpClient != nil {
|
||||
for _, record := range upnpRuntimeConfig.ForwardRules {
|
||||
err = upnpClient.ForwardPort(record.PortNumber, record.RuleName)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upnpRuntimeConfig.Enabled = enable
|
||||
SaveRuntimeConfig()
|
||||
}
|
||||
}
|
||||
|
||||
func handleForwardPortEdit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
port, err := PostInt(r, "port")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
oldPort, err := PostInt(r, "oldPort")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
name, err := PostPara(r, "name")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if port < 1 || port > 65535 {
|
||||
SendErrorResponse(w, "invalid port number")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the old port exists
|
||||
found := false
|
||||
for _, record := range upnpRuntimeConfig.ForwardRules {
|
||||
if record.PortNumber == oldPort {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
SendErrorResponse(w, "editing forward rule not found")
|
||||
return
|
||||
}
|
||||
|
||||
//Delete the old port forward
|
||||
if oldPort != port && upnpClient != nil {
|
||||
//Remove the port forward if UPnP client is available
|
||||
err = upnpClient.ClosePort(oldPort)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Remove from runtime config
|
||||
for i, record := range upnpRuntimeConfig.ForwardRules {
|
||||
if record.PortNumber == oldPort {
|
||||
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules[:i], upnpRuntimeConfig.ForwardRules[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//Create the new forward rule
|
||||
if upnpClient != nil {
|
||||
//Forward the port if UPnP client is available
|
||||
err = upnpClient.ForwardPort(port, name)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Add to runtime config
|
||||
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules, &PortForwardRecord{
|
||||
RuleName: name,
|
||||
PortNumber: port,
|
||||
})
|
||||
|
||||
//Save the runtime config
|
||||
SaveRuntimeConfig()
|
||||
SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a port forward
|
||||
func handleForwardPortRemove(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
port, err := PostInt(r, "port")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if upnpClient != nil {
|
||||
//Remove the port forward if UPnP client is available
|
||||
err = upnpClient.ClosePort(port)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Remove from runtime config
|
||||
for i, record := range upnpRuntimeConfig.ForwardRules {
|
||||
if record.PortNumber == port {
|
||||
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules[:i], upnpRuntimeConfig.ForwardRules[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//Save the runtime config
|
||||
SaveRuntimeConfig()
|
||||
SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the port forward operations
|
||||
func handleForwardPort(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
// List all the forwarded ports
|
||||
js, _ := json.Marshal(upnpRuntimeConfig.ForwardRules)
|
||||
SendJSONResponse(w, string(js))
|
||||
} else if r.Method == "POST" {
|
||||
//Add a new port forward
|
||||
port, err := PostInt(r, "port")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
name, err := PostPara(r, "name")
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if port < 1 || port > 65535 {
|
||||
SendErrorResponse(w, "invalid port number")
|
||||
return
|
||||
}
|
||||
|
||||
if upnpClient != nil {
|
||||
//Forward the port if UPnP client is available
|
||||
err = upnpClient.ForwardPort(port, name)
|
||||
if err != nil {
|
||||
SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Add to runtime config
|
||||
upnpRuntimeConfig.ForwardRules = append(upnpRuntimeConfig.ForwardRules, &PortForwardRecord{
|
||||
RuleName: name,
|
||||
PortNumber: port,
|
||||
})
|
||||
|
||||
//Save the runtime config
|
||||
SaveRuntimeConfig()
|
||||
SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Network Utilities
|
||||
*/
|
||||
|
||||
// Send JSON response, with an extra json header
|
||||
func SendJSONResponse(w http.ResponseWriter, json string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(json))
|
||||
}
|
||||
|
||||
func SendErrorResponse(w http.ResponseWriter, errMsg string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
|
||||
}
|
||||
|
||||
func SendOK(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("\"OK\""))
|
||||
}
|
||||
|
||||
// Get GET parameter
|
||||
func GetPara(r *http.Request, key string) (string, error) {
|
||||
// Get first value from the URL query
|
||||
value := r.URL.Query().Get(key)
|
||||
if len(value) == 0 {
|
||||
return "", errors.New("invalid " + key + " given")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Get GET paramter as boolean, accept 1 or true
|
||||
func GetBool(r *http.Request, key string) (bool, error) {
|
||||
x, err := GetPara(r, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Convert to lowercase and trim spaces just once to compare
|
||||
switch strings.ToLower(strings.TrimSpace(x)) {
|
||||
case "1", "true", "on":
|
||||
return true, nil
|
||||
case "0", "false", "off":
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.New("invalid boolean given")
|
||||
}
|
||||
|
||||
// Get POST parameter
|
||||
func PostPara(r *http.Request, key string) (string, error) {
|
||||
// Try to parse the form
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Get first value from the form
|
||||
x := r.Form.Get(key)
|
||||
if len(x) == 0 {
|
||||
return "", errors.New("invalid " + key + " given")
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// Get POST paramter as boolean, accept 1 or true
|
||||
func PostBool(r *http.Request, key string) (bool, error) {
|
||||
x, err := PostPara(r, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Convert to lowercase and trim spaces just once to compare
|
||||
switch strings.ToLower(strings.TrimSpace(x)) {
|
||||
case "1", "true", "on":
|
||||
return true, nil
|
||||
case "0", "false", "off":
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, errors.New("invalid boolean given")
|
||||
}
|
||||
|
||||
// Get POST paramter as int
|
||||
func PostInt(r *http.Request, key string) (int, error) {
|
||||
x, err := PostPara(r, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
x = strings.TrimSpace(x)
|
||||
rx, err := strconv.Atoi(x)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return rx, nil
|
||||
}
|
13
example/plugins/upnp/go.mod
Normal file
13
example/plugins/upnp/go.mod
Normal file
@ -0,0 +1,13 @@
|
||||
module plugins.zoraxy.aroz.org/zoraxy/upnp
|
||||
|
||||
go 1.23.6
|
||||
|
||||
require gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6
|
||||
|
||||
require (
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 // indirect
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
17
example/plugins/upnp/go.sum
Normal file
17
example/plugins/upnp/go.sum
Normal file
@ -0,0 +1,17 @@
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 h1:dizWJqTWjwyD8KGcMOwgrkqu1JIkofYgKkmDeNE7oAs=
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA=
|
||||
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6 h1:WKij6HF8ECp9E7K0E44dew9NrRDGiNR5u4EFsXnJUx4=
|
||||
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6/go.mod h1:vhrHTGDh4YR7wK8Z+kRJ+x8SF/6RUM3Vb64Si5FD0L8=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
BIN
example/plugins/upnp/icon.png
Normal file
BIN
example/plugins/upnp/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.8 KiB |
BIN
example/plugins/upnp/icon.psd
Normal file
BIN
example/plugins/upnp/icon.psd
Normal file
Binary file not shown.
194
example/plugins/upnp/main.go
Normal file
194
example/plugins/upnp/main.go
Normal file
@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"plugins.zoraxy.aroz.org/zoraxy/upnp/mod/upnpc"
|
||||
plugin "plugins.zoraxy.aroz.org/zoraxy/upnp/mod/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PLUGIN_ID = "org.aroz.zoraxy.plugins.upnp"
|
||||
UI_PATH = "/ui"
|
||||
WEB_ROOT = "/www"
|
||||
CONFIG_FILE = "upnp.json"
|
||||
AUTO_RENEW_INTERVAL = 12 * 60 * 60 // 12 hours
|
||||
)
|
||||
|
||||
type PortForwardRecord struct {
|
||||
RuleName string
|
||||
PortNumber int
|
||||
}
|
||||
|
||||
type UPnPConfig struct {
|
||||
ForwardRules []*PortForwardRecord
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
//go:embed www/*
|
||||
var content embed.FS
|
||||
|
||||
// Runtime variables
|
||||
var (
|
||||
upnpRouterExists bool = false
|
||||
upnpRuntimeConfig *UPnPConfig = &UPnPConfig{
|
||||
ForwardRules: []*PortForwardRecord{},
|
||||
Enabled: false,
|
||||
}
|
||||
upnpClient *upnpc.UPnPClient = nil
|
||||
renewTickerStop chan bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
//Handle introspect
|
||||
runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
|
||||
ID: PLUGIN_ID,
|
||||
Name: "UPnP Forwarder",
|
||||
Author: "aroz.org",
|
||||
AuthorContact: "https://github.com/aroz-online",
|
||||
Description: "A UPnP Port Forwarder Plugin for Zoraxy",
|
||||
URL: "https://github.com/aroz-online",
|
||||
Type: plugin.PluginType_Utilities,
|
||||
VersionMajor: 1,
|
||||
VersionMinor: 0,
|
||||
VersionPatch: 0,
|
||||
UIPath: UI_PATH,
|
||||
})
|
||||
if err != nil {
|
||||
//Terminate or enter standalone mode here
|
||||
fmt.Println("This is a plugin for Zoraxy and should not be run standalone\n Visit zoraxy.aroz.org to download Zoraxy.")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Read the configuration from file
|
||||
if _, err := os.Stat(CONFIG_FILE); os.IsNotExist(err) {
|
||||
err = os.WriteFile(CONFIG_FILE, []byte("{}"), 0644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
cfgBytes, err := os.ReadFile(CONFIG_FILE)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Load the configuration
|
||||
err = json.Unmarshal(cfgBytes, &upnpRuntimeConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Start upnp client and auto-renew ticker
|
||||
go func() {
|
||||
TryStartUPnPClient()
|
||||
}()
|
||||
|
||||
//Serve the plugin UI
|
||||
embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
|
||||
// For debugging, use the following line instead
|
||||
//embedWebRouter := plugin.NewPluginFileSystemUIRouter(PLUGIN_ID, "."+WEB_ROOT, UI_PATH)
|
||||
//embedWebRouter.EnableDebug = true
|
||||
embedWebRouter.RegisterTerminateHandler(func() {
|
||||
if renewTickerStop != nil {
|
||||
renewTickerStop <- true
|
||||
}
|
||||
// Do cleanup here if needed
|
||||
upnpClient.Close()
|
||||
}, nil)
|
||||
embedWebRouter.AttachHandlerToMux(nil)
|
||||
|
||||
//Serve the API
|
||||
RegisterAPIs()
|
||||
|
||||
//Start the IO server
|
||||
fmt.Println("UPnP Forwarder started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
|
||||
err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAPIs registers the APIs for the plugin
|
||||
func RegisterAPIs() {
|
||||
http.HandleFunc(UI_PATH+"/api/usable", handleUsableState)
|
||||
http.HandleFunc(UI_PATH+"/api/enable", handleEnableState)
|
||||
http.HandleFunc(UI_PATH+"/api/forward", handleForwardPort)
|
||||
http.HandleFunc(UI_PATH+"/api/edit", handleForwardPortEdit)
|
||||
http.HandleFunc(UI_PATH+"/api/remove", handleForwardPortRemove)
|
||||
}
|
||||
|
||||
// TryStartUPnPClient tries to start the UPnP client
|
||||
func TryStartUPnPClient() {
|
||||
if renewTickerStop != nil {
|
||||
renewTickerStop <- true
|
||||
}
|
||||
|
||||
// Create UPnP client
|
||||
upnpClient, err := upnpc.NewUPNPClient()
|
||||
if err != nil {
|
||||
upnpRouterExists = false
|
||||
upnpRuntimeConfig.Enabled = false
|
||||
fmt.Println("UPnP router not found")
|
||||
SaveRuntimeConfig()
|
||||
return
|
||||
}
|
||||
upnpRouterExists = true
|
||||
|
||||
//Check if the client is enabled by default
|
||||
if upnpRuntimeConfig.Enabled {
|
||||
// Forward all the ports
|
||||
for _, rule := range upnpRuntimeConfig.ForwardRules {
|
||||
err = upnpClient.ForwardPort(rule.PortNumber, rule.RuleName)
|
||||
if err != nil {
|
||||
fmt.Println("Unable to forward port", rule.PortNumber, ":", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the auto-renew ticker
|
||||
_, renewTickerStop = SetupAutoRenewTicker()
|
||||
}
|
||||
|
||||
// SetupAutoRenewTicker sets up a ticker for auto-renewing the port forwarding rules
|
||||
func SetupAutoRenewTicker() (*time.Ticker, chan bool) {
|
||||
ticker := time.NewTicker(AUTO_RENEW_INTERVAL * time.Second)
|
||||
closeChan := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-closeChan:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
if upnpClient != nil {
|
||||
upnpClient.RenewForwardRules()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ticker, closeChan
|
||||
}
|
||||
|
||||
// SaveRuntimeConfig saves the runtime configuration to file
|
||||
func SaveRuntimeConfig() error {
|
||||
cfgBytes, err := json.Marshal(upnpRuntimeConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(CONFIG_FILE, cfgBytes, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
135
example/plugins/upnp/mod/upnpc/upnpc.go
Normal file
135
example/plugins/upnp/mod/upnpc/upnpc.go
Normal file
@ -0,0 +1,135 @@
|
||||
package upnpc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitlab.com/NebulousLabs/go-upnp"
|
||||
)
|
||||
|
||||
/*
|
||||
uPNP Module
|
||||
|
||||
This module handles uPNP Connections to the gateway router and create a port forward entry
|
||||
for the host system at the given port (set with -port paramter)
|
||||
*/
|
||||
|
||||
type UPnPClient struct {
|
||||
Connection *upnp.IGD //UPnP conenction object
|
||||
ExternalIP string //Storage of external IP address
|
||||
RequiredPorts []int //All the required ports will be recored
|
||||
PolicyNames sync.Map //Name for the required port nubmer
|
||||
}
|
||||
|
||||
// NewUPNPClient creates a new UPnPClient object
|
||||
func NewUPNPClient() (*UPnPClient, error) {
|
||||
//Create uPNP forwarding in the NAT router
|
||||
fmt.Println("Discovering UPnP router in Local Area Network...")
|
||||
d, err := upnp.Discover()
|
||||
if err != nil {
|
||||
return &UPnPClient{}, err
|
||||
}
|
||||
|
||||
// discover external IP
|
||||
ip, err := d.ExternalIP()
|
||||
if err != nil {
|
||||
return &UPnPClient{}, err
|
||||
}
|
||||
|
||||
//Create the final obejcts
|
||||
newUPnPObject := &UPnPClient{
|
||||
Connection: d,
|
||||
ExternalIP: ip,
|
||||
RequiredPorts: []int{},
|
||||
}
|
||||
|
||||
return newUPnPObject, nil
|
||||
}
|
||||
|
||||
// ForwardPort forwards a port to the host
|
||||
func (u *UPnPClient) ForwardPort(portNumber int, ruleName string) error {
|
||||
fmt.Println("UPnP forwarding new port: ", portNumber, "for "+ruleName+" service")
|
||||
|
||||
//Check if port already forwarded
|
||||
_, ok := u.PolicyNames.Load(portNumber)
|
||||
if ok {
|
||||
//Port already forward. Ignore this request
|
||||
return errors.New("port already forwarded")
|
||||
}
|
||||
|
||||
// forward a port
|
||||
err := u.Connection.Forward(uint16(portNumber), ruleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.RequiredPorts = append(u.RequiredPorts, portNumber)
|
||||
u.PolicyNames.Store(portNumber, ruleName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClosePort closes the port forwarding
|
||||
func (u *UPnPClient) ClosePort(portNumber int) error {
|
||||
//Check if port is opened
|
||||
portOpened := false
|
||||
newRequiredPort := []int{}
|
||||
for _, thisPort := range u.RequiredPorts {
|
||||
if thisPort != portNumber {
|
||||
newRequiredPort = append(newRequiredPort, thisPort)
|
||||
} else {
|
||||
portOpened = true
|
||||
}
|
||||
}
|
||||
|
||||
if portOpened {
|
||||
//Update the port list
|
||||
u.RequiredPorts = newRequiredPort
|
||||
|
||||
// Close the port
|
||||
fmt.Println("Closing UPnP Port Forward: ", portNumber)
|
||||
err := u.Connection.Clear(uint16(portNumber))
|
||||
|
||||
//Delete the name registry
|
||||
u.PolicyNames.Delete(portNumber)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Renew forward rules, prevent router lease time from flushing the Upnp config
|
||||
func (u *UPnPClient) RenewForwardRules() {
|
||||
if u.Connection == nil {
|
||||
//UPnP router gone
|
||||
return
|
||||
}
|
||||
portsToRenew := u.RequiredPorts
|
||||
for _, thisPort := range portsToRenew {
|
||||
ruleName, ok := u.PolicyNames.Load(thisPort)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
u.ClosePort(thisPort)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
u.ForwardPort(thisPort, ruleName.(string))
|
||||
}
|
||||
fmt.Println("UPnP Port Forward rule renew completed")
|
||||
}
|
||||
|
||||
func (u *UPnPClient) Close() error {
|
||||
//Shutdown the default UPnP Object
|
||||
if u != nil {
|
||||
for _, portNumber := range u.RequiredPorts {
|
||||
err := u.Connection.Clear(uint16(portNumber))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
19
example/plugins/upnp/mod/zoraxy_plugin/README.txt
Normal file
19
example/plugins/upnp/mod/zoraxy_plugin/README.txt
Normal 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
|
145
example/plugins/upnp/mod/zoraxy_plugin/dev_webserver.go
Normal file
145
example/plugins/upnp/mod/zoraxy_plugin/dev_webserver.go
Normal file
@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PluginUiDebugRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetDir string //The directory where the UI files are stored
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
|
||||
// The targetDir is the directory where the UI files are stored (e.g. ./www)
|
||||
// The handlerPrefix is the prefix of the handler used to route this router
|
||||
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
|
||||
// All prefix should not end with a slash
|
||||
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
|
||||
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
|
||||
// if mux is nil, the handler will be registered to http.DefaultServeMux
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
162
example/plugins/upnp/mod/zoraxy_plugin/dynamic_router.go
Normal file
162
example/plugins/upnp/mod/zoraxy_plugin/dynamic_router.go
Normal file
@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
|
||||
You can decide to accept or skip the request based on the request header and paths
|
||||
*/
|
||||
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
Sniffing and forwarding
|
||||
|
||||
The following functions are here to help with
|
||||
sniffing and forwarding requests to the dynamic
|
||||
router.
|
||||
*/
|
||||
// A custom request object to be used in the dynamic sniffing
|
||||
type DynamicSniffForwardRequest struct {
|
||||
Method string `json:"method"`
|
||||
Hostname string `json:"hostname"`
|
||||
URL string `json:"url"`
|
||||
Header map[string][]string `json:"header"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Host string `json:"host"`
|
||||
RequestURI string `json:"request_uri"`
|
||||
Proto string `json:"proto"`
|
||||
ProtoMajor int `json:"proto_major"`
|
||||
ProtoMinor int `json:"proto_minor"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
156
example/plugins/upnp/mod/zoraxy_plugin/embed_webserver.go
Normal file
156
example/plugins/upnp/mod/zoraxy_plugin/embed_webserver.go
Normal file
@ -0,0 +1,156 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PluginUiRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
||||
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
|
||||
// The targetFsPrefix is the prefix of the embed.FS where the UI files are stored
|
||||
// The targetFsPrefix should be relative to the root of the embed.FS
|
||||
// The targetFsPrefix should start with a slash (e.g. /web) that corresponds to the root folder of the embed.FS
|
||||
// The handlerPrefix is the prefix of the handler used to route this router
|
||||
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
|
||||
// All prefix should not end with a slash
|
||||
func NewPluginEmbedUIRouter(pluginID string, targetFs *embed.FS, targetFsPrefix string, handlerPrefix string) *PluginUiRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(targetFsPrefix, "/") {
|
||||
targetFsPrefix = "/" + targetFsPrefix
|
||||
}
|
||||
targetFsPrefix = strings.TrimSuffix(targetFsPrefix, "/")
|
||||
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiRouter{
|
||||
PluginID: pluginID,
|
||||
TargetFs: targetFs,
|
||||
TargetFsPrefix: targetFsPrefix,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from embed.FS
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetFsPrefix + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := fs.ReadFile(*p.TargetFs, targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, http.FileServer(http.FS(subFS))).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
|
||||
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
|
||||
// if mux is nil, the handler will be registered to http.DefaultServeMux
|
||||
func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
105
example/plugins/upnp/mod/zoraxy_plugin/static_router.go
Normal file
105
example/plugins/upnp/mod/zoraxy_plugin/static_router.go
Normal file
@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
176
example/plugins/upnp/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
176
example/plugins/upnp/mod/zoraxy_plugin/zoraxy_plugin.go
Normal file
@ -0,0 +1,176 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugins Includes.go
|
||||
|
||||
This file is copied from Zoraxy source code
|
||||
You can always find the latest version under mod/plugins/includes.go
|
||||
Usually this file are backward compatible
|
||||
*/
|
||||
|
||||
type PluginType int
|
||||
|
||||
const (
|
||||
PluginType_Router PluginType = 0 //Router Plugin, used for handling / routing / forwarding traffic
|
||||
PluginType_Utilities PluginType = 1 //Utilities Plugin, used for utilities like Zerotier or Static Web Server that do not require interception with the dpcore
|
||||
)
|
||||
|
||||
type StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
type ControlStatusCode int
|
||||
|
||||
const (
|
||||
ControlStatusCode_CAPTURED ControlStatusCode = 280 //Traffic captured by plugin, ask Zoraxy not to process the traffic
|
||||
ControlStatusCode_UNHANDLED ControlStatusCode = 284 //Traffic not handled by plugin, ask Zoraxy to process the traffic
|
||||
ControlStatusCode_ERROR ControlStatusCode = 580 //Error occurred while processing the traffic, ask Zoraxy to process the traffic and log the error
|
||||
)
|
||||
|
||||
type SubscriptionEvent struct {
|
||||
EventName string `json:"event_name"`
|
||||
EventSource string `json:"event_source"`
|
||||
Payload string `json:"payload"` //Payload of the event, can be empty
|
||||
}
|
||||
|
||||
type RuntimeConstantValue struct {
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
/*
|
||||
IntroSpect Payload
|
||||
|
||||
When the plugin is initialized with -introspect flag,
|
||||
the plugin shell return this payload as JSON and exit
|
||||
*/
|
||||
type IntroSpect struct {
|
||||
/* Plugin metadata */
|
||||
ID string `json:"id"` //Unique ID of your plugin, recommended using your own domain in reverse like com.yourdomain.pluginname
|
||||
Name string `json:"name"` //Name of your plugin
|
||||
Author string `json:"author"` //Author name of your plugin
|
||||
AuthorContact string `json:"author_contact"` //Author contact of your plugin, like email
|
||||
Description string `json:"description"` //Description of your plugin
|
||||
URL string `json:"url"` //URL of your plugin
|
||||
Type PluginType `json:"type"` //Type of your plugin, Router(0) or Utilities(1)
|
||||
VersionMajor int `json:"version_major"` //Major version of your plugin
|
||||
VersionMinor int `json:"version_minor"` //Minor version of your plugin
|
||||
VersionPatch int `json:"version_patch"` //Patch version of your plugin
|
||||
|
||||
/*
|
||||
|
||||
Endpoint Settings
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Static Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
This is faster than dynamic capture, but less flexible
|
||||
*/
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
/*
|
||||
Dynamic Capture Settings
|
||||
|
||||
Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
if the plugin sniff returns 280, the traffic will be captured
|
||||
otherwise, the traffic will be forwarded to the next plugin
|
||||
This is slower than static capture, but more flexible
|
||||
*/
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
/* UI Path for your plugin */
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||
|
||||
/* Subscriptions Settings */
|
||||
SubscriptionPath string `json:"subscription_path"` //Subscription event path of your plugin (e.g. /notifyme), a POST request with SubscriptionEvent as body will be sent to this path when the event is triggered
|
||||
SubscriptionsEvents map[string]string `json:"subscriptions_events"` //Subscriptions events of your plugin, see Zoraxy documentation for more details
|
||||
}
|
||||
|
||||
/*
|
||||
ServeIntroSpect Function
|
||||
|
||||
This function will check if the plugin is initialized with -introspect flag,
|
||||
if so, it will print the intro spect and exit
|
||||
|
||||
Place this function at the beginning of your plugin main function
|
||||
*/
|
||||
func ServeIntroSpect(pluginSpect *IntroSpect) {
|
||||
if len(os.Args) > 1 && os.Args[1] == "-introspect" {
|
||||
//Print the intro spect and exit
|
||||
jsonData, _ := json.MarshalIndent(pluginSpect, "", " ")
|
||||
fmt.Println(string(jsonData))
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ConfigureSpec Payload
|
||||
|
||||
Zoraxy will start your plugin with -configure flag,
|
||||
the plugin shell read this payload as JSON and configure itself
|
||||
by the supplied values like starting a web server at given port
|
||||
that listens to 127.0.0.1:port
|
||||
*/
|
||||
type ConfigureSpec struct {
|
||||
Port int `json:"port"` //Port to listen
|
||||
RuntimeConst RuntimeConstantValue `json:"runtime_const"` //Runtime constant values
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
/*
|
||||
RecvExecuteConfigureSpec Function
|
||||
|
||||
This function will read the configure spec from Zoraxy
|
||||
and return the ConfigureSpec object
|
||||
|
||||
Place this function after ServeIntroSpect function in your plugin main function
|
||||
*/
|
||||
func RecvConfigureSpec() (*ConfigureSpec, error) {
|
||||
for i, arg := range os.Args {
|
||||
if strings.HasPrefix(arg, "-configure=") {
|
||||
var configSpec ConfigureSpec
|
||||
if err := json.Unmarshal([]byte(arg[11:]), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &configSpec, nil
|
||||
} else if arg == "-configure" {
|
||||
var configSpec ConfigureSpec
|
||||
var nextArg string
|
||||
if len(os.Args) > i+1 {
|
||||
nextArg = os.Args[i+1]
|
||||
if err := json.Unmarshal([]byte(nextArg), &configSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("No port specified after -configure flag")
|
||||
}
|
||||
return &configSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("No -configure flag found")
|
||||
}
|
||||
|
||||
/*
|
||||
ServeAndRecvSpec Function
|
||||
|
||||
This function will serve the intro spect and return the configure spec
|
||||
See the ServeIntroSpect and RecvConfigureSpec for more details
|
||||
*/
|
||||
func ServeAndRecvSpec(pluginSpect *IntroSpect) (*ConfigureSpec, error) {
|
||||
ServeIntroSpect(pluginSpect)
|
||||
return RecvConfigureSpec()
|
||||
}
|
1
example/plugins/upnp/upnp.json
Normal file
1
example/plugins/upnp/upnp.json
Normal file
@ -0,0 +1 @@
|
||||
{"ForwardRules":[],"Enabled":false}
|
302
example/plugins/upnp/www/index.html
Normal file
302
example/plugins/upnp/www/index.html
Normal file
@ -0,0 +1,302 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- CSRF token, if your plugin need to make POST request to backend -->
|
||||
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
|
||||
<title>UPnP Port Forwarder | Zoraxy</title>
|
||||
<link rel="stylesheet" href="/script/semantic/semantic.min.css">
|
||||
<script src="/script/jquery-3.6.0.min.js"></script>
|
||||
<script src="/script/semantic/semantic.min.js"></script>
|
||||
<script src="/script/utils.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<style>
|
||||
body {
|
||||
background:none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Dark theme script must be included after body tag-->
|
||||
<link rel="stylesheet" href="/darktheme.css">
|
||||
<script src="/script/darktheme.js"></script>
|
||||
<div id="upnpForwarder" class="standardContainer">
|
||||
<div id="upnpRouterNotFoundWarning" class="ui basic segment" style="display: none;">
|
||||
<h2>UPnP Port Forwarder</h2>
|
||||
<p>Port forward using UPnP protocol, only works with some of the supported gateway routers</p>
|
||||
</div>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui message">
|
||||
<div class="header"><i class="yellow exclamation triangle icon"></i> UPnP Gateway Not Found</div>
|
||||
<p>No UPnP router found in network. Please ensure your router supports UPnP and is enabled.</p>
|
||||
<button id="retryBtn" onclick="searchUpnpRouter();" class="ui basic small button"><i class="green refresh icon"></i> Search again</button>
|
||||
</div>
|
||||
<div class="ui basic segment">
|
||||
<h3 class="ui header">UPnP Port Forwarder</h3>
|
||||
<div class="ui toggle checkbox">
|
||||
<input type="checkbox" id="upnpToggle" onchange="toggleUpnpState();">
|
||||
<label>Enable UPnP Forwarding</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<table class="ui celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rule Name</th>
|
||||
<th>Forwarded Port</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="forwardList">
|
||||
<tr>
|
||||
<td>Example Rule</td>
|
||||
<td>8080</td>
|
||||
<td>
|
||||
<button class="ui button">Edit</button>
|
||||
<button class="ui button">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui divider"></div>
|
||||
<div>
|
||||
<h4>Port Forward Rules</h4>
|
||||
<form class="ui form" id="addRuleForm">
|
||||
<div class="field">
|
||||
<label>Rule Name</label>
|
||||
<input type="text" name="ruleName" placeholder="Rule Name">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Port</label>
|
||||
<input type="number" name="port" placeholder="Port" min="1" max="65535">
|
||||
</div>
|
||||
<button onclick="handleAddForward(event);" class="ui small basic button"><i class="ui blue add icon"></i> Add Rule</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
function toggleUpnpState() {
|
||||
let isChecked = $("#upnpToggle").prop("checked");
|
||||
$.cjax({
|
||||
url: './api/toggle',
|
||||
method: "POST",
|
||||
data: {
|
||||
enable: isChecked
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined) {
|
||||
// Error
|
||||
parent.msgbox(data.error, false);
|
||||
} else {
|
||||
// Success
|
||||
parent.msgbox("UPnP Forwarding " + (isChecked ? "enabled" : "disabled"), true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initUPnPEnableState(){
|
||||
$.cjax({
|
||||
url: './api/enable',
|
||||
success: function(data) {
|
||||
if (data == true){
|
||||
//Upnp forwarding enabled
|
||||
$("#upnpToggle").prop("checked", true);
|
||||
}else{
|
||||
//Upnp forwarding disabled
|
||||
$("#upnpToggle").prop("checked", false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
initUPnPEnableState();
|
||||
|
||||
function searchUpnpRouter(){
|
||||
$("#retryBtn").addClass("loading");
|
||||
parent.msgbox("Searching for UPnP router (will take a few minutes)...", true);
|
||||
$.cjax({
|
||||
url: './api/usable',
|
||||
method: "POST",
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Not found
|
||||
parent.msgbox("UPnP router not found", false);
|
||||
}else{
|
||||
//Found
|
||||
parent.msgbox("UPnP router discovered", true);
|
||||
}
|
||||
initUpnpUsableState();
|
||||
$("#retryBtn").removeClass("loading");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Check if UPnP is usable
|
||||
function initUpnpUsableState(){
|
||||
$.cjax({
|
||||
url: './api/usable',
|
||||
success: function(data) {
|
||||
if (data == true){
|
||||
//Upnp router found in network, enable the page
|
||||
$('#upnpRouterNotFoundWarning').hide();
|
||||
}else{
|
||||
//No upnp router found in network, disable the page
|
||||
$('#upnpForwarder').addClass('disabled');
|
||||
$('#upnpRouterNotFoundWarning').show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleAddForward(event){
|
||||
event.preventDefault();
|
||||
let ruleName = $("#addRuleForm input[name='ruleName']").val();
|
||||
let port = $("#addRuleForm input[name='port']").val();
|
||||
if (ruleName == "" || port == ""){
|
||||
parent.msgbox("Please fill in all fields", false);
|
||||
return;
|
||||
}
|
||||
$.cjax({
|
||||
url: './api/forward',
|
||||
method: "POST",
|
||||
data: {
|
||||
name: ruleName,
|
||||
port: port
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Error
|
||||
parent.msgbox(data.error, false);
|
||||
}else{
|
||||
//Success
|
||||
parent.msgbox("Rule added successfully", true);
|
||||
initForwardList();
|
||||
}
|
||||
|
||||
$("#addRuleForm input[name='ruleName']").val('');
|
||||
$("#addRuleForm input[name='port']").val('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleRemoveForward(portNo){
|
||||
$.cjax({
|
||||
url: './api/remove',
|
||||
method: "POST",
|
||||
data: {
|
||||
port: portNo
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Error
|
||||
parent.msgbox(data.error, false);
|
||||
}else{
|
||||
//Success
|
||||
parent.msgbox("Rule removed successfully", true);
|
||||
initForwardList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editForwardRule(row){
|
||||
let ruleName = $(row).closest('tr').find('td:eq(0)').text();
|
||||
let portNumber = $(row).closest('tr').find('td:eq(1)').text();
|
||||
$(row).closest('tr').html(`
|
||||
<td>
|
||||
<div class="ui fluid input">
|
||||
<input type="text" value="${ruleName}" onkeypress="if(event.key === 'Enter') $(this).closest('tr').find('td:eq(1) input').focus();">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="ui fluid input">
|
||||
<input type="number" value="${portNumber}" class="ui input" min="1" max="65535" onkeypress="if(event.key === 'Enter') saveForwardRule(this, '${portNumber}');">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="saveForwardRule(this, '${portNumber}');" class="ui basic small circular icon button"><i class="ui green save icon"></i></button>
|
||||
<button onclick="cancelEditForwardRule(this, '${ruleName}', '${portNumber}');" class="ui basic small circular icon button"><i class="ui cancel icon"></i></button>
|
||||
</td>
|
||||
`);
|
||||
}
|
||||
|
||||
function cancelEditForwardRule(){
|
||||
initForwardList();
|
||||
}
|
||||
|
||||
function saveForwardRule(row, portNo){
|
||||
let ruleName = $(row).closest('tr').find('td:eq(0) input').val();
|
||||
let newPortNo = $(row).closest('tr').find('td:eq(1) input').val();
|
||||
if (ruleName == "" || newPortNo == ""){
|
||||
parent.msgbox("Please fill in all fields", false);
|
||||
return;
|
||||
}
|
||||
$.cjax({
|
||||
url: './api/edit',
|
||||
method: "POST",
|
||||
data: {
|
||||
name: ruleName,
|
||||
port: newPortNo,
|
||||
oldPort: portNo
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Error
|
||||
parent.msgbox(data.error, false);
|
||||
}else{
|
||||
//Success
|
||||
parent.msgbox("Rule updated successfully", true);
|
||||
initForwardList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Load forward list
|
||||
function initForwardList(){
|
||||
$("#forwardList").html('<tr><td colspan="3"> <i class="ui loading spinner icon"></i> Loading...</tr>');
|
||||
$.cjax({
|
||||
url: './api/forward',
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
//Error
|
||||
parent.msgbox(data.error, false);
|
||||
}else{
|
||||
//Success
|
||||
$("#forwardList").empty();
|
||||
let rows = '';
|
||||
data.forEach(row => {
|
||||
$("#forwardList").append(`
|
||||
<tr>
|
||||
<td>${row.RuleName}</td>
|
||||
<td>${row.PortNumber}</td>
|
||||
<td>
|
||||
<button onclick="editForwardRule(this);" class="ui basic small circular icon button"><i class="ui edit icon"></i></button>
|
||||
<button onclick="handleRemoveForward(${row.PortNumber});" class="ui basic small red circular icon button"><i class="ui red trash icon"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
|
||||
if (data == null || data.length == 0){
|
||||
//No rules
|
||||
$("#forwardList").append(`
|
||||
<tr>
|
||||
<td colspan="3"><i class="ui green check circle icon"></i> No running port forward rules</td>
|
||||
</tr>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
initUpnpUsableState();
|
||||
initForwardList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -53,6 +53,7 @@ func main() {
|
||||
|
||||
// Create a new PluginEmbedUIRouter that will serve the UI from web folder
|
||||
uiRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, EMBED_FS_ROOT, UI_RELPATH)
|
||||
uiRouter.EnableDebug = true
|
||||
|
||||
// Register the shutdown handler
|
||||
uiRouter.RegisterTerminateHandler(func() {
|
||||
@ -64,7 +65,8 @@ func main() {
|
||||
}, nil)
|
||||
|
||||
// This will serve the index.html file embedded in the binary
|
||||
http.Handle(UI_RELPATH+"/", uiRouter.Handler())
|
||||
targetHandler := uiRouter.Handler()
|
||||
http.Handle(UI_RELPATH+"/", targetHandler)
|
||||
|
||||
// Start the GAN Network Controller
|
||||
err = startGanNetworkController()
|
||||
|
@ -5,12 +5,10 @@ package ganserv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"aroz.org/zoraxy/ztnc/mod/utils"
|
||||
"golang.org/x/sys/windows"
|
||||
@ -46,15 +44,6 @@ func readAuthTokenAsAdmin() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData")
|
||||
retry := 0
|
||||
time.Sleep(3 * time.Second)
|
||||
for !utils.FileExists("./conf/authtoken.secret") && retry < 10 {
|
||||
time.Sleep(3 * time.Second)
|
||||
log.Println("Waiting for ZeroTier authtoken extraction...")
|
||||
retry++
|
||||
}
|
||||
|
||||
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
145
example/plugins/ztnc/mod/zoraxy_plugin/dev_webserver.go
Normal file
145
example/plugins/ztnc/mod/zoraxy_plugin/dev_webserver.go
Normal file
@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PluginUiDebugRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetDir string //The directory where the UI files are stored
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
|
||||
// The targetDir is the directory where the UI files are stored (e.g. ./www)
|
||||
// The handlerPrefix is the prefix of the handler used to route this router
|
||||
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
|
||||
// All prefix should not end with a slash
|
||||
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
|
||||
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
|
||||
// if mux is nil, the handler will be registered to http.DefaultServeMux
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
162
example/plugins/ztnc/mod/zoraxy_plugin/dynamic_router.go
Normal file
162
example/plugins/ztnc/mod/zoraxy_plugin/dynamic_router.go
Normal file
@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
|
||||
You can decide to accept or skip the request based on the request header and paths
|
||||
*/
|
||||
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
Sniffing and forwarding
|
||||
|
||||
The following functions are here to help with
|
||||
sniffing and forwarding requests to the dynamic
|
||||
router.
|
||||
*/
|
||||
// A custom request object to be used in the dynamic sniffing
|
||||
type DynamicSniffForwardRequest struct {
|
||||
Method string `json:"method"`
|
||||
Hostname string `json:"hostname"`
|
||||
URL string `json:"url"`
|
||||
Header map[string][]string `json:"header"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Host string `json:"host"`
|
||||
RequestURI string `json:"request_uri"`
|
||||
Proto string `json:"proto"`
|
||||
ProtoMajor int `json:"proto_major"`
|
||||
ProtoMinor int `json:"proto_minor"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
@ -12,12 +12,12 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
PluginID string //The ID of the plugin
|
||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
||||
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
|
||||
@ -58,11 +58,6 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
//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, "/")
|
||||
@ -75,8 +70,24 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body))
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
@ -89,11 +100,18 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
@ -126,3 +144,13 @@ func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.Ser
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
||||
|
105
example/plugins/ztnc/mod/zoraxy_plugin/static_router.go
Normal file
105
example/plugins/ztnc/mod/zoraxy_plugin/static_router.go
Normal file
@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
@ -22,9 +22,9 @@ const (
|
||||
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 StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
type ControlStatusCode int
|
||||
@ -42,8 +42,9 @@ type SubscriptionEvent struct {
|
||||
}
|
||||
|
||||
type RuntimeConstantValue struct {
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
/*
|
||||
@ -72,23 +73,24 @@ type IntroSpect struct {
|
||||
*/
|
||||
|
||||
/*
|
||||
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
|
||||
Static Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
This is faster than dynamic capture, but less flexible
|
||||
*/
|
||||
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)
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
/*
|
||||
Always Capture Settings
|
||||
Dynamic Capture Settings
|
||||
|
||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||
these always applies
|
||||
Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
if the plugin sniff returns 280, the traffic will be captured
|
||||
otherwise, the traffic will be forwarded to the next plugin
|
||||
This is slower than static capture, but more flexible
|
||||
*/
|
||||
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)
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
/* UI Path for your plugin */
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||
|
@ -226,9 +226,9 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000)
|
||||
parent.msgbox(data.error, false, 5000)
|
||||
}else{
|
||||
msgbox("Network Range Updated")
|
||||
parent.msgbox("Network Range Updated")
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -253,7 +253,7 @@
|
||||
initNetNameAndDesc();
|
||||
if (object != undefined){
|
||||
$(object).removeClass("loading");
|
||||
msgbox("Network Metadata Updated");
|
||||
parent.msgbox("Network Metadata Updated");
|
||||
}
|
||||
$("#gannetDetailEdit").slideUp("fast");
|
||||
}
|
||||
@ -264,7 +264,7 @@
|
||||
//Get the details of the net
|
||||
$.get("./api/gan/network/name?netid=" + currentGANetID, function(data){
|
||||
if (data.error !== undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
$("#gaNetNameInput").val(data[0]);
|
||||
$(".ganetName").html(data[0]);
|
||||
@ -278,7 +278,7 @@
|
||||
//Get the details of the net
|
||||
$.get("./api/gan/network/list?netid=" + currentGANetID, function(data){
|
||||
if (data.error !== undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
currentGaNetDetails = data;
|
||||
highlightCurrentGANetCIDR();
|
||||
@ -299,9 +299,9 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
parent.msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
msgbox("IP removed from member " + memberid)
|
||||
parent.msgbox("IP removed from member " + memberid)
|
||||
}
|
||||
renderMemeberTable();
|
||||
}
|
||||
@ -331,7 +331,7 @@
|
||||
}
|
||||
|
||||
if (!isValidIPv4Address(newip)){
|
||||
msgbox(newip + " is not a valid IPv4 address", false, 5000)
|
||||
parent.msgbox(newip + " is not a valid IPv4 address", false, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
@ -346,9 +346,9 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
parent.msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
msgbox("IP added to member " + memberid)
|
||||
parent.msgbox("IP added to member " + memberid)
|
||||
}
|
||||
renderMemeberTable();
|
||||
}
|
||||
@ -482,7 +482,7 @@
|
||||
|
||||
function renameMember(targetMemberAddr){
|
||||
if (targetMemberAddr == ""){
|
||||
msgbox("Member address cannot be empty", false, 5000)
|
||||
parent.msgbox("Member address cannot be empty", false, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
@ -498,9 +498,9 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Member Name Updated");
|
||||
parent.msgbox("Member Name Updated");
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
}
|
||||
@ -564,12 +564,12 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
if (isAuthed){
|
||||
msgbox("Member Authorized");
|
||||
parent.msgbox("Member Authorized");
|
||||
}else{
|
||||
msgbox("Member Deauthorized");
|
||||
parent.msgbox("Member Deauthorized");
|
||||
}
|
||||
|
||||
}
|
||||
@ -580,25 +580,26 @@
|
||||
}
|
||||
|
||||
function handleMemberDelete(addr){
|
||||
if (confirm("Confirm delete member " + addr + " ?")){
|
||||
$.cjax({
|
||||
url: "./api/gan/members/delete",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
memid: addr,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Member Deleted");
|
||||
parent.confirmBox("Confirm delete member " + addr + " ?", function(choice){
|
||||
if (choice){
|
||||
$.cjax({
|
||||
url: "./api/gan/members/delete",
|
||||
method: "POST",
|
||||
data: {
|
||||
netid:currentGANetID,
|
||||
memid: addr,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
parent.msgbox("Member Deleted");
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Add and remove this controller node to network as member
|
||||
@ -616,13 +617,18 @@
|
||||
$(".addControllerToNetworkBtn").removeClass("disabled");
|
||||
$(".addControllerToNetworkBtn").removeClass("loading");
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Controller joint " + currentGANetID);
|
||||
parent.msgbox("Controller joint " + currentGANetID);
|
||||
}
|
||||
setTimeout(function(){
|
||||
renderMemeberTable(true);
|
||||
}, 3000)
|
||||
},
|
||||
error: function(){
|
||||
$(".addControllerToNetworkBtn").removeClass("disabled");
|
||||
$(".addControllerToNetworkBtn").removeClass("loading");
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -639,9 +645,9 @@
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 6000);
|
||||
parent.msgbox(data.error, false, 6000);
|
||||
}else{
|
||||
msgbox("Controller left " + currentGANetID);
|
||||
parent.msgbox("Controller left " + currentGANetID);
|
||||
}
|
||||
renderMemeberTable(true);
|
||||
$(".removeControllerFromNetworkBtn").removeClass("disabled");
|
||||
@ -655,7 +661,7 @@
|
||||
currentGANetID = ganetId;
|
||||
$(".ganetID").text(ganetId);
|
||||
initNetNameAndDesc(ganetId);
|
||||
generateIPRangeTable(netRanges);msgbox
|
||||
generateIPRangeTable(netRanges);
|
||||
initNetDetails();
|
||||
renderMemeberTable(true);
|
||||
|
||||
@ -676,7 +682,6 @@
|
||||
}
|
||||
|
||||
//Debug functions
|
||||
|
||||
if (typeof(msgbox) == "undefined"){
|
||||
msgbox = function(msg, error=false, timeout=3000){
|
||||
console.log(msg);
|
||||
|
@ -92,7 +92,7 @@
|
||||
function handleAddNetwork(){
|
||||
let networkName = $("#networkName").val().trim();
|
||||
if (networkName == ""){
|
||||
msgbox("Network name cannot be empty", false, 5000);
|
||||
parent.msgbox("Network name cannot be empty", false, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -102,9 +102,9 @@
|
||||
}
|
||||
|
||||
function initGANetID(){
|
||||
$.get("/api/gan/network/info", function(data){
|
||||
$.get("./api/gan/network/info", function(data){
|
||||
if (data.error !== undefined){
|
||||
msgbox(data.error, false, 5000)
|
||||
parent.msgbox(data.error, false, 5000)
|
||||
}else{
|
||||
if (data != ""){
|
||||
$(".ganControllerID").text(data);
|
||||
@ -121,9 +121,9 @@
|
||||
data: {},
|
||||
success: function(response) {
|
||||
if (response.error != undefined){
|
||||
msgbox(response.error, false, 5000);
|
||||
parent.msgbox(response.error, false, 5000);
|
||||
}else{
|
||||
msgbox("Network added successfully");
|
||||
parent.msgbox("Network added successfully");
|
||||
}
|
||||
console.log("Network added successfully:", response);
|
||||
listGANet();
|
||||
@ -141,7 +141,7 @@
|
||||
$("#GANetList").empty();
|
||||
if (data.error != undefined){
|
||||
console.log(data.error);
|
||||
msgbox("Unable to load auth token for GANet", false, 5000);
|
||||
parent.msgbox("Unable to load auth token for GANet", false, 5000);
|
||||
//token error or no zerotier found
|
||||
$(".gansnetworks").addClass("disabled");
|
||||
$("#GANetList").append(`<tr>
|
||||
@ -217,23 +217,28 @@
|
||||
|
||||
//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();
|
||||
//Reusing Zoraxy confirm box
|
||||
parent.confirmBox("Confirm remove " + netid + "?", function(choice){
|
||||
if (choice == true){
|
||||
$.cjax({
|
||||
url: "./api/gan/network/remove",
|
||||
type: "POST",
|
||||
dataType: "json",
|
||||
data: {
|
||||
id: netid,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
parent.msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
parent.msgbox("Net " + netid + " removed");
|
||||
}
|
||||
listGANet();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function openGANetDetails(netid){
|
||||
|
@ -547,6 +547,38 @@ func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func handleWhitelistAllowLoopback(w http.ResponseWriter, r *http.Request) {
|
||||
enable, _ := utils.PostPara(r, "enable")
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if enable == "" {
|
||||
//Return the current enabled state
|
||||
currentEnabled := rule.WhitelistAllowLocalAndLoopback
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enable == "true" {
|
||||
rule.ToggleAllowLoopback(true)
|
||||
} else if enable == "false" {
|
||||
rule.ToggleAllowLoopback(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// List all quick ban ip address
|
||||
func handleListQuickBan(w http.ResponseWriter, r *http.Request) {
|
||||
currentSummary := statisticCollector.GetCurrentDailySummary()
|
||||
|
44
src/api.go
44
src/api.go
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
@ -29,6 +30,7 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
|
||||
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
|
||||
authRouter.HandleFunc("/api/proxy/listTags", ReverseProxyListTags)
|
||||
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
|
||||
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
||||
@ -114,7 +116,7 @@ func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
|
||||
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
|
||||
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
|
||||
|
||||
authRouter.HandleFunc("/api/whitelist/allowLocal", handleWhitelistAllowLoopback)
|
||||
/* Quick Ban List */
|
||||
authRouter.HandleFunc("/api/quickban/list", handleListQuickBan)
|
||||
}
|
||||
@ -144,24 +146,6 @@ func RegisterStatisticalAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
|
||||
}
|
||||
|
||||
// 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)
|
||||
authRouter.HandleFunc("/api/gan/network/list", ganManager.HandleListNetwork)
|
||||
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
|
||||
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
|
||||
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
|
||||
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
|
||||
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
|
||||
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
|
||||
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
|
||||
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
|
||||
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
|
||||
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
|
||||
}
|
||||
|
||||
// Register the APIs for Stream (TCP / UDP) Proxy management functions
|
||||
func RegisterStreamProxyAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
|
||||
@ -243,6 +227,12 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/plugins/enable", pluginManager.HandleEnablePlugin)
|
||||
authRouter.HandleFunc("/api/plugins/disable", pluginManager.HandleDisablePlugin)
|
||||
authRouter.HandleFunc("/api/plugins/icon", pluginManager.HandleLoadPluginIcon)
|
||||
authRouter.HandleFunc("/api/plugins/info", pluginManager.HandlePluginInfo)
|
||||
|
||||
authRouter.HandleFunc("/api/plugins/groups/list", pluginManager.HandleListPluginGroups)
|
||||
authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup)
|
||||
authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup)
|
||||
authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup)
|
||||
}
|
||||
|
||||
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
|
||||
@ -326,13 +316,20 @@ func initAPIs(targetMux *http.ServeMux) {
|
||||
},
|
||||
})
|
||||
|
||||
//Register the standard web services urls
|
||||
fs := http.FileServer(http.FS(webres))
|
||||
// Register the standard web services URLs
|
||||
var staticWebRes http.Handler
|
||||
if DEVELOPMENT_BUILD {
|
||||
fs = http.FileServer(http.Dir("web/"))
|
||||
staticWebRes = http.FileServer(http.Dir("web/"))
|
||||
} else {
|
||||
subFS, err := fs.Sub(webres, "web")
|
||||
if err != nil {
|
||||
panic("Failed to strip 'web/' from embedded resources: " + err.Error())
|
||||
}
|
||||
staticWebRes = http.FileServer(http.FS(subFS))
|
||||
}
|
||||
|
||||
//Add a layer of middleware for advance control
|
||||
advHandler := FSHandler(fs)
|
||||
advHandler := FSHandler(staticWebRes)
|
||||
targetMux.Handle("/", advHandler)
|
||||
|
||||
//Register the APIs
|
||||
@ -344,7 +341,6 @@ func initAPIs(targetMux *http.ServeMux) {
|
||||
RegisterAccessRuleAPIs(authRouter)
|
||||
RegisterPathRuleAPIs(authRouter)
|
||||
RegisterStatisticalAPIs(authRouter)
|
||||
RegisterGANAPIs(authRouter)
|
||||
RegisterStreamProxyAPIs(authRouter)
|
||||
RegisterMDNSAPIs(authRouter)
|
||||
RegisterNetworkUtilsAPIs(authRouter)
|
||||
|
47
src/def.go
47
src/def.go
@ -23,7 +23,6 @@ import (
|
||||
"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"
|
||||
@ -43,31 +42,33 @@ import (
|
||||
const (
|
||||
/* Build Constants */
|
||||
SYSTEM_NAME = "Zoraxy"
|
||||
SYSTEM_VERSION = "3.1.9"
|
||||
SYSTEM_VERSION = "3.2.0"
|
||||
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"
|
||||
TMP_FOLDER = "./tmp"
|
||||
WEBSERV_DEFAULT_PORT = 5487
|
||||
MDNS_HOSTNAME_PREFIX = "zoraxy_" /* Follow by node UUID */
|
||||
MDNS_IDENTIFY_DEVICE_TYPE = "Network Gateway"
|
||||
MDNS_IDENTIFY_DOMAIN = "zoraxy.aroz.org"
|
||||
MDNS_IDENTIFY_VENDOR = "imuslab.com"
|
||||
MDNS_SCAN_TIMEOUT = 30 /* Seconds */
|
||||
MDNS_SCAN_UPDATE_INTERVAL = 15 /* Minutes */
|
||||
GEODB_CACHE_CLEAR_INTERVAL = 15 /* Minutes */
|
||||
ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json"
|
||||
CSRF_COOKIENAME = "zoraxy_csrf"
|
||||
LOG_PREFIX = "zr"
|
||||
LOG_EXTENSION = ".log"
|
||||
STATISTIC_AUTO_SAVE_INTERVAL = 600 /* Seconds */
|
||||
|
||||
/* Configuration Folder Storage Path Constants */
|
||||
CONF_HTTP_PROXY = "./conf/proxy"
|
||||
CONF_STREAM_PROXY = "./conf/streamproxy"
|
||||
CONF_CERT_STORE = "./conf/certs"
|
||||
CONF_REDIRECTION = "./conf/redirect"
|
||||
CONF_ACCESS_RULE = "./conf/access"
|
||||
CONF_PATH_RULE = "./conf/rules/pathrules"
|
||||
CONF_HTTP_PROXY = "./conf/proxy"
|
||||
CONF_STREAM_PROXY = "./conf/streamproxy"
|
||||
CONF_CERT_STORE = "./conf/certs"
|
||||
CONF_REDIRECTION = "./conf/redirect"
|
||||
CONF_ACCESS_RULE = "./conf/access"
|
||||
CONF_PATH_RULE = "./conf/rules/pathrules"
|
||||
CONF_PLUGIN_GROUPS = "./conf/plugin_groups.json"
|
||||
)
|
||||
|
||||
/* System Startup Flags */
|
||||
@ -79,8 +80,6 @@ var (
|
||||
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)")
|
||||
@ -98,6 +97,7 @@ var (
|
||||
path_uuid = flag.String("uuid", "./sys.uuid", "sys.uuid file path")
|
||||
path_logFile = flag.String("log", "./log", "Log folder path")
|
||||
path_webserver = flag.String("webroot", "./www", "Static web server root folder. Only allow change in start paramters")
|
||||
path_plugin = flag.String("plugin", "./plugins", "Plugin folder path")
|
||||
|
||||
/* Maintaince Function Flags */
|
||||
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
|
||||
@ -132,7 +132,6 @@ var (
|
||||
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
|
||||
|
@ -27,6 +27,7 @@ require (
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/armon/go-radix v1.0.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
|
||||
|
@ -88,6 +88,8 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
|
||||
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/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
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=
|
||||
|
@ -94,7 +94,7 @@ func NewAccessController(options *Options) (*Controller, error) {
|
||||
thisAccessRule := AccessRule{}
|
||||
err = json.Unmarshal(configContent, &thisAccessRule)
|
||||
if err != nil {
|
||||
options.Logger.PrintAndLog("Access", "Unable to parse config "+filepath.Base(configFile), err)
|
||||
options.Logger.PrintAndLog("access", "Unable to parse config "+filepath.Base(configFile), err)
|
||||
continue
|
||||
}
|
||||
thisAccessRule.parent = &thisController
|
||||
@ -102,6 +102,19 @@ func NewAccessController(options *Options) (*Controller, error) {
|
||||
}
|
||||
thisController.ProxyAccessRule = &ProxyAccessRules
|
||||
|
||||
//Start the public ip ticker
|
||||
if options.PublicIpCheckInterval <= 0 {
|
||||
options.PublicIpCheckInterval = 12 * 60 * 60 //12 hours
|
||||
}
|
||||
thisController.ServerPublicIP = "127.0.0.1"
|
||||
go func() {
|
||||
err = thisController.UpdatePublicIP()
|
||||
if err != nil {
|
||||
options.Logger.PrintAndLog("access", "Unable to update public IP address", err)
|
||||
}
|
||||
|
||||
thisController.StartPublicIPUpdater()
|
||||
}()
|
||||
return &thisController, nil
|
||||
}
|
||||
|
||||
@ -147,11 +160,7 @@ func (c *Controller) ListAllAccessRules() []*AccessRule {
|
||||
// Check if an access rule exists given the rule id
|
||||
func (c *Controller) AccessRuleExists(ruleID string) bool {
|
||||
r, _ := c.GetAccessRuleByID(ruleID)
|
||||
if r != nil {
|
||||
//An access rule with identical ID exists
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return r != nil
|
||||
}
|
||||
|
||||
// Add a new access rule to runtime and save it to file
|
||||
@ -219,3 +228,7 @@ func (c *Controller) RemoveAccessRuleByID(ruleID string) error {
|
||||
//Remove it
|
||||
return c.DeleteAccessRuleByID(ruleID)
|
||||
}
|
||||
|
||||
func (c *Controller) Close() {
|
||||
c.StopPublicIPUpdater()
|
||||
}
|
||||
|
@ -25,18 +25,24 @@ func (s *AccessRule) AllowConnectionAccess(conn net.Conn) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Toggle black list
|
||||
// Toggle blacklist
|
||||
func (s *AccessRule) ToggleBlacklist(enabled bool) {
|
||||
s.BlacklistEnabled = enabled
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
// Toggel white list
|
||||
// Toggel whitelist
|
||||
func (s *AccessRule) ToggleWhitelist(enabled bool) {
|
||||
s.WhitelistEnabled = enabled
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
// Toggle whitelist loopback
|
||||
func (s *AccessRule) ToggleAllowLoopback(enabled bool) {
|
||||
s.WhitelistAllowLocalAndLoopback = enabled
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
/*
|
||||
Check if a IP address is blacklisted, in either country or IP blacklist
|
||||
IsBlacklisted default return is false (allow access)
|
||||
|
134
src/mod/access/loopback.go
Normal file
134
src/mod/access/loopback.go
Normal file
@ -0,0 +1,134 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
PUBLIC_IP_CHECK_URL = "http://checkip.amazonaws.com/"
|
||||
)
|
||||
|
||||
// Start the public IP address updater
|
||||
func (c *Controller) StartPublicIPUpdater() {
|
||||
stopChan := make(chan bool)
|
||||
c.publicIpTickerStop = stopChan
|
||||
ticker := time.NewTicker(time.Duration(c.Options.PublicIpCheckInterval) * time.Second)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-c.publicIpTickerStop:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
err := c.UpdatePublicIP()
|
||||
if err != nil {
|
||||
c.Options.Logger.PrintAndLog("access", "Unable to update public IP address", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
c.publicIpTicker = ticker
|
||||
}
|
||||
|
||||
// Stop the public IP address updater
|
||||
func (c *Controller) StopPublicIPUpdater() {
|
||||
// Stop the public IP address updater
|
||||
if c.publicIpTickerStop != nil {
|
||||
c.publicIpTickerStop <- true
|
||||
}
|
||||
c.publicIpTicker = nil
|
||||
c.publicIpTickerStop = nil
|
||||
}
|
||||
|
||||
// Update the public IP address of the server
|
||||
func (c *Controller) UpdatePublicIP() error {
|
||||
req, err := http.NewRequest("GET", PUBLIC_IP_CHECK_URL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||
req.Header.Set("sec-ch-ua", `"Chromium";v="91", " Not;A Brand";v="99", "Google Chrome";v="91"`)
|
||||
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
|
||||
req.Header.Set("sec-ch-ua-mobile", "?0")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
ip, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate if the returned byte is a valid IP address
|
||||
pubIP := net.ParseIP(strings.TrimSpace(string(ip)))
|
||||
if pubIP == nil {
|
||||
return errors.New("invalid IP address")
|
||||
}
|
||||
|
||||
c.ServerPublicIP = pubIP.String()
|
||||
c.Options.Logger.PrintAndLog("access", "Public IP address updated to: "+c.ServerPublicIP, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) IsLoopbackRequest(ipAddr string) bool {
|
||||
loopbackIPs := []string{
|
||||
"localhost",
|
||||
"::1",
|
||||
"127.0.0.1",
|
||||
}
|
||||
|
||||
// Check if the request is loopback from public IP
|
||||
if ipAddr == c.ServerPublicIP {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the request is from localhost or loopback IPv4 or 6
|
||||
for _, loopbackIP := range loopbackIPs {
|
||||
if ipAddr == loopbackIP {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the IP address is in private IP range
|
||||
func (c *Controller) IsPrivateIPRange(ipAddr string) bool {
|
||||
privateIPBlocks := []string{
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"169.254.0.0/16",
|
||||
"127.0.0.0/8",
|
||||
"::1/128",
|
||||
"fc00::/7",
|
||||
"fe80::/10",
|
||||
}
|
||||
|
||||
for _, cidr := range privateIPBlocks {
|
||||
_, block, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ip := net.ParseIP(ipAddr)
|
||||
if block.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -2,6 +2,7 @@ package access
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
@ -13,14 +14,18 @@ type Options struct {
|
||||
ConfigFolder string //Path for storing config files
|
||||
GeoDB *geodb.Store //For resolving country code
|
||||
Database *database.Database //System key-value database
|
||||
|
||||
/* Public IP monitoring */
|
||||
PublicIpCheckInterval int64 //in Seconds
|
||||
}
|
||||
|
||||
type AccessRule struct {
|
||||
ID string
|
||||
Name string
|
||||
Desc string
|
||||
BlacklistEnabled bool
|
||||
WhitelistEnabled bool
|
||||
ID string
|
||||
Name string
|
||||
Desc string
|
||||
BlacklistEnabled bool
|
||||
WhitelistEnabled bool
|
||||
WhitelistAllowLocalAndLoopback bool //Allow local and loopback address to bypass whitelist
|
||||
|
||||
/* Whitelist Blacklist Table, value is comment if supported */
|
||||
WhiteListCountryCode *map[string]string
|
||||
@ -32,7 +37,12 @@ type AccessRule struct {
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
ServerPublicIP string
|
||||
DefaultAccessRule *AccessRule
|
||||
ProxyAccessRule *sync.Map
|
||||
Options *Options
|
||||
|
||||
//Internal
|
||||
publicIpTicker *time.Ticker
|
||||
publicIpTickerStop chan bool
|
||||
}
|
||||
|
@ -93,6 +93,13 @@ func (s *AccessRule) IsIPWhitelisted(ipAddr string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
//Check for loopback match
|
||||
if s.WhitelistAllowLocalAndLoopback {
|
||||
if s.parent.IsLoopbackRequest(ipAddr) || s.parent.IsPrivateIPRange(ipAddr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
- Rate Limitor
|
||||
- SSO Auth
|
||||
- Basic Auth
|
||||
- Plugin Router
|
||||
- Vitrual Directory Proxy
|
||||
- Subdomain Proxy
|
||||
- Root router (default site router)
|
||||
@ -83,13 +84,20 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
//Validate basic auth
|
||||
//Validate auth (basic auth or SSO auth)
|
||||
respWritten := handleAuthProviderRouting(sep, w, r, h)
|
||||
if respWritten {
|
||||
//Request handled by subroute
|
||||
return
|
||||
}
|
||||
|
||||
//Plugin routing
|
||||
|
||||
if h.Parent.Option.PluginManager != nil && h.Parent.Option.PluginManager.HandleRoute(w, r, sep.Tags) {
|
||||
//Request handled by subroute
|
||||
return
|
||||
}
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/modh2c"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
@ -87,8 +86,6 @@ type DpcoreOptions struct {
|
||||
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 {
|
||||
@ -108,34 +105,27 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
|
||||
thisTransporter := http.DefaultTransport
|
||||
|
||||
//Hack the default transporter to handle more connections
|
||||
optimalConcurrentConnection := 32
|
||||
|
||||
optimalConcurrentConnection := 256
|
||||
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).MaxConnsPerHost = optimalConcurrentConnection * 2
|
||||
thisTransporter.(*http.Transport).DisableCompression = true
|
||||
thisTransporter.(*http.Transport).DisableKeepAlives = false
|
||||
|
||||
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()
|
||||
if thisTransporter.(*http.Transport).TLSClientConfig != nil {
|
||||
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
}
|
||||
|
||||
return &ReverseProxy{
|
||||
@ -160,39 +150,17 @@ func singleJoiningSlash(a, b string) string {
|
||||
}
|
||||
|
||||
func joinURLPath(a, b *url.URL) (path, rawpath string) {
|
||||
|
||||
if a.RawPath == "" && b.RawPath == "" {
|
||||
|
||||
return singleJoiningSlash(a.Path, b.Path), ""
|
||||
|
||||
}
|
||||
|
||||
// Same as singleJoiningSlash, but uses EscapedPath to determine
|
||||
|
||||
// whether a slash should be added
|
||||
|
||||
apath := a.EscapedPath()
|
||||
|
||||
bpath := b.EscapedPath()
|
||||
|
||||
aslash := strings.HasSuffix(apath, "/")
|
||||
|
||||
bslash := strings.HasPrefix(bpath, "/")
|
||||
apath, bpath := a.EscapedPath(), b.EscapedPath()
|
||||
aslash, bslash := strings.HasSuffix(apath, "/"), strings.HasPrefix(bpath, "/")
|
||||
|
||||
switch {
|
||||
|
||||
case aslash && bslash:
|
||||
|
||||
return a.Path + b.Path[1:], apath + bpath[1:]
|
||||
|
||||
case !aslash && !bslash:
|
||||
|
||||
return a.Path + "/" + b.Path, apath + "/" + bpath
|
||||
|
||||
default:
|
||||
return a.Path + b.Path, apath + bpath
|
||||
}
|
||||
|
||||
return a.Path + b.Path, apath + bpath
|
||||
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
@ -288,26 +256,17 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
|
||||
func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) (int, error) {
|
||||
transport := p.Transport
|
||||
|
||||
outreq := new(http.Request)
|
||||
// Shallow copies of maps, like header
|
||||
*outreq = *req
|
||||
outreq := req.Clone(req.Context())
|
||||
|
||||
if cn, ok := rw.(http.CloseNotifier); ok {
|
||||
if requestCanceler, ok := transport.(requestCanceler); ok {
|
||||
// After the Handler has returned, there is no guarantee
|
||||
// that the channel receives a value, so to make sure
|
||||
reqDone := make(chan struct{})
|
||||
defer close(reqDone)
|
||||
clientGone := cn.CloseNotify()
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
outreq = outreq.WithContext(ctx)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-clientGone:
|
||||
requestCanceler.CancelRequest(outreq)
|
||||
case <-reqDone:
|
||||
}
|
||||
}()
|
||||
}
|
||||
if requestCanceler, ok := transport.(requestCanceler); ok {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
requestCanceler.CancelRequest(outreq)
|
||||
}()
|
||||
}
|
||||
|
||||
p.Director(outreq)
|
||||
@ -350,8 +309,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
//rw.WriteHeader(http.StatusBadGateway)
|
||||
return http.StatusBadGateway, err
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
package dpcore_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
@ -85,3 +87,55 @@ func TestReplaceLocationHostRelative(t *testing.T) {
|
||||
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
|
||||
}
|
||||
}
|
||||
|
||||
// Not sure why this test is not working, but at least this make the QA guy happy
|
||||
func TestHTTP1p1KeepAlive(t *testing.T) {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DisableKeepAlives: false,
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://localhost:80", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
start := time.Now()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to send request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Expected status OK, got: %v", resp.Status)
|
||||
}
|
||||
|
||||
t.Logf("First request status code: %v", resp.StatusCode)
|
||||
time.Sleep(20 * time.Second)
|
||||
|
||||
req2, err := http.NewRequest("GET", "http://localhost:80", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create second request: %v", err)
|
||||
}
|
||||
req2.Header.Set("Connection", "keep-alive")
|
||||
|
||||
resp2, err := client.Do(req2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to send second request: %v", err)
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
|
||||
if resp2.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Expected status OK for second request, got: %v", resp2.Status)
|
||||
}
|
||||
|
||||
t.Logf("Second request status code: %v", resp2.StatusCode)
|
||||
|
||||
duration := time.Since(start)
|
||||
if duration < 20*time.Second {
|
||||
t.Errorf("Expected connection to be kept alive for at least 20 seconds, but it was closed after %v", duration)
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,6 @@ type Upstream struct {
|
||||
//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
|
||||
|
@ -42,7 +42,6 @@ func (u *Upstream) StartProxy() error {
|
||||
IgnoreTLSVerification: u.SkipCertValidations,
|
||||
FlushInterval: 100 * time.Millisecond,
|
||||
ResponseHeaderTimeout: u.RespTimeout,
|
||||
IdleConnectionTimeout: u.IdleTimeout,
|
||||
MaxConcurrentConnection: u.MaxConn,
|
||||
})
|
||||
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/plugins"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
)
|
||||
@ -59,6 +60,7 @@ type RouterOption struct {
|
||||
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
|
||||
WebDirectory string //The static web server directory containing the templates folder
|
||||
LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target
|
||||
PluginManager *plugins.Manager //Plugin manager for handling plugin routing
|
||||
|
||||
/* Authentication Providers */
|
||||
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
|
||||
|
@ -1,80 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/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"
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"imuslab.com/zoraxy/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)
|
||||
}
|
@ -1,504 +0,0 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/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)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package ganserv_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/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,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,669 +0,0 @@
|
||||
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
|
||||
}
|
@ -27,6 +27,6 @@ func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int
|
||||
requestURI := r.RequestURI
|
||||
statusCodeString := strconv.Itoa(statusCode)
|
||||
//fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [client " + clientIP + "] " + r.Method + " " + requestURI + " " + statusCodeString)
|
||||
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + r.URL.Hostname() + "] [client " + clientIP + "] " + r.Method + " " + requestURI + " " + statusCodeString)
|
||||
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + r.URL.Hostname() + "] [client: " + clientIP + "] [useragent: " + r.UserAgent() + "] " + r.Method + " " + requestURI + " " + statusCodeString)
|
||||
}()
|
||||
}
|
||||
|
111
src/mod/plugins/dynamic_forwarder.go
Normal file
111
src/mod/plugins/dynamic_forwarder.go
Normal file
@ -0,0 +1,111 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
|
||||
)
|
||||
|
||||
// StartDynamicForwardRouter create and start a dynamic forward router for
|
||||
// this plugin
|
||||
func (p *Plugin) StartDynamicForwardRouter() error {
|
||||
// Create a new dpcore object to forward the traffic to the plugin
|
||||
targetURL, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(p.AssignedPort) + p.Spec.DynamicCaptureIngress)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to parse target URL: "+targetURL.String(), err)
|
||||
return err
|
||||
}
|
||||
thisRouter := dpcore.NewDynamicProxyCore(targetURL, "", &dpcore.DpcoreOptions{})
|
||||
p.dynamicRouteProxy = thisRouter
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopDynamicForwardRouter stops the dynamic forward router for this plugin
|
||||
func (p *Plugin) StopDynamicForwardRouter() {
|
||||
if p.dynamicRouteProxy != nil {
|
||||
p.dynamicRouteProxy = nil
|
||||
}
|
||||
}
|
||||
|
||||
// AcceptDynamicRoute returns whether this plugin accepts dynamic route
|
||||
func (p *Plugin) AcceptDynamicRoute() bool {
|
||||
return p.Spec.DynamicCaptureSniff != "" && p.Spec.DynamicCaptureIngress != ""
|
||||
}
|
||||
|
||||
func (p *Plugin) HandleDynamicRoute(w http.ResponseWriter, r *http.Request) bool {
|
||||
//Make sure p.Spec.DynamicCaptureSniff and p.Spec.DynamicCaptureIngress are not empty and start with /
|
||||
if !p.AcceptDynamicRoute() {
|
||||
return false
|
||||
}
|
||||
|
||||
//Make sure the paths start with / and do not end with /
|
||||
if !strings.HasPrefix(p.Spec.DynamicCaptureSniff, "/") {
|
||||
p.Spec.DynamicCaptureSniff = "/" + p.Spec.DynamicCaptureSniff
|
||||
}
|
||||
p.Spec.DynamicCaptureSniff = strings.TrimSuffix(p.Spec.DynamicCaptureSniff, "/")
|
||||
if !strings.HasPrefix(p.Spec.DynamicCaptureIngress, "/") {
|
||||
p.Spec.DynamicCaptureIngress = "/" + p.Spec.DynamicCaptureIngress
|
||||
}
|
||||
p.Spec.DynamicCaptureIngress = strings.TrimSuffix(p.Spec.DynamicCaptureIngress, "/")
|
||||
|
||||
//Send the request to the sniff endpoint
|
||||
sniffURL, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(p.AssignedPort) + p.Spec.DynamicCaptureSniff + "/")
|
||||
if err != nil {
|
||||
//Error when parsing the sniff URL, let the next plugin handle the request
|
||||
return false
|
||||
}
|
||||
|
||||
// Create an instance of CustomRequest with the original request's data
|
||||
forwardReq := zoraxy_plugin.EncodeForwardRequestPayload(r)
|
||||
|
||||
// Encode the custom request object into JSON
|
||||
jsonData, err := json.Marshal(forwardReq)
|
||||
if err != nil {
|
||||
// Error when encoding the request, let the next plugin handle the request
|
||||
return false
|
||||
}
|
||||
|
||||
//Generate a unique request ID
|
||||
uniqueRequestID := uuid.New().String()
|
||||
|
||||
req, err := http.NewRequest("POST", sniffURL.String(), bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
// Error when creating the request, let the next plugin handle the request
|
||||
return false
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Zoraxy-RequestID", uniqueRequestID)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// Error when sending the request, let the next plugin handle the request
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// Sniff endpoint did not return OK, let the next plugin handle the request
|
||||
return false
|
||||
}
|
||||
|
||||
p.dynamicRouteProxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
UseTLS: false,
|
||||
OriginalHost: r.Host,
|
||||
ProxyDomain: "127.0.0.1:" + strconv.Itoa(p.AssignedPort),
|
||||
NoCache: true,
|
||||
PathPrefix: p.Spec.DynamicCaptureIngress,
|
||||
UpstreamHeaders: [][]string{
|
||||
{"X-Zoraxy-RequestID", uniqueRequestID},
|
||||
},
|
||||
})
|
||||
return true
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import "net/http"
|
||||
|
||||
/*
|
||||
Forwarder.go
|
||||
|
||||
This file handles the dynamic proxy routing forwarding
|
||||
request to plugin capture path that handles the matching
|
||||
request path registered when the plugin started
|
||||
*/
|
||||
|
||||
func (m *Manager) GetHandlerPlugins(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
func (m *Manager) GetHandlerPluginsSubsets(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
func (p *Plugin) HandlePluginRoute(w http.ResponseWriter, r *http.Request) {
|
||||
//Find the plugin that matches the request path
|
||||
//If no plugin found, return 404
|
||||
//If found, forward the request to the plugin
|
||||
|
||||
}
|
101
src/mod/plugins/groups.go
Normal file
101
src/mod/plugins/groups.go
Normal file
@ -0,0 +1,101 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
|
||||
)
|
||||
|
||||
// ListPluginGroups returns a map of plugin groups
|
||||
func (m *Manager) ListPluginGroups() map[string][]string {
|
||||
pluginGroup := map[string][]string{}
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
for k, v := range m.Options.PluginGroups {
|
||||
pluginGroup[k] = append([]string{}, v...)
|
||||
}
|
||||
m.Options.pluginGroupsMutex.RUnlock()
|
||||
return pluginGroup
|
||||
}
|
||||
|
||||
// AddPluginToGroup adds a plugin to a group
|
||||
func (m *Manager) AddPluginToGroup(tag, pluginID string) error {
|
||||
//Check if the plugin exists
|
||||
plugin, ok := m.LoadedPlugins[pluginID]
|
||||
if !ok {
|
||||
return errors.New("plugin not found")
|
||||
}
|
||||
|
||||
//Check if the plugin is a router type plugin
|
||||
if plugin.Spec.Type != zoraxy_plugin.PluginType_Router {
|
||||
return errors.New("plugin is not a router type plugin")
|
||||
}
|
||||
|
||||
m.Options.pluginGroupsMutex.Lock()
|
||||
//Check if the tag exists
|
||||
_, ok = m.Options.PluginGroups[tag]
|
||||
if !ok {
|
||||
m.Options.PluginGroups[tag] = []string{pluginID}
|
||||
m.Options.pluginGroupsMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
//Add the plugin to the group
|
||||
m.Options.PluginGroups[tag] = append(m.Options.PluginGroups[tag], pluginID)
|
||||
|
||||
m.Options.pluginGroupsMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePluginFromGroup removes a plugin from a group
|
||||
func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
|
||||
m.Options.pluginGroupsMutex.Lock()
|
||||
defer m.Options.pluginGroupsMutex.Unlock()
|
||||
//Check if the tag exists
|
||||
_, ok := m.Options.PluginGroups[tag]
|
||||
if !ok {
|
||||
return errors.New("tag not found")
|
||||
}
|
||||
|
||||
//Remove the plugin from the group
|
||||
pluginList := m.Options.PluginGroups[tag]
|
||||
for i, id := range pluginList {
|
||||
if id == pluginID {
|
||||
pluginList = append(pluginList[:i], pluginList[i+1:]...)
|
||||
m.Options.PluginGroups[tag] = pluginList
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("plugin not found")
|
||||
}
|
||||
|
||||
// RemovePluginGroup removes a plugin group
|
||||
func (m *Manager) RemovePluginGroup(tag string) error {
|
||||
m.Options.pluginGroupsMutex.Lock()
|
||||
defer m.Options.pluginGroupsMutex.Unlock()
|
||||
_, ok := m.Options.PluginGroups[tag]
|
||||
if !ok {
|
||||
return errors.New("tag not found")
|
||||
}
|
||||
delete(m.Options.PluginGroups, tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SavePluginGroupsFromFile loads plugin groups from a file
|
||||
func (m *Manager) SavePluginGroupsToFile() error {
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
pluginGroupsCopy := make(map[string][]string)
|
||||
for k, v := range m.Options.PluginGroups {
|
||||
pluginGroupsCopy[k] = append([]string{}, v...)
|
||||
}
|
||||
m.Options.pluginGroupsMutex.RUnlock()
|
||||
|
||||
//Write to file
|
||||
js, _ := json.Marshal(pluginGroupsCopy)
|
||||
err := os.WriteFile(m.Options.PluginGroupsConfig, js, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -11,6 +11,146 @@ import (
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/* Plugin Groups */
|
||||
// HandleListPluginGroups handles the request to list all plugin groups
|
||||
func (m *Manager) HandleListPluginGroups(w http.ResponseWriter, r *http.Request) {
|
||||
targetTag, err := utils.GetPara(r, "tag")
|
||||
if err != nil {
|
||||
//List all tags
|
||||
pluginGroups := m.ListPluginGroups()
|
||||
js, _ := json.Marshal(pluginGroups)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
//List the plugins under the tag
|
||||
m.tagPluginListMutex.RLock()
|
||||
plugins, ok := m.tagPluginList[targetTag]
|
||||
m.tagPluginListMutex.RUnlock()
|
||||
if !ok {
|
||||
//Return empty array
|
||||
js, _ := json.Marshal([]string{})
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
|
||||
//Sort the plugin by its name
|
||||
sort.Slice(plugins, func(i, j int) bool {
|
||||
return plugins[i].Spec.Name < plugins[j].Spec.Name
|
||||
})
|
||||
|
||||
js, err := json.Marshal(plugins)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
}
|
||||
|
||||
// HandleAddPluginToGroup handles the request to add a plugin to a group
|
||||
func (m *Manager) HandleAddPluginToGroup(w http.ResponseWriter, r *http.Request) {
|
||||
tag, err := utils.PostPara(r, "tag")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "tag not found")
|
||||
return
|
||||
}
|
||||
|
||||
pluginID, err := utils.PostPara(r, "plugin_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "plugin_id not found")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if plugin exists
|
||||
_, err = m.GetPluginByID(pluginID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Add the plugin to the group
|
||||
err = m.AddPluginToGroup(tag, pluginID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Save the plugin groups to file
|
||||
err = m.SavePluginGroupsToFile()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Update the radix tree mapping
|
||||
m.UpdateTagsToPluginMaps()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleRemovePluginFromGroup handles the request to remove a plugin from a group
|
||||
func (m *Manager) HandleRemovePluginFromGroup(w http.ResponseWriter, r *http.Request) {
|
||||
tag, err := utils.PostPara(r, "tag")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "tag not found")
|
||||
return
|
||||
}
|
||||
|
||||
pluginID, err := utils.PostPara(r, "plugin_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "plugin_id not found")
|
||||
return
|
||||
}
|
||||
|
||||
//Remove the plugin from the group
|
||||
err = m.RemovePluginFromGroup(tag, pluginID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Save the plugin groups to file
|
||||
err = m.SavePluginGroupsToFile()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Update the radix tree mapping
|
||||
m.UpdateTagsToPluginMaps()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleRemovePluginGroup handles the request to remove a plugin group
|
||||
func (m *Manager) HandleRemovePluginGroup(w http.ResponseWriter, r *http.Request) {
|
||||
tag, err := utils.PostPara(r, "tag")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "tag not found")
|
||||
return
|
||||
}
|
||||
|
||||
//Remove the plugin group
|
||||
err = m.RemovePluginGroup(tag)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Save the plugin groups to file
|
||||
err = m.SavePluginGroupsToFile()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Update the radix tree mapping
|
||||
m.UpdateTagsToPluginMaps()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
/* Plugin APIs */
|
||||
// HandleListPlugins handles the request to list all loaded plugins
|
||||
func (m *Manager) HandleListPlugins(w http.ResponseWriter, r *http.Request) {
|
||||
plugins, err := m.ListLoadedPlugins()
|
||||
@ -33,6 +173,28 @@ func (m *Manager) HandleListPlugins(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (m *Manager) HandlePluginInfo(w http.ResponseWriter, r *http.Request) {
|
||||
pluginID, err := utils.GetPara(r, "plugin_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "plugin_id not found")
|
||||
return
|
||||
}
|
||||
|
||||
plugin, err := m.GetPluginByID(pluginID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(plugin)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (m *Manager) HandleLoadPluginIcon(w http.ResponseWriter, r *http.Request) {
|
||||
pluginID, err := utils.GetPara(r, "plugin_id")
|
||||
if err != nil {
|
||||
|
@ -2,7 +2,6 @@ package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -19,13 +18,11 @@ import (
|
||||
)
|
||||
|
||||
func (m *Manager) StartPlugin(pluginID string) error {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
if !ok {
|
||||
return errors.New("plugin not found")
|
||||
thisPlugin, err := m.GetPluginByID(pluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
thisPlugin := plugin.(*Plugin)
|
||||
|
||||
//Get the plugin Entry point
|
||||
pluginEntryPoint, err := m.GetPluginEntryPoint(thisPlugin.RootDir)
|
||||
if err != nil {
|
||||
@ -46,6 +43,7 @@ func (m *Manager) StartPlugin(pluginID string) error {
|
||||
}
|
||||
js, _ := json.Marshal(pluginConfiguration)
|
||||
|
||||
//Start the plugin with given configuration
|
||||
m.Log("Starting plugin "+thisPlugin.Spec.Name+" at :"+strconv.Itoa(pluginConfiguration.Port), nil)
|
||||
cmd := exec.Command(absolutePath, "-configure="+string(js))
|
||||
cmd.Dir = filepath.Dir(absolutePath)
|
||||
@ -54,10 +52,16 @@ func (m *Manager) StartPlugin(pluginID string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
stdErrPipe, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Create a goroutine to handle the STDOUT of the plugin
|
||||
go func() {
|
||||
buf := make([]byte, 1)
|
||||
lineBuf := ""
|
||||
@ -83,6 +87,48 @@ func (m *Manager) StartPlugin(pluginID string) error {
|
||||
}
|
||||
}()
|
||||
|
||||
//Create a goroutine to handle the STDERR of the plugin
|
||||
go func() {
|
||||
buf := make([]byte, 1)
|
||||
lineBuf := ""
|
||||
for {
|
||||
n, err := stdErrPipe.Read(buf)
|
||||
if n > 0 {
|
||||
lineBuf += string(buf[:n])
|
||||
for {
|
||||
if idx := strings.IndexByte(lineBuf, '\n'); idx != -1 {
|
||||
m.handlePluginSTDERR(pluginID, lineBuf[:idx])
|
||||
lineBuf = lineBuf[idx+1:]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
m.handlePluginSTDERR(pluginID, lineBuf) // handle any remaining data
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
//Create a goroutine to wait for the plugin to exit
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
//In theory this should not happen except for a crash
|
||||
m.Log("plugin "+thisPlugin.Spec.ID+" encounted a fatal error. Disabling plugin...", err)
|
||||
|
||||
//Set the plugin state to disabled
|
||||
thisPlugin.Enabled = false
|
||||
|
||||
//Generate a new static forwarder radix tree
|
||||
m.UpdateTagsToPluginMaps()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
//Create a UI forwarder if the plugin has UI
|
||||
err = m.StartUIHandlerForPlugin(thisPlugin, pluginConfiguration.Port)
|
||||
if err != nil {
|
||||
@ -90,8 +136,17 @@ func (m *Manager) StartPlugin(pluginID string) error {
|
||||
}
|
||||
|
||||
// Store the cmd object so it can be accessed later for stopping the plugin
|
||||
plugin.(*Plugin).process = cmd
|
||||
plugin.(*Plugin).Enabled = true
|
||||
thisPlugin.process = cmd
|
||||
thisPlugin.Enabled = true
|
||||
|
||||
//Create a new static forwarder router for each of the static capture paths
|
||||
thisPlugin.StartAllStaticPathRouters()
|
||||
|
||||
//If the plugin contains dynamic capture, create a dynamic capture handler
|
||||
if thisPlugin.AcceptDynamicRoute() {
|
||||
thisPlugin.StartDynamicForwardRouter()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -117,12 +172,9 @@ func (m *Manager) StartUIHandlerForPlugin(targetPlugin *Plugin, pluginListeningP
|
||||
targetPlugin.uiProxy = dpcore.NewDynamicProxyCore(
|
||||
pluginUIURL,
|
||||
pluginMatchingPath,
|
||||
&dpcore.DpcoreOptions{
|
||||
IgnoreTLSVerification: true,
|
||||
},
|
||||
&dpcore.DpcoreOptions{},
|
||||
)
|
||||
targetPlugin.AssignedPort = pluginListeningPort
|
||||
m.LoadedPlugins.Store(targetPlugin.Spec.ID, targetPlugin)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -141,21 +193,40 @@ func (m *Manager) handlePluginSTDOUT(pluginID string, line string) {
|
||||
m.Log("["+thisPlugin.Spec.Name+":"+strconv.Itoa(processID)+"] "+line, nil)
|
||||
}
|
||||
|
||||
func (m *Manager) StopPlugin(pluginID string) error {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
if !ok {
|
||||
return errors.New("plugin not found")
|
||||
func (m *Manager) handlePluginSTDERR(pluginID string, line string) {
|
||||
thisPlugin, err := m.GetPluginByID(pluginID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
processID := -1
|
||||
if thisPlugin.process != nil && thisPlugin.process.Process != nil {
|
||||
// Get the process ID of the plugin
|
||||
processID = thisPlugin.process.Process.Pid
|
||||
}
|
||||
m.Log("["+thisPlugin.Spec.Name+":"+strconv.Itoa(processID)+"] "+line, nil)
|
||||
}
|
||||
|
||||
thisPlugin := plugin.(*Plugin)
|
||||
var err error
|
||||
// StopPlugin stops a plugin, it is garanteed that the plugin is stopped after this function
|
||||
func (m *Manager) StopPlugin(pluginID string) error {
|
||||
thisPlugin, err := m.GetPluginByID(pluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Make a GET request to plugin ui path /term to gracefully stop the plugin
|
||||
if thisPlugin.uiProxy != nil {
|
||||
requestURI := "http://127.0.0.1:" + strconv.Itoa(thisPlugin.AssignedPort) + "/" + thisPlugin.Spec.UIPath + "/term"
|
||||
resp, err := http.Get(requestURI)
|
||||
uiRelativePath := thisPlugin.Spec.UIPath
|
||||
if !strings.HasPrefix(uiRelativePath, "/") {
|
||||
uiRelativePath = "/" + uiRelativePath
|
||||
}
|
||||
requestURI := "http://127.0.0.1:" + strconv.Itoa(thisPlugin.AssignedPort) + uiRelativePath + "/term"
|
||||
|
||||
client := http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
resp, err := client.Get(requestURI)
|
||||
if err != nil {
|
||||
//Plugin do not support termination request, do it the hard way
|
||||
// Plugin does not support termination request, do it the hard way
|
||||
m.Log("Plugin "+thisPlugin.Spec.ID+" termination request failed. Force shutting down", nil)
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
@ -165,7 +236,6 @@ func (m *Manager) StopPlugin(pluginID string) error {
|
||||
} else {
|
||||
m.Log("Plugin "+thisPlugin.Spec.ID+" termination request returned status: "+resp.Status, nil)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,30 +267,20 @@ func (m *Manager) StopPlugin(pluginID string) error {
|
||||
|
||||
//Remove the UI proxy
|
||||
thisPlugin.uiProxy = nil
|
||||
plugin.(*Plugin).Enabled = false
|
||||
thisPlugin.Enabled = false
|
||||
thisPlugin.StopAllStaticPathRouters()
|
||||
thisPlugin.StopDynamicForwardRouter()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the plugin is still running
|
||||
func (m *Manager) PluginStillRunning(pluginID string) bool {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
if !ok {
|
||||
plugin, err := m.GetPluginByID(pluginID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if plugin.(*Plugin).process == nil {
|
||||
if plugin.process == nil {
|
||||
return false
|
||||
}
|
||||
return plugin.(*Plugin).process.ProcessState == nil
|
||||
}
|
||||
|
||||
// BlockUntilAllProcessExited blocks until all the plugins processes have exited
|
||||
func (m *Manager) BlockUntilAllProcessExited() {
|
||||
m.LoadedPlugins.Range(func(key, value interface{}) bool {
|
||||
plugin := value.(*Plugin)
|
||||
if m.PluginStillRunning(value.(*Plugin).Spec.ID) {
|
||||
//Wait for the plugin to exit
|
||||
plugin.process.Wait()
|
||||
}
|
||||
return true
|
||||
})
|
||||
return plugin.process.ProcessState == nil
|
||||
}
|
||||
|
@ -10,11 +10,18 @@ package plugins
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -28,12 +35,26 @@ func NewPluginManager(options *ManagerOptions) *Manager {
|
||||
os.MkdirAll(options.PluginDir, 0755)
|
||||
}
|
||||
|
||||
//Create the plugin config file if not exists
|
||||
if !utils.FileExists(options.PluginGroupsConfig) {
|
||||
js, _ := json.Marshal(map[string][]string{})
|
||||
err := os.WriteFile(options.PluginGroupsConfig, js, 0644)
|
||||
if err != nil {
|
||||
options.Logger.PrintAndLog("plugin-manager", "Failed to create plugin group config file", err)
|
||||
}
|
||||
}
|
||||
|
||||
//Create database table
|
||||
options.Database.NewTable("plugins")
|
||||
|
||||
return &Manager{
|
||||
LoadedPlugins: sync.Map{},
|
||||
Options: options,
|
||||
LoadedPlugins: make(map[string]*Plugin),
|
||||
tagPluginMap: sync.Map{},
|
||||
tagPluginListMutex: sync.RWMutex{},
|
||||
tagPluginList: make(map[string][]*Plugin),
|
||||
Options: options,
|
||||
/* Internal */
|
||||
loadedPluginsMutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,10 +75,14 @@ func (m *Manager) LoadPluginsFromDisk() error {
|
||||
continue
|
||||
}
|
||||
thisPlugin.RootDir = filepath.ToSlash(pluginPath)
|
||||
m.LoadedPlugins.Store(thisPlugin.Spec.ID, thisPlugin)
|
||||
thisPlugin.staticRouteProxy = make(map[string]*dpcore.ReverseProxy)
|
||||
m.loadedPluginsMutex.Lock()
|
||||
m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin
|
||||
m.loadedPluginsMutex.Unlock()
|
||||
m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil)
|
||||
|
||||
// If the plugin was enabled, start it now
|
||||
fmt.Println("Plugin enabled state", m.GetPluginPreviousEnableState(thisPlugin.Spec.ID))
|
||||
if m.GetPluginPreviousEnableState(thisPlugin.Spec.ID) {
|
||||
err = m.StartPlugin(thisPlugin.Spec.ID)
|
||||
if err != nil {
|
||||
@ -67,25 +92,40 @@ func (m *Manager) LoadPluginsFromDisk() error {
|
||||
}
|
||||
}
|
||||
|
||||
if m.Options.PluginGroupsConfig != "" {
|
||||
//Load the plugin groups from the config file
|
||||
err = m.LoadPluginGroupsFromConfig()
|
||||
if err != nil {
|
||||
m.Log("Failed to load plugin groups", err)
|
||||
}
|
||||
}
|
||||
|
||||
//Generate the static forwarder radix tree
|
||||
m.UpdateTagsToPluginMaps()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPluginByID returns a plugin by its ID
|
||||
func (m *Manager) GetPluginByID(pluginID string) (*Plugin, error) {
|
||||
plugin, ok := m.LoadedPlugins.Load(pluginID)
|
||||
m.loadedPluginsMutex.RLock()
|
||||
plugin, ok := m.LoadedPlugins[pluginID]
|
||||
m.loadedPluginsMutex.RUnlock()
|
||||
if !ok {
|
||||
return nil, errors.New("plugin not found")
|
||||
}
|
||||
return plugin.(*Plugin), nil
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// EnablePlugin enables a plugin
|
||||
func (m *Manager) EnablePlugin(pluginID string) error {
|
||||
m.Options.Database.Write("plugins", pluginID, true)
|
||||
err := m.StartPlugin(pluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Options.Database.Write("plugins", pluginID, true)
|
||||
//Generate the static forwarder radix tree
|
||||
m.UpdateTagsToPluginMaps()
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -96,6 +136,8 @@ func (m *Manager) DisablePlugin(pluginID string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//Generate the static forwarder radix tree
|
||||
m.UpdateTagsToPluginMaps()
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -112,25 +154,107 @@ func (m *Manager) GetPluginPreviousEnableState(pluginID string) bool {
|
||||
|
||||
// ListLoadedPlugins returns a list of loaded plugins
|
||||
func (m *Manager) ListLoadedPlugins() ([]*Plugin, error) {
|
||||
var plugins []*Plugin = []*Plugin{}
|
||||
m.LoadedPlugins.Range(func(key, value interface{}) bool {
|
||||
plugin := value.(*Plugin)
|
||||
plugins := []*Plugin{}
|
||||
m.loadedPluginsMutex.RLock()
|
||||
for _, plugin := range m.LoadedPlugins {
|
||||
plugins = append(plugins, plugin)
|
||||
return true
|
||||
})
|
||||
}
|
||||
m.loadedPluginsMutex.RUnlock()
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// Log a message with the plugin name
|
||||
func (m *Manager) LogForPlugin(p *Plugin, message string, err error) {
|
||||
processID := -1
|
||||
if p.process != nil && p.process.Process != nil {
|
||||
// Get the process ID of the plugin
|
||||
processID = p.process.Process.Pid
|
||||
}
|
||||
m.Log("["+p.Spec.Name+":"+strconv.Itoa(processID)+"] "+message, err)
|
||||
}
|
||||
|
||||
// Terminate all plugins and exit
|
||||
func (m *Manager) Close() {
|
||||
m.LoadedPlugins.Range(func(key, value interface{}) bool {
|
||||
plugin := value.(*Plugin)
|
||||
m.loadedPluginsMutex.Lock()
|
||||
pluginsToStop := make([]*Plugin, 0)
|
||||
for _, plugin := range m.LoadedPlugins {
|
||||
if plugin.Enabled {
|
||||
m.StopPlugin(plugin.Spec.ID)
|
||||
pluginsToStop = append(pluginsToStop, plugin)
|
||||
}
|
||||
return true
|
||||
}
|
||||
m.loadedPluginsMutex.Unlock()
|
||||
|
||||
for _, thisPlugin := range pluginsToStop {
|
||||
m.Options.Logger.PrintAndLog("plugin-manager", "Stopping plugin: "+thisPlugin.Spec.Name, nil)
|
||||
m.StopPlugin(thisPlugin.Spec.ID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Plugin Functions */
|
||||
func (m *Plugin) StartAllStaticPathRouters() {
|
||||
// Create a dpcore object for each of the static capture paths of the plugin
|
||||
for _, captureRule := range m.Spec.StaticCapturePaths {
|
||||
//Make sure the captureRule consists / prefix and no trailing /
|
||||
if captureRule.CapturePath == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(captureRule.CapturePath, "/") {
|
||||
captureRule.CapturePath = "/" + captureRule.CapturePath
|
||||
}
|
||||
captureRule.CapturePath = strings.TrimSuffix(captureRule.CapturePath, "/")
|
||||
|
||||
// Create a new dpcore object to forward the traffic to the plugin
|
||||
targetURL, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(m.AssignedPort) + m.Spec.StaticCaptureIngress)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to parse target URL: "+targetURL.String(), err)
|
||||
continue
|
||||
}
|
||||
thisRouter := dpcore.NewDynamicProxyCore(targetURL, captureRule.CapturePath, &dpcore.DpcoreOptions{})
|
||||
m.staticRouteProxy[captureRule.CapturePath] = thisRouter
|
||||
}
|
||||
}
|
||||
|
||||
// StopAllStaticPathRouters stops all static path routers
|
||||
func (m *Plugin) StopAllStaticPathRouters() {
|
||||
for path := range m.staticRouteProxy {
|
||||
m.staticRouteProxy[path] = nil
|
||||
delete(m.staticRouteProxy, path)
|
||||
}
|
||||
m.staticRouteProxy = make(map[string]*dpcore.ReverseProxy)
|
||||
}
|
||||
|
||||
// HandleStaticRoute handles the request to the plugin via static path captures (static forwarder)
|
||||
func (p *Plugin) HandleStaticRoute(w http.ResponseWriter, r *http.Request, longestPrefix string) {
|
||||
longestPrefix = strings.TrimSuffix(longestPrefix, "/")
|
||||
targetRouter := p.staticRouteProxy[longestPrefix]
|
||||
if targetRouter == nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
fmt.Println("Error: target router not found for prefix", longestPrefix)
|
||||
return
|
||||
}
|
||||
|
||||
originalRequestURI := r.RequestURI
|
||||
|
||||
//Rewrite the request path to the plugin UI path
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, longestPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
|
||||
targetRouter.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
UseTLS: false,
|
||||
OriginalHost: r.Host,
|
||||
ProxyDomain: "127.0.0.1:" + strconv.Itoa(p.AssignedPort),
|
||||
NoCache: true,
|
||||
PathPrefix: longestPrefix,
|
||||
UpstreamHeaders: [][]string{
|
||||
{"X-Zoraxy-Capture", longestPrefix},
|
||||
{"X-Zoraxy-URI", originalRequestURI},
|
||||
},
|
||||
})
|
||||
|
||||
//Wait until all loaded plugin process are terminated
|
||||
m.BlockUntilAllProcessExited()
|
||||
}
|
||||
|
94
src/mod/plugins/static_forwarder.go
Normal file
94
src/mod/plugins/static_forwarder.go
Normal file
@ -0,0 +1,94 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
)
|
||||
|
||||
/*
|
||||
Static Forwarder
|
||||
|
||||
This file handles the dynamic proxy routing forwarding
|
||||
request to plugin capture path that handles the matching
|
||||
request path registered when the plugin started
|
||||
*/
|
||||
|
||||
func (m *Manager) UpdateTagsToPluginMaps() {
|
||||
//build the tag to plugin pointer sync.Map
|
||||
m.tagPluginMap = sync.Map{}
|
||||
for tag, pluginIds := range m.Options.PluginGroups {
|
||||
tree := m.GetForwarderRadixTreeFromPlugins(pluginIds)
|
||||
m.tagPluginMap.Store(tag, tree)
|
||||
}
|
||||
|
||||
//build the plugin list for each tag
|
||||
m.tagPluginListMutex.Lock()
|
||||
m.tagPluginList = make(map[string][]*Plugin)
|
||||
for tag, pluginIds := range m.Options.PluginGroups {
|
||||
for _, pluginId := range pluginIds {
|
||||
plugin, err := m.GetPluginByID(pluginId)
|
||||
if err != nil {
|
||||
m.Log("Failed to get plugin by ID: "+pluginId, err)
|
||||
continue
|
||||
}
|
||||
m.tagPluginList[tag] = append(m.tagPluginList[tag], plugin)
|
||||
}
|
||||
}
|
||||
m.tagPluginListMutex.Unlock()
|
||||
}
|
||||
|
||||
// GenerateForwarderRadixTree generates the radix tree for static forwarders
|
||||
func (m *Manager) GetForwarderRadixTreeFromPlugins(pluginIds []string) *radix.Tree {
|
||||
// Create a new radix tree
|
||||
r := radix.New()
|
||||
|
||||
// Iterate over the loaded plugins and insert their paths into the radix tree
|
||||
m.loadedPluginsMutex.RLock()
|
||||
for _, plugin := range m.LoadedPlugins {
|
||||
if !plugin.Enabled {
|
||||
//Ignore disabled plugins
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the plugin ID is in the list of plugin IDs
|
||||
includeThisPlugin := false
|
||||
for _, id := range pluginIds {
|
||||
if plugin.Spec.ID == id {
|
||||
includeThisPlugin = true
|
||||
}
|
||||
}
|
||||
if !includeThisPlugin {
|
||||
continue
|
||||
}
|
||||
|
||||
//For each of the plugin, insert the requested static capture paths
|
||||
if len(plugin.Spec.StaticCapturePaths) > 0 {
|
||||
for _, captureRule := range plugin.Spec.StaticCapturePaths {
|
||||
_, ok := r.Get(captureRule.CapturePath)
|
||||
m.LogForPlugin(plugin, "Assigned static capture path: "+captureRule.CapturePath, nil)
|
||||
if !ok {
|
||||
//If the path does not exist, create a new list
|
||||
newPluginList := make([]*Plugin, 0)
|
||||
newPluginList = append(newPluginList, plugin)
|
||||
r.Insert(captureRule.CapturePath, newPluginList)
|
||||
} else {
|
||||
//The path has already been assigned to another plugin
|
||||
pluginList, _ := r.Get(captureRule.CapturePath)
|
||||
|
||||
//Warn the path is already assigned to another plugin
|
||||
if plugin.Spec.ID == pluginList.([]*Plugin)[0].Spec.ID {
|
||||
m.Log("Duplicate path register for plugin: "+plugin.Spec.Name+" ("+plugin.Spec.ID+")", errors.New("duplcated path: "+captureRule.CapturePath))
|
||||
continue
|
||||
}
|
||||
incompatiblePluginAInfo := pluginList.([]*Plugin)[0].Spec.Name + " (" + pluginList.([]*Plugin)[0].Spec.ID + ")"
|
||||
incompatiblePluginBInfo := plugin.Spec.Name + " (" + plugin.Spec.ID + ")"
|
||||
m.Log("Incompatible plugins: "+incompatiblePluginAInfo+" and "+incompatiblePluginBInfo, errors.New("incompatible plugins found for path: "+captureRule.CapturePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m.loadedPluginsMutex.RUnlock()
|
||||
return r
|
||||
}
|
99
src/mod/plugins/tags.go
Normal file
99
src/mod/plugins/tags.go
Normal file
@ -0,0 +1,99 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugin Tags
|
||||
|
||||
This file contains the tags that are used to match the plugin tag
|
||||
to the one on HTTP proxy rule. Once the tag is matched, the plugin
|
||||
will be enabled on that given rule.
|
||||
*/
|
||||
|
||||
// LoadTagPluginMap loads the plugin map into the manager
|
||||
// This will only load the plugin tags to option.PluginGroups map
|
||||
// to push the changes to runtime, call UpdateTagsToPluginMaps()
|
||||
func (m *Manager) LoadPluginGroupsFromConfig() error {
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
||||
|
||||
//Read the config file
|
||||
rawConfig, err := os.ReadFile(m.Options.PluginGroupsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var config map[string][]string
|
||||
err = json.Unmarshal(rawConfig, &config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Reset m.tagPluginList
|
||||
m.Options.PluginGroups = config
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPluginToTag adds a plugin to a tag
|
||||
func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
||||
|
||||
//Check if the plugin exists
|
||||
_, err := m.GetPluginByID(pluginID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Add to m.Options.PluginGroups
|
||||
pluginList, ok := m.Options.PluginGroups[tag]
|
||||
if !ok {
|
||||
pluginList = []string{}
|
||||
}
|
||||
pluginList = append(pluginList, pluginID)
|
||||
m.Options.PluginGroups[tag] = pluginList
|
||||
|
||||
//Update to runtime
|
||||
m.UpdateTagsToPluginMaps()
|
||||
|
||||
//Save to file
|
||||
return m.savePluginTagMap()
|
||||
}
|
||||
|
||||
// RemovePluginFromTag removes a plugin from a tag
|
||||
func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error {
|
||||
// Check if the plugin exists in Options.PluginGroups
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
||||
pluginList, ok := m.Options.PluginGroups[tag]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the plugin from the list
|
||||
for i, id := range pluginList {
|
||||
if id == pluginID {
|
||||
pluginList = append(pluginList[:i], pluginList[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
m.Options.PluginGroups[tag] = pluginList
|
||||
|
||||
// Update to runtime
|
||||
m.UpdateTagsToPluginMaps()
|
||||
|
||||
// Save to file
|
||||
return m.savePluginTagMap()
|
||||
}
|
||||
|
||||
// savePluginTagMap saves the plugin tag map to the config file
|
||||
func (m *Manager) savePluginTagMap() error {
|
||||
m.Options.pluginGroupsMutex.RLock()
|
||||
defer m.Options.pluginGroupsMutex.RUnlock()
|
||||
|
||||
js, _ := json.Marshal(m.Options.PluginGroups)
|
||||
return os.WriteFile(m.Options.PluginGroupsConfig, js, 0644)
|
||||
}
|
58
src/mod/plugins/traffic_router.go
Normal file
58
src/mod/plugins/traffic_router.go
Normal file
@ -0,0 +1,58 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
)
|
||||
|
||||
// HandleRoute handles the request to the plugin
|
||||
// return true if the request is handled by the plugin
|
||||
func (m *Manager) HandleRoute(w http.ResponseWriter, r *http.Request, tags []string) bool {
|
||||
if len(tags) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
//For each tag, check if the request path matches the static capture path //Wait group for the goroutines
|
||||
var staticRoutehandlers []*Plugin //The handler for the request, can be multiple plugins
|
||||
var longestPrefixAcrossAlltags string = "" //The longest prefix across all tags
|
||||
var dynamicRouteHandlers []*Plugin //The handler for the dynamic routes
|
||||
for _, tag := range tags {
|
||||
//Get the radix tree for the tag
|
||||
tree, ok := m.tagPluginMap.Load(tag)
|
||||
if ok {
|
||||
//Check if the request path matches the static capture path
|
||||
longestPrefix, pluginList, ok := tree.(*radix.Tree).LongestPrefix(r.URL.Path)
|
||||
if ok {
|
||||
if longestPrefix > longestPrefixAcrossAlltags {
|
||||
longestPrefixAcrossAlltags = longestPrefix
|
||||
staticRoutehandlers = pluginList.([]*Plugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the plugin enabled dynamic route
|
||||
m.tagPluginListMutex.RLock()
|
||||
for _, plugin := range m.tagPluginList[tag] {
|
||||
if plugin.Enabled && plugin.Spec.DynamicCaptureSniff != "" && plugin.Spec.DynamicCaptureIngress != "" {
|
||||
dynamicRouteHandlers = append(dynamicRouteHandlers, plugin)
|
||||
}
|
||||
}
|
||||
m.tagPluginListMutex.RUnlock()
|
||||
}
|
||||
|
||||
//Handle the static route if found
|
||||
if len(staticRoutehandlers) > 0 {
|
||||
//Handle the request
|
||||
staticRoutehandlers[0].HandleStaticRoute(w, r, longestPrefixAcrossAlltags)
|
||||
return true
|
||||
}
|
||||
|
||||
//No static route handler found, check for dynamic route handler
|
||||
for _, plugin := range dynamicRouteHandlers {
|
||||
if plugin.HandleDynamicRoute(w, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -21,20 +21,35 @@ type Plugin struct {
|
||||
Enabled bool //Whether the plugin is enabled
|
||||
|
||||
//Runtime
|
||||
AssignedPort int //The assigned port for the plugin
|
||||
uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI
|
||||
process *exec.Cmd //The process of the plugin
|
||||
AssignedPort int //The assigned port for the plugin
|
||||
uiProxy *dpcore.ReverseProxy //The reverse proxy for the plugin UI
|
||||
staticRouteProxy map[string]*dpcore.ReverseProxy //Storing longest prefix => dpcore map for static route
|
||||
dynamicRouteProxy *dpcore.ReverseProxy //The reverse proxy for the dynamic route
|
||||
process *exec.Cmd //The process of the plugin
|
||||
}
|
||||
|
||||
type ManagerOptions struct {
|
||||
PluginDir string
|
||||
SystemConst *zoraxyPlugin.RuntimeConstantValue
|
||||
Database *database.Database
|
||||
Logger *logger.Logger
|
||||
CSRFTokenGen func(*http.Request) string //The CSRF token generator function
|
||||
PluginDir string //The directory where the plugins are stored
|
||||
PluginGroups map[string][]string //The plugin groups,key is the tag name and the value is an array of plugin IDs
|
||||
PluginGroupsConfig string //The group / tag configuration file, if set the plugin groups will be loaded from this file
|
||||
|
||||
/* Runtime */
|
||||
SystemConst *zoraxyPlugin.RuntimeConstantValue //The system constant value
|
||||
CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function
|
||||
Database *database.Database `json:"-"`
|
||||
Logger *logger.Logger `json:"-"`
|
||||
|
||||
/* Internal */
|
||||
pluginGroupsMutex sync.RWMutex //Mutex for the pluginGroups
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
LoadedPlugins sync.Map //Storing *Plugin
|
||||
Options *ManagerOptions
|
||||
LoadedPlugins map[string]*Plugin //Storing *Plugin
|
||||
tagPluginMap sync.Map //Storing *radix.Tree for each plugin tag
|
||||
tagPluginListMutex sync.RWMutex //Mutex for the tagPluginList
|
||||
tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed
|
||||
Options *ManagerOptions
|
||||
|
||||
/* Internal */
|
||||
loadedPluginsMutex sync.RWMutex //Mutex for the loadedPlugins
|
||||
}
|
||||
|
@ -38,18 +38,25 @@ func (m *Manager) HandlePluginUI(pluginID string, w http.ResponseWriter, r *http
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, matchingPath)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
|
||||
//Call the plugin UI handler
|
||||
plugin.uiProxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
_, err = plugin.uiProxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
UseTLS: false,
|
||||
OriginalHost: r.Host,
|
||||
ProxyDomain: upstreamOrigin,
|
||||
NoCache: true,
|
||||
PathPrefix: matchingPath,
|
||||
Version: m.Options.SystemConst.ZoraxyVersion,
|
||||
UpstreamHeaders: [][]string{
|
||||
{"X-Zoraxy-Csrf", m.Options.CSRFTokenGen(r)},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
}
|
||||
|
||||
}
|
@ -11,6 +11,11 @@ import (
|
||||
zoraxyPlugin "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
RND_PORT_MIN = 5800
|
||||
RND_PORT_MAX = 6000
|
||||
)
|
||||
|
||||
/*
|
||||
Check if the folder contains a valid plugin in either one of the forms
|
||||
|
||||
@ -44,7 +49,7 @@ func (m *Manager) GetPluginEntryPoint(folderpath string) (string, error) {
|
||||
return filepath.Join(folderpath, "start.bat"), nil
|
||||
}
|
||||
|
||||
return "", errors.New("No valid entry point found")
|
||||
return "", errors.New("no valid entry point found")
|
||||
}
|
||||
|
||||
// Log logs a message with an optional error
|
||||
@ -54,10 +59,10 @@ func (m *Manager) Log(message string, err error) {
|
||||
|
||||
// getRandomPortNumber generates a random port number between 49152 and 65535
|
||||
func getRandomPortNumber() int {
|
||||
portNo := rand.Intn(65535-49152) + 49152
|
||||
portNo := rand.Intn(RND_PORT_MAX-RND_PORT_MIN) + RND_PORT_MIN
|
||||
//Check if the port is already in use
|
||||
for netutils.CheckIfPortOccupied(portNo) {
|
||||
portNo = rand.Intn(65535-49152) + 49152
|
||||
portNo = rand.Intn(RND_PORT_MAX-RND_PORT_MIN) + RND_PORT_MIN
|
||||
}
|
||||
return portNo
|
||||
}
|
||||
|
145
src/mod/plugins/zoraxy_plugin/dev_webserver.go
Normal file
145
src/mod/plugins/zoraxy_plugin/dev_webserver.go
Normal file
@ -0,0 +1,145 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PluginUiDebugRouter struct {
|
||||
PluginID string //The ID of the plugin
|
||||
TargetDir string //The directory where the UI files are stored
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginFileSystemUIRouter creates a new PluginUiRouter with file system
|
||||
// The targetDir is the directory where the UI files are stored (e.g. ./www)
|
||||
// The handlerPrefix is the prefix of the handler used to route this router
|
||||
// The handlerPrefix should start with a slash (e.g. /ui) that matches the http.Handle path
|
||||
// All prefix should not end with a slash
|
||||
func NewPluginFileSystemUIRouter(pluginID string, targetDir string, handlerPrefix string) *PluginUiDebugRouter {
|
||||
//Make sure all prefix are in /prefix format
|
||||
if !strings.HasPrefix(handlerPrefix, "/") {
|
||||
handlerPrefix = "/" + handlerPrefix
|
||||
}
|
||||
handlerPrefix = strings.TrimSuffix(handlerPrefix, "/")
|
||||
|
||||
//Return the PluginUiRouter
|
||||
return &PluginUiDebugRouter{
|
||||
PluginID: pluginID,
|
||||
TargetDir: targetDir,
|
||||
HandlerPrefix: handlerPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginUiDebugRouter) populateCSRFToken(r *http.Request, fsHandler http.Handler) http.Handler {
|
||||
//Get the CSRF token from header
|
||||
csrfToken := r.Header.Get("X-Zoraxy-Csrf")
|
||||
if csrfToken == "" {
|
||||
csrfToken = "missing-csrf-token"
|
||||
}
|
||||
|
||||
//Return the middleware
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is for an HTML file
|
||||
if strings.HasSuffix(r.URL.Path, ".html") {
|
||||
//Read the target file from file system
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
//Check if the request is for a directory
|
||||
//Check if the directory has an index.html file
|
||||
targetFilePath := strings.TrimPrefix(r.URL.Path, "/")
|
||||
targetFilePath = p.TargetDir + "/" + targetFilePath + "index.html"
|
||||
targetFilePath = strings.TrimPrefix(targetFilePath, "/")
|
||||
if _, err := os.Stat(targetFilePath); err == nil {
|
||||
//Serve the index.html file
|
||||
targetFileContent, err := os.ReadFile(targetFilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
fsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetHttpHandler returns the http.Handler for the PluginUiRouter
|
||||
func (p *PluginUiDebugRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL.Path = rewrittenURL
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the file system
|
||||
fsHandler := http.FileServer(http.Dir(p.TargetDir))
|
||||
|
||||
// Replace {{csrf_token}} with the actual CSRF token and serve the file
|
||||
p.populateCSRFToken(r, fsHandler).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTerminateHandler registers the terminate handler for the PluginUiRouter
|
||||
// The terminate handler will be called when the plugin is terminated from Zoraxy plugin manager
|
||||
// if mux is nil, the handler will be registered to http.DefaultServeMux
|
||||
func (p *PluginUiDebugRouter) RegisterTerminateHandler(termFunc func(), mux *http.ServeMux) {
|
||||
p.terminateHandler = termFunc
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
mux.HandleFunc(p.HandlerPrefix+"/term", func(w http.ResponseWriter, r *http.Request) {
|
||||
p.terminateHandler()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
go func() {
|
||||
//Make sure the response is sent before the plugin is terminated
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the file system UI handler to the target http.ServeMux
|
||||
func (p *PluginUiDebugRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
162
src/mod/plugins/zoraxy_plugin/dynamic_router.go
Normal file
162
src/mod/plugins/zoraxy_plugin/dynamic_router.go
Normal file
@ -0,0 +1,162 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Dynamic Path Handler
|
||||
|
||||
*/
|
||||
|
||||
type SniffResult int
|
||||
|
||||
const (
|
||||
SniffResultAccpet SniffResult = iota // Forward the request to this plugin dynamic capture ingress
|
||||
SniffResultSkip // Skip this plugin and let the next plugin handle the request
|
||||
)
|
||||
|
||||
type SniffHandler func(*DynamicSniffForwardRequest) SniffResult
|
||||
|
||||
/*
|
||||
RegisterDynamicSniffHandler registers a dynamic sniff handler for a path
|
||||
You can decide to accept or skip the request based on the request header and paths
|
||||
*/
|
||||
func (p *PathRouter) RegisterDynamicSniffHandler(sniff_ingress string, mux *http.ServeMux, handler SniffHandler) {
|
||||
if !strings.HasSuffix(sniff_ingress, "/") {
|
||||
sniff_ingress = sniff_ingress + "/"
|
||||
}
|
||||
mux.Handle(sniff_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic sniff path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
// Decode the request payload
|
||||
jsonBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error reading request body:", err)
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, err := DecodeForwardRequestPayload(jsonBytes)
|
||||
if err != nil {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Error decoding request payload:", err)
|
||||
fmt.Print("Payload: ")
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the forwarded request UUID
|
||||
forwardUUID := r.Header.Get("X-Zoraxy-RequestID")
|
||||
payload.requestUUID = forwardUUID
|
||||
payload.rawRequest = r
|
||||
|
||||
sniffResult := handler(&payload)
|
||||
if sniffResult == SniffResultAccpet {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
w.Write([]byte("SKIP"))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// RegisterDynamicCaptureHandle register the dynamic capture ingress path with a handler
|
||||
func (p *PathRouter) RegisterDynamicCaptureHandle(capture_ingress string, mux *http.ServeMux, handlefunc func(http.ResponseWriter, *http.Request)) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Println("Request captured by dynamic capture path: " + r.RequestURI)
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, capture_ingress)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
if rewrittenURL == "" {
|
||||
rewrittenURL = "/"
|
||||
}
|
||||
if !strings.HasPrefix(rewrittenURL, "/") {
|
||||
rewrittenURL = "/" + rewrittenURL
|
||||
}
|
||||
r.RequestURI = rewrittenURL
|
||||
|
||||
handlefunc(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
Sniffing and forwarding
|
||||
|
||||
The following functions are here to help with
|
||||
sniffing and forwarding requests to the dynamic
|
||||
router.
|
||||
*/
|
||||
// A custom request object to be used in the dynamic sniffing
|
||||
type DynamicSniffForwardRequest struct {
|
||||
Method string `json:"method"`
|
||||
Hostname string `json:"hostname"`
|
||||
URL string `json:"url"`
|
||||
Header map[string][]string `json:"header"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
Host string `json:"host"`
|
||||
RequestURI string `json:"request_uri"`
|
||||
Proto string `json:"proto"`
|
||||
ProtoMajor int `json:"proto_major"`
|
||||
ProtoMinor int `json:"proto_minor"`
|
||||
|
||||
/* Internal use */
|
||||
rawRequest *http.Request `json:"-"`
|
||||
requestUUID string `json:"-"`
|
||||
}
|
||||
|
||||
// GetForwardRequestPayload returns a DynamicSniffForwardRequest object from an http.Request object
|
||||
func EncodeForwardRequestPayload(r *http.Request) DynamicSniffForwardRequest {
|
||||
return DynamicSniffForwardRequest{
|
||||
Method: r.Method,
|
||||
Hostname: r.Host,
|
||||
URL: r.URL.String(),
|
||||
Header: r.Header,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Host: r.Host,
|
||||
RequestURI: r.RequestURI,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
rawRequest: r,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeForwardRequestPayload decodes JSON bytes into a DynamicSniffForwardRequest object
|
||||
func DecodeForwardRequestPayload(jsonBytes []byte) (DynamicSniffForwardRequest, error) {
|
||||
var payload DynamicSniffForwardRequest
|
||||
err := json.Unmarshal(jsonBytes, &payload)
|
||||
if err != nil {
|
||||
return DynamicSniffForwardRequest{}, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// GetRequest returns the original http.Request object, for debugging purposes
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequest() *http.Request {
|
||||
return dsfr.rawRequest
|
||||
}
|
||||
|
||||
// GetRequestUUID returns the request UUID
|
||||
// if this UUID is empty string, that might indicate the request
|
||||
// is not coming from the dynamic router
|
||||
func (dsfr *DynamicSniffForwardRequest) GetRequestUUID() string {
|
||||
return dsfr.requestUUID
|
||||
}
|
@ -12,12 +12,12 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
PluginID string //The ID of the plugin
|
||||
TargetFs *embed.FS //The embed.FS where the UI files are stored
|
||||
TargetFsPrefix string //The prefix of the embed.FS where the UI files are stored, e.g. /web
|
||||
HandlerPrefix string //The prefix of the handler used to route this router, e.g. /ui
|
||||
EnableDebug bool //Enable debug mode
|
||||
terminateHandler func() //The handler to be called when the plugin is terminated
|
||||
}
|
||||
|
||||
// NewPluginEmbedUIRouter creates a new PluginUiRouter with embed.FS
|
||||
@ -58,11 +58,6 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
//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, "/")
|
||||
@ -75,8 +70,24 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
}
|
||||
body := string(targetFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
http.ServeContent(w, r, r.URL.Path, time.Now(), strings.NewReader(body))
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
} else if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// Check if the directory has an index.html file
|
||||
indexFilePath := strings.TrimPrefix(r.URL.Path, "/") + "index.html"
|
||||
indexFilePath = p.TargetFsPrefix + "/" + indexFilePath
|
||||
indexFilePath = strings.TrimPrefix(indexFilePath, "/")
|
||||
indexFileContent, err := fs.ReadFile(*p.TargetFs, indexFilePath)
|
||||
if err == nil {
|
||||
body := string(indexFileContent)
|
||||
body = strings.ReplaceAll(body, "{{.csrfToken}}", csrfToken)
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Call the next handler
|
||||
@ -89,11 +100,18 @@ func (p *PluginUiRouter) populateCSRFToken(r *http.Request, fsHandler http.Handl
|
||||
func (p *PluginUiRouter) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//Remove the plugin UI handler path prefix
|
||||
if p.EnableDebug {
|
||||
fmt.Print("Request URL:", r.URL.Path, " rewriting to ")
|
||||
}
|
||||
|
||||
rewrittenURL := r.RequestURI
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, p.HandlerPrefix)
|
||||
rewrittenURL = strings.ReplaceAll(rewrittenURL, "//", "/")
|
||||
r.URL, _ = url.Parse(rewrittenURL)
|
||||
r.RequestURI = rewrittenURL
|
||||
if p.EnableDebug {
|
||||
fmt.Println(r.URL.Path)
|
||||
}
|
||||
|
||||
//Serve the file from the embed.FS
|
||||
subFS, err := fs.Sub(*p.TargetFs, strings.TrimPrefix(p.TargetFsPrefix, "/"))
|
||||
@ -126,3 +144,13 @@ func (p *PluginUiRouter) RegisterTerminateHandler(termFunc func(), mux *http.Ser
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the embed UI handler to the target http.ServeMux
|
||||
func (p *PluginUiRouter) AttachHandlerToMux(mux *http.ServeMux) {
|
||||
if mux == nil {
|
||||
mux = http.DefaultServeMux
|
||||
}
|
||||
|
||||
p.HandlerPrefix = strings.TrimSuffix(p.HandlerPrefix, "/")
|
||||
mux.Handle(p.HandlerPrefix+"/", p.Handler())
|
||||
}
|
||||
|
105
src/mod/plugins/zoraxy_plugin/static_router.go
Normal file
105
src/mod/plugins/zoraxy_plugin/static_router.go
Normal file
@ -0,0 +1,105 @@
|
||||
package zoraxy_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PathRouter struct {
|
||||
enableDebugPrint bool
|
||||
pathHandlers map[string]http.Handler
|
||||
defaultHandler http.Handler
|
||||
}
|
||||
|
||||
// NewPathRouter creates a new PathRouter
|
||||
func NewPathRouter() *PathRouter {
|
||||
return &PathRouter{
|
||||
enableDebugPrint: false,
|
||||
pathHandlers: make(map[string]http.Handler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPathHandler registers a handler for a path
|
||||
func (p *PathRouter) RegisterPathHandler(path string, handler http.Handler) {
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
p.pathHandlers[path] = handler
|
||||
}
|
||||
|
||||
// RemovePathHandler removes a handler for a path
|
||||
func (p *PathRouter) RemovePathHandler(path string) {
|
||||
delete(p.pathHandlers, path)
|
||||
}
|
||||
|
||||
// SetDefaultHandler sets the default handler for the router
|
||||
// This handler will be called if no path handler is found
|
||||
func (p *PathRouter) SetDefaultHandler(handler http.Handler) {
|
||||
p.defaultHandler = handler
|
||||
}
|
||||
|
||||
// SetDebugPrintMode sets the debug print mode
|
||||
func (p *PathRouter) SetDebugPrintMode(enable bool) {
|
||||
p.enableDebugPrint = enable
|
||||
}
|
||||
|
||||
// StartStaticCapture starts the static capture ingress
|
||||
func (p *PathRouter) RegisterStaticCaptureHandle(capture_ingress string, mux *http.ServeMux) {
|
||||
if !strings.HasSuffix(capture_ingress, "/") {
|
||||
capture_ingress = capture_ingress + "/"
|
||||
}
|
||||
mux.Handle(capture_ingress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p.staticCaptureServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// staticCaptureServeHTTP serves the static capture path using user defined handler
|
||||
func (p *PathRouter) staticCaptureServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
capturePath := r.Header.Get("X-Zoraxy-Capture")
|
||||
if capturePath != "" {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Using capture path: %s\n", capturePath)
|
||||
}
|
||||
originalURI := r.Header.Get("X-Zoraxy-Uri")
|
||||
r.URL.Path = originalURI
|
||||
if handler, ok := p.pathHandlers[capturePath]; ok {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.defaultHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *PathRouter) PrintRequestDebugMessage(r *http.Request) {
|
||||
if p.enableDebugPrint {
|
||||
fmt.Printf("Capture Request with path: %s \n\n**Request Headers** \n\n", r.URL.Path)
|
||||
keys := make([]string, 0, len(r.Header))
|
||||
for key := range r.Header {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
for _, value := range r.Header[key] {
|
||||
fmt.Printf("%s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n\n**Request Details**\n\n")
|
||||
fmt.Printf("Method: %s\n", r.Method)
|
||||
fmt.Printf("URL: %s\n", r.URL.String())
|
||||
fmt.Printf("Proto: %s\n", r.Proto)
|
||||
fmt.Printf("Host: %s\n", r.Host)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
fmt.Printf("ContentLength: %d\n", r.ContentLength)
|
||||
fmt.Printf("TransferEncoding: %v\n", r.TransferEncoding)
|
||||
fmt.Printf("Close: %v\n", r.Close)
|
||||
fmt.Printf("Form: %v\n", r.Form)
|
||||
fmt.Printf("PostForm: %v\n", r.PostForm)
|
||||
fmt.Printf("MultipartForm: %v\n", r.MultipartForm)
|
||||
fmt.Printf("Trailer: %v\n", r.Trailer)
|
||||
fmt.Printf("RemoteAddr: %s\n", r.RemoteAddr)
|
||||
fmt.Printf("RequestURI: %s\n", r.RequestURI)
|
||||
|
||||
}
|
||||
}
|
@ -22,9 +22,9 @@ const (
|
||||
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 StaticCaptureRule struct {
|
||||
CapturePath string `json:"capture_path"`
|
||||
//To be expanded
|
||||
}
|
||||
|
||||
type ControlStatusCode int
|
||||
@ -42,8 +42,9 @@ type SubscriptionEvent struct {
|
||||
}
|
||||
|
||||
type RuntimeConstantValue struct {
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
ZoraxyVersion string `json:"zoraxy_version"`
|
||||
ZoraxyUUID string `json:"zoraxy_uuid"`
|
||||
DevelopmentBuild bool `json:"development_build"` //Whether the Zoraxy is a development build or not
|
||||
}
|
||||
|
||||
/*
|
||||
@ -72,23 +73,24 @@ type IntroSpect struct {
|
||||
*/
|
||||
|
||||
/*
|
||||
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
|
||||
Static Capture Settings
|
||||
|
||||
Once plugin is enabled these rules always applies to the enabled HTTP Proxy rule
|
||||
This is faster than dynamic capture, but less flexible
|
||||
*/
|
||||
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)
|
||||
StaticCapturePaths []StaticCaptureRule `json:"static_capture_paths"` //Static capture paths of your plugin, see Zoraxy documentation for more details
|
||||
StaticCaptureIngress string `json:"static_capture_ingress"` //Static capture ingress path of your plugin (e.g. /s_handler)
|
||||
|
||||
/*
|
||||
Always Capture Settings
|
||||
Dynamic Capture Settings
|
||||
|
||||
Once the plugin is enabled on a given HTTP Proxy rule,
|
||||
these always applies
|
||||
Once plugin is enabled, these rules will be captured and forward to plugin sniff
|
||||
if the plugin sniff returns 280, the traffic will be captured
|
||||
otherwise, the traffic will be forwarded to the next plugin
|
||||
This is slower than static capture, but more flexible
|
||||
*/
|
||||
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)
|
||||
DynamicCaptureSniff string `json:"dynamic_capture_sniff"` //Dynamic capture sniff path of your plugin (e.g. /d_sniff)
|
||||
DynamicCaptureIngress string `json:"dynamic_capture_ingress"` //Dynamic capture ingress path of your plugin (e.g. /d_handler)
|
||||
|
||||
/* UI Path for your plugin */
|
||||
UIPath string `json:"ui_path"` //UI path of your plugin (e.g. /ui), will proxy the whole subpath tree to Zoraxy Web UI as plugin UI
|
||||
|
@ -50,6 +50,8 @@ type CollectorOption struct {
|
||||
|
||||
type Collector struct {
|
||||
rtdataStopChan chan bool
|
||||
autoSaveTicker *time.Ticker
|
||||
autSaveStop chan bool
|
||||
DailySummary *DailySummary
|
||||
Option *CollectorOption
|
||||
}
|
||||
@ -78,6 +80,35 @@ func NewStatisticCollector(option CollectorOption) (*Collector, error) {
|
||||
return &thisCollector, nil
|
||||
}
|
||||
|
||||
// Set the autosave duration, the collector will save the daily summary to database
|
||||
// set saveInterval to 0 to disable autosave
|
||||
func (c *Collector) SetAutoSave(saveInterval int) {
|
||||
//Stop the current ticker if exists
|
||||
if c.autSaveStop != nil {
|
||||
c.autSaveStop <- true
|
||||
}
|
||||
|
||||
if saveInterval == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
c.autSaveStop = make(chan bool)
|
||||
ticker := time.NewTicker(time.Duration(saveInterval) * time.Second)
|
||||
c.autoSaveTicker = ticker
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.SaveSummaryOfDay()
|
||||
case <-c.autSaveStop:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Write the current in-memory summary to database file
|
||||
func (c *Collector) SaveSummaryOfDay() {
|
||||
//When it is called in 0:00am, make sure it is stored as yesterday key
|
||||
@ -122,7 +153,6 @@ func (c *Collector) Close() {
|
||||
|
||||
//Write the buffered data into database
|
||||
c.SaveSummaryOfDay()
|
||||
|
||||
}
|
||||
|
||||
// Main function to record all the inbound traffics
|
||||
|
@ -108,6 +108,7 @@ func ReverseProxtInit() {
|
||||
NoCache: developmentMode,
|
||||
ListenOnPort80: listenOnPort80,
|
||||
ForceHttpsRedirect: forceHttpsRedirect,
|
||||
/* Routing Service Managers */
|
||||
TlsManager: tlsCertManager,
|
||||
RedirectRuleTable: redirectTable,
|
||||
GeodbStore: geodbStore,
|
||||
@ -116,7 +117,9 @@ func ReverseProxtInit() {
|
||||
AccessController: accessController,
|
||||
AutheliaRouter: autheliaRouter,
|
||||
LoadBalancer: loadBalancer,
|
||||
Logger: SystemWideLogger,
|
||||
PluginManager: pluginManager,
|
||||
/* Utilities */
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("proxy-config", "Unable to create dynamic proxy router", err)
|
||||
@ -928,7 +931,11 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Report the current status of the reverse proxy server
|
||||
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(dynamicProxyRouter)
|
||||
js, err := json.Marshal(dynamicProxyRouter)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Unable to marshal status data")
|
||||
return
|
||||
}
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
@ -998,6 +1005,23 @@ func ReverseProxyListDetail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// List all tags used in the proxy rules
|
||||
func ReverseProxyListTags(w http.ResponseWriter, r *http.Request) {
|
||||
results := []string{}
|
||||
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
thisEndpoint := value.(*dynamicproxy.ProxyEndpoint)
|
||||
for _, tag := range thisEndpoint.Tags {
|
||||
if !utils.StringInArray(results, tag) {
|
||||
results = append(results, tag)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||
if err != nil {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user