Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
7eda6ba501 | |||
2da5ef048f | |||
6c48939316 | |||
544894bbba | |||
153d056bdf | |||
12c1118af9 | |||
67ba143999 | |||
0a8a821394 | |||
36b17ce4cf | |||
519372069f | |||
2f14d6f271 | |||
44ac7144ec | |||
741d3f8de1 | |||
23eca5afae | |||
050fab9481 | |||
3fc92bac27 | |||
594f75da97 | |||
3fbf246fb4 | |||
828af6263d | |||
ab42cec31f | |||
a8bf07dbba | |||
48dc85ea3e | |||
a73a7944ec | |||
d187124db6 | |||
0dd9e5d73c | |||
5e7599756f | |||
5db50c1ca2 | |||
884507b45a | |||
2574d0504e | |||
9535abe314 | |||
8e6a60f684 | |||
ead26ea16d | |||
3d66c01d7b | |||
20fd8e9a49 | |||
5952a1b55f | |||
edd19e2b30 | |||
7ad64de145 | |||
dd08d003a6 | |||
f4f2b55f14 | |||
01daf5541a | |||
b1d0a79131 | |||
23e289eaff | |||
e71d4154d6 | |||
80f566e312 | |||
530a99a544 | |||
ba347c5fa1 | |||
d51cb6efdf | |||
c07d5f85df | |||
5ac0fdde1d |
6
.gitignore
vendored
@ -27,4 +27,8 @@ src/conf/*
|
||||
src/ReverseProxy_*_*
|
||||
src/Zoraxy_*_*
|
||||
src/certs/*
|
||||
src/rules/*
|
||||
src/rules/*
|
||||
src/README.md
|
||||
docker/ContainerTester.sh
|
||||
docker/ImagePublisher.sh
|
||||
src/mod/acme/test/stackoverflow.pem
|
53
CHANGELOG.md
Normal file
@ -0,0 +1,53 @@
|
||||
v2.6.5 Jul 19 2023
|
||||
|
||||
+ Added Import / Export-Feature
|
||||
+ Moved configurationfiles to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26)
|
||||
+ Added auto-renew with ACME [#6](https://github.com/tobychui/zoraxy/issues/6)
|
||||
+ Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18)
|
||||
+ Added Whois
|
||||
|
||||
# v2.6.4 Jun 15 2023
|
||||
|
||||
+ Added force TLS v1.2 above toggle
|
||||
+ Added trace route
|
||||
+ Added ICMP ping
|
||||
+ Added special routing rules module for up-coming acme integration
|
||||
+ Fixed IPv6 check bug in black/whitelist
|
||||
+ Optimized UI for TCP Proxy
|
||||
|
||||
# v2.6.3 Jun 8 2023
|
||||
|
||||
+ Added X-Forwarded-Proto for automatic proxy detector
|
||||
+ Split blacklist and whitelist from geodb script file
|
||||
+ Optimized compile binary size
|
||||
+ Added access control to TCP proxy
|
||||
+ Added "invalid config detect" in up time monitor for isse [#7](https://github.com/tobychui/zoraxy/issues/7)
|
||||
+ Fixed minor bugs in advance stats panel
|
||||
+ Reduced file size of embedded materials
|
||||
|
||||
# v2.6.2 Jun 4 2023
|
||||
|
||||
+ Added advance stats operation tab
|
||||
+ Added statistic reset [#13](https://github.com/tobychui/zoraxy/issues/13)
|
||||
+ Added statistic export to csv and json (please use json)
|
||||
+ Make subdomain clickable (not vdir) [#12](https://github.com/tobychui/zoraxy/issues/12)
|
||||
+ Added TCP Proxy
|
||||
+ Updates SMTP setup UI to make it more straight forward to setup
|
||||
|
||||
# v2.6.1 May 31 2023
|
||||
|
||||
+ Added reverse proxy TLS skip verification
|
||||
+ Added basic auth
|
||||
+ Edit proxy settings
|
||||
+ Whitelist
|
||||
+ TCP Proxy (experimental)
|
||||
+ Info (Utilities page)
|
||||
|
||||
# v2.6 May 27 2023
|
||||
|
||||
+ Basic auth
|
||||
+ Support TLS verification skip (for self signed certs)
|
||||
+ Added trend analysis
|
||||
+ Added referer and file type analysis
|
||||
+ Added cert expire day display
|
||||
+ Moved subdomain proxy logic to dpcore
|
661
LICENSE
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 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.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
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, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public 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.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
50
README.md
@ -27,7 +27,18 @@ General purpose request (reverse) proxy and forwarding tool for low power device
|
||||
- Basic single-admin management mode
|
||||
- External permission management system for easy system integration
|
||||
- SMTP config for password reset
|
||||
|
||||
|
||||
## Build from Source
|
||||
Require Go 1.20 or above
|
||||
|
||||
```
|
||||
git clone https://github.com/tobychui/zoraxy
|
||||
cd ./zoraxy/src/
|
||||
go mod tidy
|
||||
go build
|
||||
|
||||
sudo ./zoraxy -port=:8000
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@ -55,6 +66,9 @@ The installation method is same as Linux. If you are using Raspberry Pi 4 or new
|
||||
|
||||
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
||||
|
||||
#### Docker
|
||||
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details
|
||||
|
||||
### External Permission Management Mode
|
||||
|
||||
If you already have a up-stream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable noauth mode, start Zoraxy with the following flag
|
||||
@ -102,27 +116,13 @@ To start the module, go to System Settings > Modules > Subservice and enable it
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
|
||||
|
||||

|
||||
## FAQ
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychui/zoraxy/wiki/FAQ---Frequently-Asked-Questions)!
|
||||
|
||||
## Global Area Network Controller
|
||||
|
||||
@ -130,7 +130,7 @@ This project also compatible with [ZeroTier](https://www.zerotier.com/). However
|
||||
|
||||
Assuming you already have a valid license, to use Zoraxy with ZeroTier, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken at correct location in your host.
|
||||
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some wierd installation profile, you can also pass in the ZeroTier auth token using the following flags
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags
|
||||
|
||||
```
|
||||
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
|
||||
@ -142,7 +142,12 @@ This allows you to have infinite number of network members in your Global Area N
|
||||
|
||||
## Web.SSH
|
||||
|
||||
Web SSH currently only support Linux based OS.
|
||||
Web SSH currently only support Linux based OS. The following platforms are supported
|
||||
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/armv6 (experimental)
|
||||
- linux/386 (experimental)
|
||||
|
||||
### Loopback Connection
|
||||
|
||||
@ -152,8 +157,7 @@ Loopback web ssh connection, by default, is disabled. This means that if you are
|
||||
./zoraxy -sshlb=true
|
||||
```
|
||||
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This Github repo is for storing the release binary and collecting issue only. **This project is not open source and the provided binaries are for function review purpose only.** For business users, please contact toby@imuslab.com for commercial licensing details.
|
||||
This project is open source under AGPL. I open source this project so everyone can check for security issues and benefit all users. **If your plans to use this project in commercial environment which violate the AGPL terms, please contact toby@imuslab.com for an alternative commercial license.**
|
||||
|
||||
|
19
docker/Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk update && apk upgrade &&\
|
||||
apk add bash curl jq sudo &&\
|
||||
mkdir -p /zoraxy/data/
|
||||
|
||||
VOLUME [ "/zoraxy/data/" ]
|
||||
|
||||
COPY entrypoint.sh /zoraxy/
|
||||
|
||||
RUN chmod +x /zoraxy/entrypoint.sh
|
||||
|
||||
ENV ARGS="-port=:8000 -noauth=false"
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["/zoraxy/entrypoint.sh"]
|
21
docker/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 PassiveLemon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
67
docker/README.md
Normal file
@ -0,0 +1,67 @@
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
|
||||
## Setup: </br>
|
||||
Although not required, it is recommended to give Zoraxy a dedicated location on the host to mount the container. That way, the host/user can access them whenever needed. A volume will be created automatically within Docker if a location is not specified. </br>
|
||||
|
||||
You may also need to portforward your 80/443 to allow http and https traffic. If you are accessing the interface from outside of the local network, you may also need to forward your management port. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. </br>
|
||||
|
||||
### Using Docker run </br>
|
||||
```
|
||||
docker run -d --name (container name) -p (ports) -v (path to storage directory):/zoraxy/data/ -e ARGS=(your arguments) -e VERSION=(version) passivelemon/zoraxy-docker:latest
|
||||
```
|
||||
|
||||
### Using Docker Compose </br>
|
||||
```
|
||||
version: '3.3'
|
||||
services:
|
||||
zoraxy-docker:
|
||||
image: passivelemon/zoraxy-docker:latest
|
||||
container_name: (container name)
|
||||
ports:
|
||||
- 80:80 # Http port
|
||||
- 443:443 # Https port
|
||||
- (external):8000 # Management portal port
|
||||
volumes:
|
||||
- (path to storage directory):/zoraxy/data/ # Host directory for Zoraxy file storage
|
||||
environment:
|
||||
ARGS: '(your arguments)' # The arguments to run with Zoraxy. Enter them as they would be entered normally.
|
||||
```
|
||||
|
||||
| Operator | Need | Details |
|
||||
|:-|:-|:-|
|
||||
| `-d` | Yes | will run the container in the background. |
|
||||
| `--name (container name)` | No | Sets the name of the container to the following word. You can change this to whatever you want. |
|
||||
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
|
||||
| `-v (path to storage directory):/zoraxy/data/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
|
||||
| `-e ARGS=(your arguments)` | No | Sets the arguments to run Zoraxy with. Enter them as you would normally. By default, it is ran with `-port=:8000 -noauth=false` |
|
||||
| `-e VERSION=(version)` | No | Sets the version of Zoraxy that the container will download. Must be a supported version found on the Zoraxy Github. Defaults to the latest if not set. |
|
||||
| `passivelemon/zoraxy-docker:latest` | Yes | The repository on Docker hub. By default, it is the latest version that I have published. |
|
||||
|
||||
## Examples: </br>
|
||||
### Docker Run </br>
|
||||
```
|
||||
docker run -d --name zoraxy -p 80:80 -p 443:443 -p 8005:8000/tcp -v /home/docker/Containers/Zoraxy:/zoraxy/data/ -e ARGS="-port=:8000 -noauth=false" passivelemon/zoraxy-docker:latest
|
||||
```
|
||||
|
||||
### Docker Compose </br>
|
||||
```
|
||||
version: '3.3'
|
||||
services:
|
||||
zoraxy-docker:
|
||||
image: passivelemon/zoraxy-docker:latest
|
||||
container_name: zoraxy
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 8005:8000/tcp
|
||||
volumes:
|
||||
- /home/docker/Containers/Zoraxy:/zoraxy/data/
|
||||
environment:
|
||||
ARGS: '-port=:8000 -noauth=false'
|
||||
```
|
||||
|
||||
### Other </br>
|
||||
If the container doesn't start properly, you might be rate limited from GitHub for some amount of time. You can check this by running `curl -s https://api.github.com/repos/tobychui/zoraxy/releases` on the host. If you are, you will just have to wait for a little while or use a VPN. </br>
|
20
docker/entrypoint.sh
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd /zoraxy/data/
|
||||
|
||||
if [ "$VERSION" != "" ]; then
|
||||
echo "|| Using release ${VERSION} ||"
|
||||
release=${VERSION}
|
||||
else
|
||||
echo "|| Using latest release ||"
|
||||
# Gets the latest pre-release version tag. Will be updated when official release comes out.
|
||||
release=$(curl -s https://api.github.com/repos/tobychui/zoraxy/releases | jq -r 'map(select(.prerelease)) | .[0].tag_name')
|
||||
fi
|
||||
|
||||
if [ ! -e "/zoraxy/data/zoraxy_linux_amd64-${release}" ]; then
|
||||
echo "|| Downloading version ${release} ||"
|
||||
curl -Lso /zoraxy/data/zoraxy_linux_amd64-${release} https://github.com/tobychui/zoraxy/releases/download/${release}/zoraxy_linux_amd64
|
||||
chmod u+x /zoraxy/data/zoraxy_linux_amd64-${release}
|
||||
fi
|
||||
|
||||
./zoraxy_linux_amd64-${release} ${ARGS}
|
BIN
docs/favicon.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
docs/img/bg.png
Normal file
After Width: | Height: | Size: 4.5 MiB |
BIN
docs/img/bg2.png
Normal file
After Width: | Height: | Size: 9.4 MiB |
BIN
docs/img/icon.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
1
docs/img/icons/awesome.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="m772-635-43-100-104-46 104-45 43-95 43 95 104 45-104 46-43 100Zm0 595-43-96-104-45 104-45 43-101 43 101 104 45-104 45-43 96ZM333-194l-92-197-201-90 201-90 92-196 93 196 200 90-200 90-93 197Zm0-148 48-96 98-43-98-43-48-96-47 96-99 43 99 43 47 96Zm0-139Z"/></svg>
|
After Width: | Height: | Size: 358 B |
1
docs/img/icons/blacklist.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M280-453h400v-60H280v60ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>
|
After Width: | Height: | Size: 433 B |
1
docs/img/icons/code.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M320-242 80-482l242-242 43 43-199 199 197 197-43 43Zm318 2-43-43 199-199-197-197 43-43 240 240-242 242Z"/></svg>
|
After Width: | Height: | Size: 209 B |
1
docs/img/icons/gan.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M120-80v-270h120v-160h210v-100H330v-270h300v270H510v100h210v160h120v270H540v-270h120v-100H300v100h120v270H120Zm270-590h180v-150H390v150ZM180-140h180v-150H180v150Zm420 0h180v-150H600v150ZM480-670ZM360-290Zm240 0Z"/></svg>
|
After Width: | Height: | Size: 317 B |
1
docs/img/icons/home.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M220-180h150v-250h220v250h150v-390L480-765 220-570v390Zm-60 60v-480l320-240 320 240v480H530v-250H430v250H160Zm320-353Z"/></svg>
|
After Width: | Height: | Size: 224 B |
1
docs/img/icons/plugin.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M356-120H180q-24 0-42-18t-18-42v-176q44-5 75.5-34.5T227-463q0-43-31.5-72.5T120-570v-176q0-24 18-42t42-18h177q11-40 39.5-67t68.5-27q40 0 68.5 27t39.5 67h173q24 0 42 18t18 42v173q40 11 65.5 41.5T897-461q0 40-25.5 67T806-356v176q0 24-18 42t-42 18H570q-5-48-35.5-77.5T463-227q-41 0-71.5 29.5T356-120Zm-176-60h130q25-61 69.888-84 44.888-23 83-23T546-264q45 23 70 84h130v-235h45q20 0 33-13t13-33q0-20-13-33t-33-13h-45v-239H511v-48q0-20-13-33t-33-13q-20 0-33 13t-13 33v48H180v130q48.15 17.817 77.575 59.686Q287-514.445 287-462.777 287-412 257.5-370T180-310v130Zm329-330Z"/></svg>
|
After Width: | Height: | Size: 669 B |
1
docs/img/icons/proxy.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M273-160 80-353l193-193 42 42-121 121h316v60H194l121 121-42 42Zm414-254-42-42 121-121H450v-60h316L645-758l42-42 193 193-193 193Z"/></svg>
|
After Width: | Height: | Size: 234 B |
1
docs/img/icons/redirect.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M700-160v-410H275l153 153-42 43-226-226 226-226 42 42-153 154h485v470h-60Z"/></svg>
|
After Width: | Height: | Size: 180 B |
1
docs/img/icons/scan.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M197-197q-54-54-85.5-126.5T80-480q0-84 31.5-156.5T197-763l43 43q-46 46-73 107.5T140-480q0 71 26.5 132T240-240l-43 43Zm113-113q-32-32-51-75.5T240-480q0-51 19-94.5t51-75.5l43 43q-24 24-38.5 56.5T300-480q0 38 14 70t39 57l-43 43Zm170-90q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm170 90-43-43q24-24 38.5-56.5T660-480q0-38-14-70t-39-57l43-43q32 32 51 75.5t19 94.5q0 50-19 93.5T650-310Zm113 113-43-43q46-46 73-107.5T820-480q0-71-26.5-132T720-720l43-43q54 55 85.5 127.5T880-480q0 83-31.5 155.5T763-197Z"/></svg>
|
After Width: | Height: | Size: 652 B |
1
docs/img/icons/screenshots.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M345-377h391L609-548 506-413l-68-87-93 123Zm-85 177q-24 0-42-18t-18-42v-560q0-24 18-42t42-18h560q24 0 42 18t18 42v560q0 24-18 42t-42 18H260Zm0-60h560v-560H260v560ZM140-80q-24 0-42-18t-18-42v-620h60v620h620v60H140Zm120-740v560-560Z"/></svg>
|
After Width: | Height: | Size: 336 B |
1
docs/img/icons/stats.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M109.912-150Q81-150 60.5-170.589 40-191.177 40-220.089 40-249 60.494-269.5t49.273-20.5q5.233 0 10.233.5 5 .5 13 2.5l200-200q-2-8-2.5-13t-.5-10.233q0-28.779 20.589-49.273Q371.177-580 400.089-580 429-580 449.5-559.366t20.5 49.61Q470-508 467-487l110 110q8-2 13-2.5t10-.5q5 0 10 .5t13 2.5l160-160q-2-8-2.5-13t-.5-10.233q0-28.779 20.589-49.273Q821.177-630 850.089-630 879-630 899.5-609.411q20.5 20.588 20.5 49.5Q920-531 899.506-510.5T850.233-490Q845-490 840-490.5q-5-.5-13-2.5L667-333q2 8 2.5 13t.5 10.233q0 28.779-20.589 49.273Q628.823-240 599.911-240 571-240 550.5-260.494T530-309.767q0-5.233.5-10.233.5-5 2.5-13L423-443q-8 2-13 2.5t-10.25.5q-1.75 0-22.75-3L177-243q2 8 2.5 13t.5 10.233q0 28.779-20.589 49.273Q138.823-150 109.912-150ZM160-592l-20.253-43.747L96-656l43.747-20.253L160-720l20.253 43.747L224-656l-43.747 20.253L160-592Zm440-51-30.717-66.283L503-740l66.283-30.717L600-837l30.717 66.283L697-740l-66.283 30.717L600-643Z"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
docs/img/icons/terminal.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M140-160q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42v520q0 24-18 42t-42 18H140Zm0-60h680v-436H140v436Zm160-72-42-42 103-104-104-104 43-42 146 146-146 146Zm190 4v-60h220v60H490Z"/></svg>
|
After Width: | Height: | Size: 300 B |
BIN
docs/img/logo.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
docs/img/og.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
docs/img/og.psd
Normal file
BIN
docs/img/screenshots/1.webp
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
docs/img/screenshots/10.webp
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
docs/img/screenshots/2.webp
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
docs/img/screenshots/3.webp
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/img/screenshots/4.webp
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
docs/img/screenshots/5.webp
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
docs/img/screenshots/6.webp
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
docs/img/screenshots/7.webp
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
docs/img/screenshots/8.webp
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
docs/img/screenshots/9.webp
Normal file
After Width: | Height: | Size: 153 KiB |
BIN
docs/img/title.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/img/title.psd
Normal file
384
docs/index.html
@ -1,9 +1,375 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Zoraxy</title>
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<h1>Work In Progress</h1>
|
||||
</body>
|
||||
</html>
|
||||
<!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>Cluster Proxy Gateway | 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.arozos.com/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta property="og:image" content="https://zoraxy.arozos.com/img/og.png">
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="arozos.com">
|
||||
<meta property="twitter:url" content="https://zoraxy.arozos.com/">
|
||||
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta name="twitter:image" content="https://zoraxy.arozos.com/img/og.png">
|
||||
|
||||
<!-- 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="dot-container">
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
<div class="headbanner"></div>
|
||||
<div id="home" class="herotext">
|
||||
<div class="ui basic segment">
|
||||
<div class="bannerHeaderWrapper">
|
||||
<h1 class="bannerHeader">Zoraxy</h1>
|
||||
<p class="bannerSubheader">All in one homelab network routing solution</p>
|
||||
</div>
|
||||
<br><br>
|
||||
<a class="ui black big button" href="#features">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>
|
||||
|
||||
<!-- 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, noobs 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 of the 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. Allow 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>Integrated with Gotty Web SSH terminal, allow 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 insights 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 discovering service, 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.webp" target="_blank"><img src="img/screenshots/1.webp" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/2.webp" target="_blank"><img src="img/screenshots/2.webp" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/3.webp" target="_blank"><img src="img/screenshots/3.webp" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/4.webp" target="_blank"><img src="img/screenshots/4.webp" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/5.webp" target="_blank"><img src="img/screenshots/5.webp" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/6.webp" target="_blank"><img src="img/screenshots/6.webp" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/7.webp" target="_blank"><img src="img/screenshots/7.webp" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/8.webp" target="_blank"><img src="img/screenshots/8.webp" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/9.webp" target="_blank"><img src="img/screenshots/9.webp" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/10.webp" target="_blank"><img src="img/screenshots/10.webp" 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>
|
||||
|
228
docs/style.css
Normal file
@ -0,0 +1,228 @@
|
||||
body{
|
||||
background: #f6f6f6 !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
:root{
|
||||
--themeTextColor: #404040;
|
||||
--themeSkyblueColor: #a9d1f3;
|
||||
--themeSkyblueColorDecondary: #8eb9df;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.left-menu {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
background-color: #ffffff;
|
||||
min-height: 100vh;
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
|
||||
.iconWrapper{
|
||||
padding: 1em;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
}
|
||||
|
||||
.right-content {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ui.black.button{
|
||||
background-color: var(--themeTextColor) !important;
|
||||
}
|
||||
|
||||
/* Menu items */
|
||||
.menu-item{
|
||||
display: block;
|
||||
padding: 0.4em;
|
||||
padding-top: 1.2em;
|
||||
padding-bottom: 1.2em;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
width: 100%;
|
||||
border-right: 0.4em solid var(--themeTextColor);
|
||||
transition: border-left ease-in-out 0.1s, background-color ease-in-out 0.1s;
|
||||
}
|
||||
|
||||
.menu-item.active{
|
||||
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.menu-item:hover{
|
||||
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
|
||||
}
|
||||
|
||||
.menu-item img{
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
/* Head banner */
|
||||
.headbanner{
|
||||
background-image: url('img/bg.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
background-size: auto 100%;
|
||||
position:absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
z-index: -100;
|
||||
}
|
||||
|
||||
.herotext{
|
||||
padding-top: 15em;
|
||||
padding-left: 8vw;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.bannerHeader{
|
||||
font-size: 8em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bannerSubheader{
|
||||
font-weight: 400;
|
||||
font-size: 1.2em;
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
.bannerHeaderWrapper{
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* features */
|
||||
#features{
|
||||
padding-top: 4em;
|
||||
padding-bottom: 4em;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* screenshots */
|
||||
.screenshot{
|
||||
transition: transform ease-in-out 0.1s;
|
||||
box-shadow: 3px 3px 5px 0px rgba(51,51,51,0.14);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.screenshot:hover {
|
||||
transform: scale(1.1); /* (150% zoom - Note: if the zoom is too large, it will go outside of the viewport) */
|
||||
}
|
||||
|
||||
|
||||
/* RWD */
|
||||
@media (max-width:960px) {
|
||||
/* Menu RWD */
|
||||
.left-menu {
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
}
|
||||
.iconWrapper{
|
||||
padding: 0.2em;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
}
|
||||
|
||||
.menu-item{
|
||||
padding: 0.3em;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.menu-item img{
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
/* head banner RWD */
|
||||
.headbanner{
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.herotext{
|
||||
padding-left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bannerSubheader{
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.bannerHeader{
|
||||
font-size: 5em;
|
||||
}
|
||||
|
||||
.bannerHeaderWrapper{
|
||||
display: inline;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.herotext .ui.collapsing.table{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Decorative Animation */
|
||||
.dot-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: 2em;
|
||||
left: 2em;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #d9d9d9;
|
||||
margin-right: 6px;
|
||||
animation-name: dot-animation;
|
||||
animation-duration: 4s;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.dot:nth-child(4) {
|
||||
animation-delay: 3s;
|
||||
}
|
||||
|
||||
@keyframes dot-animation {
|
||||
0% {
|
||||
background-color: #d9d9d9;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
background-color: #a9d1f3;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
100% {
|
||||
background-color: #d9d9d9;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 100 KiB |
BIN
img/screenshots/6.png
Normal file
After Width: | Height: | Size: 77 KiB |
60
src/Makefile
Normal file
@ -0,0 +1,60 @@
|
||||
# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
|
||||
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64
|
||||
temp = $(subst /, ,$@)
|
||||
os = $(word 1, $(temp))
|
||||
arch = $(word 2, $(temp))
|
||||
|
||||
#all: web.tar.gz $(PLATFORMS) fixwindows zoraxy_file_checksum.sha1
|
||||
all: clear_old $(PLATFORMS) fixwindows
|
||||
|
||||
binary: $(PLATFORMS)
|
||||
|
||||
hash: zoraxy_file_checksum.sha1
|
||||
|
||||
web: web.tar.gz
|
||||
|
||||
clean:
|
||||
rm -f zoraxy_*_*
|
||||
rm -f web.tar.gz
|
||||
|
||||
$(PLATFORMS):
|
||||
@echo "Building $(os)/$(arch)"
|
||||
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
fixwindows:
|
||||
-mv ./dist/zoraxy_windows_amd64 ./dist/zoraxy_windows_amd64.exe
|
||||
# -mv ./dist/zoraxy_windows_arm64 ./dist/zoraxy_windows_arm64.exe
|
||||
|
||||
|
||||
clear_old:
|
||||
-rm -rf ./dist/
|
||||
-mkdir ./dist/
|
||||
|
||||
web.tar.gz:
|
||||
|
||||
@echo "Removing old build resources, if exists"
|
||||
-rm -rf ./dist/
|
||||
-mkdir ./dist/
|
||||
|
||||
@echo "Moving subfolders to build folder"
|
||||
-cp -r ./web ./dist/web/
|
||||
-cp -r ./system ./dist/system/
|
||||
|
||||
@ echo "Remove sensitive information"
|
||||
-rm -rf ./dist/certs/
|
||||
-rm -rf ./dist/conf/
|
||||
-rm -rf ./dist/rules/
|
||||
|
||||
|
||||
@echo "Creating tarball for all required files"
|
||||
-rm ./dist/web.tar.gz
|
||||
-cd ./dist/ && tar -czf ./web.tar.gz system/ web/
|
||||
|
||||
@echo "Clearing up unzipped folder structures"
|
||||
-rm -rf "./dist/web"
|
||||
-rm -rf "./dist/system"
|
||||
|
||||
zoraxy_file_checksum.sha1:
|
||||
@echo "Generating the checksum, if sha1sum installed"
|
||||
-sha1sum ./dist/web.tar.gz > ./dist/zoraxy_file_checksum.sha1
|
195
src/accesslist.go
Normal file
@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
accesslist.go
|
||||
|
||||
This script file is added to extend the
|
||||
reverse proxy function to include
|
||||
banning / whitelist a specific IP address or country code
|
||||
*/
|
||||
|
||||
/*
|
||||
Blacklist Related
|
||||
*/
|
||||
|
||||
// List a of blacklisted ip address or country code
|
||||
func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype, err := utils.GetPara(r, "type")
|
||||
if err != nil {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
resulst := []string{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllBlacklistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
resulst = geodbStore.GetAllBlacklistedIp()
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(resulst)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
|
||||
}
|
||||
|
||||
func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
countryCode, err := utils.PostPara(r, "cc")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty country code")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddCountryCodeToBlackList(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
countryCode, err := utils.PostPara(r, "cc")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty country code")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveCountryCodeFromBlackList(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
ipAddr, err := utils.PostPara(r, "ip")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty ip address")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddIPToBlackList(ipAddr)
|
||||
}
|
||||
|
||||
func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
ipAddr, err := utils.PostPara(r, "ip")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty ip address")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveIPFromBlackList(ipAddr)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
enable, err := utils.PostPara(r, "enable")
|
||||
if err != nil {
|
||||
//Return the current enabled state
|
||||
currentEnabled := geodbStore.BlacklistEnabled
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enable == "true" {
|
||||
geodbStore.ToggleBlacklist(true)
|
||||
} else if enable == "false" {
|
||||
geodbStore.ToggleBlacklist(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Whitelist Related
|
||||
*/
|
||||
|
||||
func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype, err := utils.GetPara(r, "type")
|
||||
if err != nil {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
resulst := []string{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllWhitelistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
resulst = geodbStore.GetAllWhitelistedIp()
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(resulst)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
|
||||
}
|
||||
|
||||
func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
countryCode, err := utils.PostPara(r, "cc")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty country code")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddCountryCodeToWhitelist(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
countryCode, err := utils.PostPara(r, "cc")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty country code")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveCountryCodeFromWhitelist(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
ipAddr, err := utils.PostPara(r, "ip")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty ip address")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddIPToWhiteList(ipAddr)
|
||||
}
|
||||
|
||||
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
ipAddr, err := utils.PostPara(r, "ip")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty ip address")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveIPFromWhiteList(ipAddr)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
enable, err := utils.PostPara(r, "enable")
|
||||
if err != nil {
|
||||
//Return the current enabled state
|
||||
currentEnabled := geodbStore.WhitelistEnabled
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enable == "true" {
|
||||
geodbStore.ToggleWhitelist(true)
|
||||
} else if enable == "false" {
|
||||
geodbStore.ToggleWhitelist(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
115
src/acme.go
Normal file
@ -0,0 +1,115 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
acme.go
|
||||
|
||||
This script handle special routing required for acme auto cert renew functions
|
||||
*/
|
||||
|
||||
// Helper function to generate a random port above a specified value
|
||||
func getRandomPort(minPort int) int {
|
||||
return rand.Intn(65535-minPort) + minPort
|
||||
}
|
||||
|
||||
// init the new ACME instance
|
||||
func initACME() *acme.ACMEHandler {
|
||||
log.Println("Starting ACME handler")
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
// Generate a random port above 30000
|
||||
port := getRandomPort(30000)
|
||||
|
||||
// Check if the port is already in use
|
||||
for acme.IsPortInUse(port) {
|
||||
port = getRandomPort(30000)
|
||||
}
|
||||
|
||||
return acme.NewACME("https://acme-staging-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
|
||||
}
|
||||
|
||||
// create the special routing rule for ACME
|
||||
func acmeRegisterSpecialRoutingRule() {
|
||||
log.Println("Assigned temporary port:" + acmeHandler.Getport())
|
||||
|
||||
err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
|
||||
ID: "acme-autorenew",
|
||||
MatchRule: func(r *http.Request) bool {
|
||||
found, _ := regexp.MatchString("/.well-known/acme-challenge/*", r.RequestURI)
|
||||
return found
|
||||
},
|
||||
RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://localhost:"+acmeHandler.Getport()+r.RequestURI, nil)
|
||||
req.Host = r.Host
|
||||
if err != nil {
|
||||
fmt.Printf("client: could not create request: %s\n", err)
|
||||
return
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("client: error making http request: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
resBody, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("error reading: %s\n", err)
|
||||
return
|
||||
}
|
||||
w.Write(resBody)
|
||||
},
|
||||
Enabled: true,
|
||||
UseSystemAccessControl: false,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Println("[Err] " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// This function check if the renew setup is satisfied. If not, toggle them automatically
|
||||
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
isForceHttpsRedirectEnabledOriginally := false
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Enable port 80 to 443 redirect
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
log.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
} else {
|
||||
//Set this to true, so after renew, do not turn it off
|
||||
isForceHttpsRedirectEnabledOriginally = true
|
||||
}
|
||||
|
||||
} else if dynamicProxyRouter.Option.Port == 80 {
|
||||
//Go ahead
|
||||
|
||||
} else {
|
||||
//This port do not support ACME
|
||||
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
|
||||
}
|
||||
|
||||
// Pass over to the acmeHandler to deal with the communication
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
log.Println("Restoring HTTP to HTTPS redirect settings")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
}
|
||||
}
|
246
src/api.go
Normal file
@ -0,0 +1,246 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
API.go
|
||||
|
||||
This file contains all the API called by the web management interface
|
||||
|
||||
*/
|
||||
|
||||
var requireAuth = true
|
||||
|
||||
func initAPIs() {
|
||||
|
||||
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
|
||||
AuthAgent: authAgent,
|
||||
RequireAuth: requireAuth,
|
||||
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
||||
},
|
||||
})
|
||||
|
||||
//Register the standard web services urls
|
||||
fs := http.FileServer(http.FS(webres))
|
||||
if development {
|
||||
fs = http.FileServer(http.Dir("web/"))
|
||||
}
|
||||
//Add a layer of middleware for advance control
|
||||
advHandler := FSHandler(fs)
|
||||
http.Handle("/", advHandler)
|
||||
|
||||
//Authentication APIs
|
||||
registerAuthAPIs(requireAuth)
|
||||
|
||||
//Reverse proxy
|
||||
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
||||
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
|
||||
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
||||
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
|
||||
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
|
||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||
|
||||
//TLS / SSL config
|
||||
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
||||
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
||||
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
||||
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
|
||||
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
|
||||
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
||||
|
||||
//Redirection config
|
||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||
|
||||
//Blacklist APIs
|
||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
||||
|
||||
//Whitelist APIs
|
||||
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
||||
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
||||
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
|
||||
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
|
||||
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
|
||||
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
|
||||
|
||||
//Path Blocker APIs
|
||||
authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath)
|
||||
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
|
||||
authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath)
|
||||
|
||||
//Statistic & uptime monitoring API
|
||||
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
|
||||
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
|
||||
authRouter.HandleFunc("/api/stats/netstat", netstat.HandleGetNetworkInterfaceStats)
|
||||
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
|
||||
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
|
||||
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
|
||||
|
||||
//Global Area Network APIs
|
||||
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
|
||||
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
|
||||
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
|
||||
authRouter.HandleFunc("/api/gan/network/list", ganManager.HandleListNetwork)
|
||||
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
|
||||
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
|
||||
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
|
||||
authRouter.HandleFunc("/api/gan/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)
|
||||
|
||||
//TCP Proxy
|
||||
authRouter.HandleFunc("/api/tcpprox/config/add", tcpProxyManager.HandleAddProxyConfig)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/edit", tcpProxyManager.HandleEditProxyConfigs)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/list", tcpProxyManager.HandleListConfigs)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/start", tcpProxyManager.HandleStartProxy)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/stop", tcpProxyManager.HandleStopProxy)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/delete", tcpProxyManager.HandleRemoveProxy)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/status", tcpProxyManager.HandleGetProxyStatus)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/validate", tcpProxyManager.HandleConfigValidate)
|
||||
|
||||
//mDNS APIs
|
||||
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
|
||||
authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning)
|
||||
|
||||
//Zoraxy Analytic
|
||||
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
|
||||
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
|
||||
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
|
||||
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
|
||||
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
|
||||
|
||||
//Network utilities
|
||||
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
|
||||
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
|
||||
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
|
||||
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
|
||||
authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession)
|
||||
authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck)
|
||||
authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan)
|
||||
authRouter.HandleFunc("/api/tools/smtp/get", HandleSMTPGet)
|
||||
authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet)
|
||||
authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet)
|
||||
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
|
||||
|
||||
//Account Reset
|
||||
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
||||
http.HandleFunc("/api/account/new", HandleNewPasswordSetup)
|
||||
|
||||
//ACME & Auto Renewer
|
||||
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
|
||||
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
||||
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
|
||||
|
||||
//Others
|
||||
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
||||
http.HandleFunc("/api/conf/export", ExportConfigAsZip)
|
||||
http.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
||||
|
||||
//If you got APIs to add, append them here
|
||||
}
|
||||
|
||||
// Function to renders Auth related APIs
|
||||
func registerAuthAPIs(requireAuth bool) {
|
||||
//Auth APIs
|
||||
http.HandleFunc("/api/auth/login", authAgent.HandleLogin)
|
||||
http.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
|
||||
http.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
|
||||
if requireAuth {
|
||||
authAgent.CheckLogin(w, r)
|
||||
} else {
|
||||
utils.SendJSONResponse(w, "true")
|
||||
}
|
||||
})
|
||||
http.HandleFunc("/api/auth/username", func(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := authAgent.GetUserName(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(username)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
})
|
||||
http.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
|
||||
uc := authAgent.GetUserCounts()
|
||||
js, _ := json.Marshal(uc)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
})
|
||||
http.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
|
||||
if authAgent.GetUserCounts() == 0 {
|
||||
//Allow register root admin
|
||||
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {
|
||||
|
||||
})
|
||||
} else {
|
||||
//This function is disabled
|
||||
utils.SendErrorResponse(w, "Root management account already exists")
|
||||
}
|
||||
})
|
||||
http.HandleFunc("/api/auth/changePassword", func(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := authAgent.GetUserName(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
oldPassword, err := utils.PostPara(r, "oldPassword")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "empty current password")
|
||||
return
|
||||
}
|
||||
newPassword, err := utils.PostPara(r, "newPassword")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "empty new password")
|
||||
return
|
||||
}
|
||||
confirmPassword, _ := utils.PostPara(r, "confirmPassword")
|
||||
|
||||
if newPassword != confirmPassword {
|
||||
utils.SendErrorResponse(w, "confirm password not match")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the old password correct
|
||||
oldPasswordCorrect, _ := authAgent.ValidateUsernameAndPasswordWithReason(username, oldPassword)
|
||||
if !oldPasswordCorrect {
|
||||
utils.SendErrorResponse(w, "Invalid current password given")
|
||||
return
|
||||
}
|
||||
|
||||
//Change the password of the root user
|
||||
authAgent.UnregisterUser(username)
|
||||
authAgent.CreateUserAccount(username, newPassword, "")
|
||||
})
|
||||
|
||||
}
|
306
src/cert.go
Normal file
@ -0,0 +1,306 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// Check if the default certificates is correctly setup
|
||||
func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
|
||||
type CheckResult struct {
|
||||
DefaultPubExists bool
|
||||
DefaultPriExists bool
|
||||
}
|
||||
|
||||
pub, pri := tlsCertManager.DefaultCertExistsSep()
|
||||
js, _ := json.Marshal(CheckResult{
|
||||
pub,
|
||||
pri,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Return a list of domains where the certificates covers
|
||||
func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
filenames, err := tlsCertManager.ListCertDomains()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
showDate, _ := utils.GetPara(r, "date")
|
||||
if showDate == "true" {
|
||||
type CertInfo struct {
|
||||
Domain string
|
||||
LastModifiedDate string
|
||||
ExpireDate string
|
||||
RemainingDays int
|
||||
}
|
||||
|
||||
results := []*CertInfo{}
|
||||
|
||||
for _, filename := range filenames {
|
||||
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt")
|
||||
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
||||
fileInfo, err := os.Stat(certFilepath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
|
||||
return
|
||||
}
|
||||
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
||||
|
||||
certExpireTime := "Unknown"
|
||||
certBtyes, err := os.ReadFile(certFilepath)
|
||||
expiredIn := 0
|
||||
if err != nil {
|
||||
//Unable to load this file
|
||||
continue
|
||||
} else {
|
||||
//Cert loaded. Check its expire time
|
||||
block, _ := pem.Decode(certBtyes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
|
||||
|
||||
duration := cert.NotAfter.Sub(time.Now())
|
||||
|
||||
// Convert the duration to days
|
||||
expiredIn = int(duration.Hours() / 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thisCertInfo := CertInfo{
|
||||
Domain: filename,
|
||||
LastModifiedDate: modifiedTime,
|
||||
ExpireDate: certExpireTime,
|
||||
RemainingDays: expiredIn,
|
||||
}
|
||||
|
||||
results = append(results, &thisCertInfo)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(js)
|
||||
} else {
|
||||
response, err := json.Marshal(filenames)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(response)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// List all certificates and map all their domains to the cert filename
|
||||
func handleListDomains(w http.ResponseWriter, r *http.Request) {
|
||||
filenames, err := os.ReadDir("./conf/certs/")
|
||||
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
certnameToDomainMap := map[string]string{}
|
||||
for _, filename := range filenames {
|
||||
if filename.IsDir() {
|
||||
continue
|
||||
}
|
||||
certFilepath := filepath.Join("./conf/certs/", filename.Name())
|
||||
|
||||
certBtyes, err := os.ReadFile(certFilepath)
|
||||
if err != nil {
|
||||
// Unable to load this file
|
||||
log.Println("Unable to load certificate: " + certFilepath)
|
||||
continue
|
||||
} else {
|
||||
// Cert loaded. Check its expiry time
|
||||
block, _ := pem.Decode(certBtyes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
|
||||
for _, dnsName := range cert.DNSNames {
|
||||
certnameToDomainMap[dnsName] = certname
|
||||
}
|
||||
certnameToDomainMap[cert.Subject.CommonName] = certname
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requireCompact, _ := utils.GetPara(r, "compact")
|
||||
if requireCompact == "true" {
|
||||
result := make(map[string][]string)
|
||||
|
||||
for key, value := range certnameToDomainMap {
|
||||
if _, ok := result[value]; !ok {
|
||||
result[value] = make([]string, 0)
|
||||
}
|
||||
|
||||
result[value] = append(result[value], key)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(result)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(certnameToDomainMap)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Handle front-end toggling TLS mode
|
||||
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
currentTlsSetting := false
|
||||
if sysdb.KeyExists("settings", "usetls") {
|
||||
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
||||
}
|
||||
|
||||
newState, err := utils.PostPara(r, "set")
|
||||
if err != nil {
|
||||
//No setting. Get the current status
|
||||
js, _ := json.Marshal(currentTlsSetting)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if newState == "true" {
|
||||
sysdb.Write("settings", "usetls", true)
|
||||
log.Println("Enabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(true)
|
||||
} else if newState == "false" {
|
||||
sysdb.Write("settings", "usetls", false)
|
||||
log.Println("Disabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid state given. Only support true or false")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the GET and SET of reverse proxy TLS versions
|
||||
func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
|
||||
newState, err := utils.PostPara(r, "set")
|
||||
if err != nil {
|
||||
//GET
|
||||
var reqLatestTLS bool = false
|
||||
if sysdb.KeyExists("settings", "forceLatestTLS") {
|
||||
sysdb.Read("settings", "forceLatestTLS", &reqLatestTLS)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(reqLatestTLS)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if newState == "true" {
|
||||
sysdb.Write("settings", "forceLatestTLS", true)
|
||||
log.Println("Updating minimum TLS version to v1.2 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(true)
|
||||
} else if newState == "false" {
|
||||
sysdb.Write("settings", "forceLatestTLS", false)
|
||||
log.Println("Updating minimum TLS version to v1.0 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid state given")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle upload of the certificate
|
||||
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
// check if request method is POST
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// get the key type
|
||||
keytype, err := utils.GetPara(r, "ktype")
|
||||
overWriteFilename := ""
|
||||
if err != nil {
|
||||
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get the domain
|
||||
domain, err := utils.GetPara(r, "domain")
|
||||
if err != nil {
|
||||
//Assume localhost
|
||||
domain = "default"
|
||||
}
|
||||
|
||||
if keytype == "pub" {
|
||||
overWriteFilename = domain + ".crt"
|
||||
} else if keytype == "pri" {
|
||||
overWriteFilename = domain + ".key"
|
||||
} else {
|
||||
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// parse multipart form data
|
||||
err = r.ParseMultipartForm(10 << 20) // 10 MB
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get file from form data
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// create file in upload directory
|
||||
os.MkdirAll("./conf/certs", 0775)
|
||||
f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// copy file contents to destination file
|
||||
_, err = io.Copy(f, file)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// send response
|
||||
fmt.Fprintln(w, "File upload successful!")
|
||||
}
|
||||
|
||||
// Handle cert remove
|
||||
func handleCertRemove(w http.ResponseWriter, r *http.Request) {
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid domain given")
|
||||
return
|
||||
}
|
||||
err = tlsCertManager.RemoveCert(domain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
}
|
||||
}
|
290
src/config.go
Normal file
@ -0,0 +1,290 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
Reverse Proxy Configs
|
||||
|
||||
The following section handle
|
||||
the reverse proxy configs
|
||||
*/
|
||||
|
||||
type Record struct {
|
||||
ProxyType string
|
||||
Rootname string
|
||||
ProxyTarget string
|
||||
UseTLS bool
|
||||
SkipTlsValidation bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
|
||||
}
|
||||
|
||||
func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
|
||||
//TODO: Make this accept new def types
|
||||
os.MkdirAll("./conf/proxy/", 0775)
|
||||
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
|
||||
|
||||
//Generate record
|
||||
thisRecord := proxyConfigRecord
|
||||
|
||||
//Write to file
|
||||
js, _ := json.MarshalIndent(thisRecord, "", " ")
|
||||
return ioutil.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfig(rootname string) error {
|
||||
filename := getFilenameFromRootName(rootname)
|
||||
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
|
||||
log.Println("Config Removed: ", removePendingFile)
|
||||
if utils.FileExists(removePendingFile) {
|
||||
err := os.Remove(removePendingFile)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//File already gone
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return ptype, rootname and proxyTarget, error if any
|
||||
func LoadReverseProxyConfig(filename string) (*Record, error) {
|
||||
thisRecord := Record{}
|
||||
configContent, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return &thisRecord, err
|
||||
}
|
||||
|
||||
//Unmarshal the content into config
|
||||
err = json.Unmarshal(configContent, &thisRecord)
|
||||
if err != nil {
|
||||
return &thisRecord, err
|
||||
}
|
||||
|
||||
//Return it
|
||||
return &thisRecord, nil
|
||||
}
|
||||
|
||||
func getFilenameFromRootName(rootname string) string {
|
||||
//Generate a filename for this rootname
|
||||
filename := strings.ReplaceAll(rootname, ".", "_")
|
||||
filename = strings.ReplaceAll(filename, "/", "-")
|
||||
filename = filename + ".config"
|
||||
return filename
|
||||
}
|
||||
|
||||
/*
|
||||
Importer and Exporter of Zoraxy proxy config
|
||||
*/
|
||||
|
||||
func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
includeSysDBRaw, err := utils.GetPara(r, "includeDB")
|
||||
includeSysDB := false
|
||||
if includeSysDBRaw == "true" {
|
||||
//Include the system database in backup snapshot
|
||||
//Temporary set it to read only
|
||||
sysdb.ReadOnly = true
|
||||
includeSysDB = true
|
||||
}
|
||||
|
||||
// Specify the folder path to be zipped
|
||||
folderPath := "./conf/"
|
||||
|
||||
// Set the Content-Type header to indicate it's a zip file
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
// Set the Content-Disposition header to specify the file name
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"")
|
||||
|
||||
// Create a zip writer
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Walk through the folder and add files to the zip
|
||||
err = filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if folderPath == filePath {
|
||||
//Skip root folder
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new file in the zip
|
||||
if !utils.IsDir(filePath) {
|
||||
zipFile, err := zipWriter.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open the file on disk
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy the file contents to the zip file
|
||||
_, err = io.Copy(zipFile, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if includeSysDB {
|
||||
//Also zip in the sysdb
|
||||
zipFile, err := zipWriter.Create("sys.db")
|
||||
if err != nil {
|
||||
log.Println("[Backup] Unable to zip sysdb: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Open the file on disk
|
||||
file, err := os.Open("sys.db")
|
||||
if err != nil {
|
||||
log.Println("[Backup] Unable to open sysdb: " + err.Error())
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy the file contents to the zip file
|
||||
_, err = io.Copy(zipFile, file)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
//Restore sysdb state
|
||||
sysdb.ReadOnly = false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Handle the error and send an HTTP response with the error message
|
||||
http.Error(w, fmt.Sprintf("Failed to zip folder: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is a POST with a file upload
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Invalid request method", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Max file size limit (10 MB in this example)
|
||||
r.ParseMultipartForm(10 << 20)
|
||||
|
||||
// Get the uploaded file
|
||||
file, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to retrieve uploaded file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if filepath.Ext(handler.Filename) != ".zip" {
|
||||
http.Error(w, "Upload file is not a zip file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Create the target directory to unzip the files
|
||||
targetDir := "./conf"
|
||||
if utils.FileExists(targetDir) {
|
||||
//Backup the old config to old
|
||||
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
|
||||
}
|
||||
|
||||
err = os.MkdirAll(targetDir, os.ModePerm)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to create target directory: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Open the zip file
|
||||
zipReader, err := zip.NewReader(file, handler.Size)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to open zip file: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
restoreDatabase := false
|
||||
|
||||
// Extract each file from the zip archive
|
||||
for _, zipFile := range zipReader.File {
|
||||
// Open the file in the zip archive
|
||||
rc, err := zipFile.Open()
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to open file in zip: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Create the corresponding file on disk
|
||||
zipFile.Name = strings.ReplaceAll(zipFile.Name, "../", "")
|
||||
fmt.Println("Restoring: " + strings.ReplaceAll(zipFile.Name, "\\", "/"))
|
||||
if zipFile.Name == "sys.db" {
|
||||
//Sysdb replacement. Close the database and restore
|
||||
sysdb.Close()
|
||||
restoreDatabase = true
|
||||
} else if !strings.HasPrefix(strings.ReplaceAll(zipFile.Name, "\\", "/"), "conf/") {
|
||||
//Malformed zip file.
|
||||
http.Error(w, fmt.Sprintf("Invalid zip file structure or version too old"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
//Check if parent dir exists
|
||||
if !utils.FileExists(filepath.Dir(zipFile.Name)) {
|
||||
os.MkdirAll(filepath.Dir(zipFile.Name), 0775)
|
||||
}
|
||||
|
||||
//Create the file
|
||||
newFile, err := os.Create(zipFile.Name)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to create file: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer newFile.Close()
|
||||
|
||||
// Copy the file contents from the zip to the new file
|
||||
_, err = io.Copy(newFile, rc)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to extract file from zip: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Send a success response
|
||||
w.WriteHeader(http.StatusOK)
|
||||
log.Println("Configuration restored")
|
||||
fmt.Fprintln(w, "Configuration restored")
|
||||
|
||||
if restoreDatabase {
|
||||
go func() {
|
||||
log.Println("Database altered. Restarting in 3 seconds...")
|
||||
time.Sleep(3 * time.Second)
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
}
|
298
src/emails.go
Normal file
@ -0,0 +1,298 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
SMTP Settings and Test Email Handlers
|
||||
*/
|
||||
|
||||
func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
|
||||
hostname, err := utils.PostPara(r, "hostname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "hostname cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "domain cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
portString, err := utils.PostPara(r, "port")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "port must be a valid integer")
|
||||
return
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portString)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "port must be a valid integer")
|
||||
return
|
||||
}
|
||||
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "username cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
//Empty password. Use old one if exists
|
||||
oldConfig := loadSMTPConfig()
|
||||
if oldConfig.Password == "" {
|
||||
utils.SendErrorResponse(w, "password cannot be empty")
|
||||
return
|
||||
} else {
|
||||
password = oldConfig.Password
|
||||
}
|
||||
}
|
||||
|
||||
senderAddr, err := utils.PostPara(r, "senderAddr")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "senderAddr cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
adminAddr, err := utils.PostPara(r, "adminAddr")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "adminAddr cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
//Set the email sender properties
|
||||
thisEmailSender := email.Sender{
|
||||
Hostname: strings.TrimSpace(hostname),
|
||||
Domain: strings.TrimSpace(domain),
|
||||
Port: port,
|
||||
Username: strings.TrimSpace(username),
|
||||
Password: strings.TrimSpace(password),
|
||||
SenderAddr: strings.TrimSpace(senderAddr),
|
||||
}
|
||||
|
||||
//Write this into database
|
||||
setSMTPConfig(&thisEmailSender)
|
||||
|
||||
//Update the current EmailSender
|
||||
EmailSender = &thisEmailSender
|
||||
|
||||
//Set the admin address of password reset
|
||||
setSMTPAdminAddress(adminAddr)
|
||||
|
||||
//Reply ok
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func HandleSMTPGet(w http.ResponseWriter, r *http.Request) {
|
||||
// Create a buffer to store the encoded value
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Encode the original object into the buffer
|
||||
encoder := gob.NewEncoder(&buf)
|
||||
err := encoder.Encode(*EmailSender)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Internal encode error")
|
||||
return
|
||||
}
|
||||
|
||||
// Decode the buffer into a new object
|
||||
var copied email.Sender
|
||||
decoder := gob.NewDecoder(&buf)
|
||||
err = decoder.Decode(&copied)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Internal decode error")
|
||||
return
|
||||
}
|
||||
|
||||
copied.Password = ""
|
||||
|
||||
js, _ := json.Marshal(copied)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func HandleAdminEmailGet(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(loadSMTPAdminAddr())
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func HandleTestEmailSend(w http.ResponseWriter, r *http.Request) {
|
||||
adminEmailAccount := loadSMTPAdminAddr()
|
||||
if adminEmailAccount == "" {
|
||||
utils.SendErrorResponse(w, "Management account not set")
|
||||
return
|
||||
}
|
||||
|
||||
err := EmailSender.SendEmail(adminEmailAccount,
|
||||
"SMTP Testing Email | Zoraxy", "This is a test email sent by Zoraxy. Please do not reply to this email.<br>Zoraxy からのテストメールです。このメールには返信しないでください。<br>這是由 Zoraxy 發送的測試電子郵件。請勿回覆此郵件。<br>Ceci est un email de test envoyé par Zoraxy. Merci de ne pas répondre à cet email.<br>Dies ist eine Test-E-Mail, die von Zoraxy gesendet wurde. Bitte antworten Sie nicht auf diese E-Mail.")
|
||||
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
/*
|
||||
SMTP config
|
||||
|
||||
The following handle SMTP configs
|
||||
*/
|
||||
|
||||
func setSMTPConfig(config *email.Sender) error {
|
||||
return sysdb.Write("smtp", "config", config)
|
||||
}
|
||||
|
||||
func loadSMTPConfig() *email.Sender {
|
||||
if sysdb.KeyExists("smtp", "config") {
|
||||
thisEmailSender := email.Sender{
|
||||
Port: 587,
|
||||
}
|
||||
err := sysdb.Read("smtp", "config", &thisEmailSender)
|
||||
if err != nil {
|
||||
return &email.Sender{
|
||||
Port: 587,
|
||||
}
|
||||
}
|
||||
return &thisEmailSender
|
||||
} else {
|
||||
//Not set. Return an empty one
|
||||
return &email.Sender{
|
||||
Port: 587,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setSMTPAdminAddress(adminAddr string) error {
|
||||
return sysdb.Write("smtp", "admin", adminAddr)
|
||||
}
|
||||
|
||||
//Load SMTP admin address. Return empty string if not set
|
||||
func loadSMTPAdminAddr() string {
|
||||
adminAddr := ""
|
||||
if sysdb.KeyExists("smtp", "admin") {
|
||||
err := sysdb.Read("smtp", "admin", &adminAddr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return adminAddr
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Admin Account Reset
|
||||
*/
|
||||
|
||||
var (
|
||||
accountResetEmailDelay int64 = 30 //Delay between each account reset email, default 30s
|
||||
tokenValidDuration int64 = 15 * 60 //Duration of the token, default 15 minutes
|
||||
lastAccountResetEmail int64 = 0 //Timestamp for last sent account reset email
|
||||
passwordResetAccessToken string = "" //Access token for resetting password
|
||||
)
|
||||
|
||||
func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if EmailSender.Username == "" || EmailSender.Domain == "" {
|
||||
//Reset account not setup
|
||||
utils.SendErrorResponse(w, "Reset account not setup.")
|
||||
return
|
||||
}
|
||||
|
||||
if loadSMTPAdminAddr() == "" {
|
||||
utils.SendErrorResponse(w, "Reset account not setup.")
|
||||
}
|
||||
|
||||
//Check if the delay expired
|
||||
if lastAccountResetEmail+accountResetEmailDelay > time.Now().Unix() {
|
||||
//Too frequent
|
||||
utils.SendErrorResponse(w, "You cannot send another account reset email in cooldown time")
|
||||
return
|
||||
}
|
||||
|
||||
passwordResetAccessToken = uuid.NewV4().String()
|
||||
|
||||
//SMTP info exists. Send reset account email
|
||||
lastAccountResetEmail = time.Now().Unix()
|
||||
EmailSender.SendEmail(loadSMTPAdminAddr(), "Management Account Reset | Zoraxy",
|
||||
"Enter the following reset token to reset your password on your Zoraxy router.<br>"+passwordResetAccessToken+"<br><br> This is an automated generated email. DO NOT REPLY TO THIS EMAIL.")
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func HandleNewPasswordSetup(w http.ResponseWriter, r *http.Request) {
|
||||
if passwordResetAccessToken == "" {
|
||||
//Not initiated
|
||||
utils.SendErrorResponse(w, "Invalid usage")
|
||||
return
|
||||
}
|
||||
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid username given")
|
||||
return
|
||||
}
|
||||
token, err := utils.PostPara(r, "token")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid token given")
|
||||
return
|
||||
}
|
||||
newPassword, err := utils.PostPara(r, "newpw")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid new password given")
|
||||
return
|
||||
}
|
||||
|
||||
token = strings.TrimSpace(token)
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
//Validate the token
|
||||
if token != passwordResetAccessToken {
|
||||
utils.SendErrorResponse(w, "Invalid Token")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if time expired
|
||||
if lastAccountResetEmail+tokenValidDuration < time.Now().Unix() {
|
||||
//Expired
|
||||
utils.SendErrorResponse(w, "Token expired")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if user exists
|
||||
if !authAgent.UserExists(username) {
|
||||
//Invalid admin account name
|
||||
utils.SendErrorResponse(w, "Invalid Username")
|
||||
return
|
||||
}
|
||||
|
||||
//Delete the user account
|
||||
authAgent.UnregisterUser(username)
|
||||
|
||||
//Ok. Set the new password
|
||||
err = authAgent.CreateUserAccount(username, newPassword, "")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
39
src/geoip.go
Normal file
@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
func getCountryCodeFromRequest(r *http.Request) string {
|
||||
countryCode := ""
|
||||
|
||||
// Get the IP address of the user from the request headers
|
||||
ipAddress := r.Header.Get("X-Forwarded-For")
|
||||
if ipAddress == "" {
|
||||
ipAddress = strings.Split(r.RemoteAddr, ":")[0]
|
||||
}
|
||||
|
||||
// Open the GeoIP database
|
||||
db, err := geoip2.Open("./tmp/GeoIP2-Country.mmdb")
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return countryCode
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Look up the country code for the IP address
|
||||
record, err := db.Country(net.ParseIP(ipAddress))
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return countryCode
|
||||
}
|
||||
|
||||
// Get the ISO country code from the record
|
||||
countryCode = record.Country.IsoCode
|
||||
|
||||
return countryCode
|
||||
}
|
19
src/go.mod
Normal file
@ -0,0 +1,19 @@
|
||||
module imuslab.com/zoraxy
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/go-acme/lego/v4 v4.12.1 // indirect
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.0 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.24
|
||||
github.com/oschwald/geoip2-golang v1.8.0
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
golang.org/x/net v0.11.0
|
||||
golang.org/x/sys v0.9.0
|
||||
)
|
1697
src/go.sum
Normal file
173
src/main.go
Normal file
@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/aroz"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
"imuslab.com/zoraxy/mod/sshprox"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/statistic/analytic"
|
||||
"imuslab.com/zoraxy/mod/tcpprox"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// General flags
|
||||
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||
var showver = flag.Bool("version", false, "Show version of this server")
|
||||
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
||||
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
|
||||
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "2.6.5"
|
||||
nodeUUID = "generic"
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
|
||||
/*
|
||||
Binary Embedding File System
|
||||
*/
|
||||
//go:embed web/*
|
||||
webres embed.FS
|
||||
|
||||
/*
|
||||
Handler Modules
|
||||
*/
|
||||
handler *aroz.ArozHandler //Handle arozos managed permission system
|
||||
sysdb *database.Database //System database
|
||||
authAgent *auth.AuthAgent //Authentication agent
|
||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||
redirectTable *redirection.RuleTable //Handle special redirection rule sets
|
||||
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
|
||||
geodbStore *geodb.Store //GeoIP database, also handle black list and whitelist features
|
||||
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
|
||||
statisticCollector *statistic.Collector //Collecting statistic from visitors
|
||||
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
|
||||
mdnsScanner *mdns.MDNSHost //mDNS discovery services
|
||||
ganManager *ganserv.NetworkManager //Global Area Network Manager
|
||||
webSshManager *sshprox.Manager //Web SSH connection service
|
||||
tcpProxyManager *tcpprox.Manager //TCP Proxy Manager
|
||||
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
||||
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
||||
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||
)
|
||||
|
||||
// Kill signal handler. Do something before the system the core terminate.
|
||||
func SetupCloseHandler() {
|
||||
c := make(chan os.Signal, 2)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
ShutdownSeq()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func ShutdownSeq() {
|
||||
fmt.Println("- Shutting down " + name)
|
||||
fmt.Println("- Closing GeoDB ")
|
||||
geodbStore.Close()
|
||||
fmt.Println("- Closing Netstats Listener")
|
||||
netstatBuffers.Close()
|
||||
fmt.Println("- Closing Statistic Collector")
|
||||
statisticCollector.Close()
|
||||
fmt.Println("- Stopping mDNS Discoverer")
|
||||
//Stop the mdns service
|
||||
mdnsTickerStop <- true
|
||||
mdnsScanner.Close()
|
||||
fmt.Println("- Closing Certificates Auto Renewer")
|
||||
acmeAutoRenewer.Close()
|
||||
//Remove the tmp folder
|
||||
fmt.Println("- Cleaning up tmp files")
|
||||
os.RemoveAll("./tmp")
|
||||
|
||||
//Close database, final
|
||||
fmt.Println("- Stopping system database")
|
||||
sysdb.Close()
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
||||
Name: name,
|
||||
Desc: "Dynamic Reverse Proxy Server",
|
||||
Group: "Network",
|
||||
IconPath: "zoraxy/img/small_icon.png",
|
||||
Version: version,
|
||||
StartDir: "zoraxy/index.html",
|
||||
SupportFW: true,
|
||||
LaunchFWDir: "zoraxy/index.html",
|
||||
SupportEmb: false,
|
||||
InitFWSize: []int{1080, 580},
|
||||
})
|
||||
|
||||
if *showver {
|
||||
fmt.Println(name + " - Version " + version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
SetupCloseHandler()
|
||||
|
||||
//Read or create the system uuid
|
||||
uuidRecord := "./sys.uuid"
|
||||
if !utils.FileExists(uuidRecord) {
|
||||
newSystemUUID := uuid.New().String()
|
||||
os.WriteFile(uuidRecord, []byte(newSystemUUID), 0775)
|
||||
}
|
||||
uuidBytes, err := os.ReadFile(uuidRecord)
|
||||
if err != nil {
|
||||
log.Println("Unable to read system uuid from file system")
|
||||
panic(err)
|
||||
}
|
||||
nodeUUID = string(uuidBytes)
|
||||
|
||||
//Startup all modules
|
||||
startupSequence()
|
||||
|
||||
//Initiate management interface APIs
|
||||
requireAuth = !(*noauth || handler.IsUsingExternalPermissionManager())
|
||||
initAPIs()
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
go func() {
|
||||
ReverseProxtInit()
|
||||
}()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
//Start the finalize sequences
|
||||
finalSequence()
|
||||
|
||||
log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
|
||||
err = http.ListenAndServe(handler.Port, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
288
src/mod/acme/acme.go
Normal file
@ -0,0 +1,288 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge/http01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// ACMEUser represents a user in the ACME system.
|
||||
type ACMEUser struct {
|
||||
Email string
|
||||
Registration *registration.Resource
|
||||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
// GetEmail returns the email of the ACMEUser.
|
||||
func (u *ACMEUser) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
// GetRegistration returns the registration resource of the ACMEUser.
|
||||
func (u ACMEUser) GetRegistration() *registration.Resource {
|
||||
return u.Registration
|
||||
}
|
||||
|
||||
// GetPrivateKey returns the private key of the ACMEUser.
|
||||
func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
|
||||
return u.key
|
||||
}
|
||||
|
||||
// ACMEHandler handles ACME-related operations.
|
||||
type ACMEHandler struct {
|
||||
DefaultAcmeServer string
|
||||
Port string
|
||||
}
|
||||
|
||||
// NewACME creates a new ACMEHandler instance.
|
||||
func NewACME(acmeServer string, port string) *ACMEHandler {
|
||||
return &ACMEHandler{
|
||||
DefaultAcmeServer: acmeServer,
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
// ObtainCert obtains a certificate for the specified domains.
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, ca string) (bool, error) {
|
||||
log.Println("[ACME] Obtaining certificate...")
|
||||
|
||||
// generate private key
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// create a admin user for our new generation
|
||||
adminUser := ACMEUser{
|
||||
Email: email,
|
||||
key: privateKey,
|
||||
}
|
||||
|
||||
// create config
|
||||
config := lego.NewConfig(&adminUser)
|
||||
|
||||
// setup who is the issuer and the key type
|
||||
config.CADirURL = a.DefaultAcmeServer
|
||||
|
||||
//Overwrite the CADir URL if set
|
||||
if ca != "" {
|
||||
caLinkOverwrite, err := loadCAApiServerFromName(ca)
|
||||
if err == nil {
|
||||
config.CADirURL = caLinkOverwrite
|
||||
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
|
||||
} else {
|
||||
return false, errors.New("CA " + ca + " is not supported. Please contribute to the source code and add this CA's directory link.")
|
||||
}
|
||||
}
|
||||
|
||||
config.Certificate.KeyType = certcrypto.RSA2048
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// setup how to receive challenge
|
||||
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// New users will need to register
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
adminUser.Registration = reg
|
||||
|
||||
// obtain the certificate
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
Bundle: true,
|
||||
}
|
||||
certificates, err := client.Certificate.Obtain(request)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Each certificate comes back with the cert bytes, the bytes of the client's
|
||||
// private key, and a certificate URL.
|
||||
err = ioutil.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
err = ioutil.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckCertificate returns a list of domains that are in expired certificates.
|
||||
// It will return all domains that is in expired certificates
|
||||
// *** if there is a vaild certificate contains the domain and there is a expired certificate contains the same domain
|
||||
// it will said expired as well!
|
||||
func (a *ACMEHandler) CheckCertificate() []string {
|
||||
// read from dir
|
||||
filenames, err := os.ReadDir("./conf/certs/")
|
||||
|
||||
expiredCerts := []string{}
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
for _, filename := range filenames {
|
||||
certFilepath := filepath.Join("./conf/certs/", filename.Name())
|
||||
|
||||
certBytes, err := os.ReadFile(certFilepath)
|
||||
if err != nil {
|
||||
// Unable to load this file
|
||||
continue
|
||||
} else {
|
||||
// Cert loaded. Check its expiry time
|
||||
block, _ := pem.Decode(certBytes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
elapsed := time.Since(cert.NotAfter)
|
||||
if elapsed > 0 {
|
||||
// if it is expired then add it in
|
||||
// make sure it's uniqueless
|
||||
for _, dnsName := range cert.DNSNames {
|
||||
if !contains(expiredCerts, dnsName) {
|
||||
expiredCerts = append(expiredCerts, dnsName)
|
||||
}
|
||||
}
|
||||
if !contains(expiredCerts, cert.Subject.CommonName) {
|
||||
expiredCerts = append(expiredCerts, cert.Subject.CommonName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expiredCerts
|
||||
}
|
||||
|
||||
// return the current port number
|
||||
func (a *ACMEHandler) Getport() string {
|
||||
return a.Port
|
||||
}
|
||||
|
||||
// contains checks if a string is present in a slice.
|
||||
func contains(slice []string, str string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HandleGetExpiredDomains handles the HTTP GET request to retrieve the list of expired domains.
|
||||
// It calls the CheckCertificate method to obtain the expired domains and sends a JSON response
|
||||
// containing the list of expired domains.
|
||||
func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Request) {
|
||||
type ExpiredDomains struct {
|
||||
Domain []string `json:"domain"`
|
||||
}
|
||||
|
||||
info := ExpiredDomains{
|
||||
Domain: a.CheckCertificate(),
|
||||
}
|
||||
|
||||
js, _ := json.MarshalIndent(info, "", " ")
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// HandleRenewCertificate handles the HTTP GET request to renew a certificate for the provided domains.
|
||||
// It retrieves the domains and filename parameters from the request, calls the ObtainCert method
|
||||
// to renew the certificate, and sends a JSON response indicating the result of the renewal process.
|
||||
func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
domainPara, err := utils.PostPara(r, "domains")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
filename, err := utils.PostPara(r, "filename")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
email, err := utils.PostPara(r, "email")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
ca, err := utils.PostPara(r, "ca")
|
||||
if err != nil {
|
||||
log.Println("CA not set. Using default (Let's Encrypt)")
|
||||
ca = "Let's Encrypt"
|
||||
}
|
||||
|
||||
domains := strings.Split(domainPara, ",")
|
||||
result, err := a.ObtainCert(domains, filename, email, ca)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
}
|
||||
utils.SendJSONResponse(w, strconv.FormatBool(result))
|
||||
}
|
||||
|
||||
// Escape JSON string
|
||||
func jsonEscape(i string) string {
|
||||
b, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
log.Println("Unable to escape json data: " + err.Error())
|
||||
return i
|
||||
}
|
||||
s := string(b)
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
|
||||
// Helper function to check if a port is in use
|
||||
func IsPortInUse(port int) bool {
|
||||
address := fmt.Sprintf(":%d", port)
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return true // Port is in use
|
||||
}
|
||||
defer listener.Close()
|
||||
return false // Port is not in use
|
||||
}
|
24
src/mod/acme/acme_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package acme_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
)
|
||||
|
||||
// Test if the issuer extraction is working
|
||||
func TestExtractIssuerNameFromPEM(t *testing.T) {
|
||||
pemFilePath := "test/stackoverflow.pem"
|
||||
expectedIssuer := "Let's Encrypt"
|
||||
|
||||
issuerName, err := acme.ExtractIssuerNameFromPEM(pemFilePath)
|
||||
fmt.Println(issuerName)
|
||||
if err != nil {
|
||||
t.Errorf("Error extracting issuer name: %v", err)
|
||||
}
|
||||
|
||||
if issuerName != expectedIssuer {
|
||||
t.Errorf("Unexpected issuer name. Expected: %s, Got: %s", expectedIssuer, issuerName)
|
||||
}
|
||||
}
|
163
src/mod/acme/acmewizard/acmewizard.go
Normal file
@ -0,0 +1,163 @@
|
||||
package acmewizard
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
ACME Wizard
|
||||
|
||||
This wizard help validate the acme settings and configurations
|
||||
*/
|
||||
|
||||
func HandleGuidedStepCheck(w http.ResponseWriter, r *http.Request) {
|
||||
stepNoStr, err := utils.GetPara(r, "step")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid step number given")
|
||||
return
|
||||
}
|
||||
|
||||
stepNo, err := strconv.Atoi(stepNoStr)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid step number given")
|
||||
return
|
||||
}
|
||||
|
||||
if stepNo == 1 {
|
||||
isListening, err := isLocalhostListening()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(isListening)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if stepNo == 2 {
|
||||
publicIp, err := getPublicIPAddress()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
publicIp = strings.TrimSpace(publicIp)
|
||||
|
||||
httpServerReachable := isHTTPServerAvailable(publicIp)
|
||||
|
||||
js, _ := json.Marshal(httpServerReachable)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if stepNo == 3 {
|
||||
domain, err := utils.GetPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "domain cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
domain = strings.TrimSpace(domain)
|
||||
|
||||
//Check if the domain is reachable
|
||||
reachable := isDomainReachable(domain)
|
||||
if !reachable {
|
||||
utils.SendErrorResponse(w, "domain is not reachable")
|
||||
return
|
||||
}
|
||||
|
||||
//Check http is setup correctly
|
||||
httpServerReachable := isHTTPServerAvailable(domain)
|
||||
js, _ := json.Marshal(httpServerReachable)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid step number")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1
|
||||
func isLocalhostListening() (isListening bool, err error) {
|
||||
timeout := 2 * time.Second
|
||||
isListening = false
|
||||
// Check if localhost is listening on port 80 (HTTP)
|
||||
conn, err := net.DialTimeout("tcp", "localhost:80", timeout)
|
||||
if err == nil {
|
||||
isListening = true
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
// Check if localhost is listening on port 443 (HTTPS)
|
||||
conn, err = net.DialTimeout("tcp", "localhost:443", timeout)
|
||||
if err == nil {
|
||||
isListening = true
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
if isListening {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return isListening, err
|
||||
}
|
||||
|
||||
// Step 2
|
||||
func getPublicIPAddress() (string, error) {
|
||||
resp, err := http.Get("http://checkip.amazonaws.com/")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
ip, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(ip), nil
|
||||
}
|
||||
|
||||
func isHTTPServerAvailable(ipAddress string) bool {
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second, // Timeout for the HTTP request
|
||||
}
|
||||
|
||||
urls := []string{
|
||||
"http://" + ipAddress + ":80",
|
||||
"https://" + ipAddress + ":443",
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err, url)
|
||||
continue // Ignore invalid URLs
|
||||
}
|
||||
|
||||
// Disable TLS verification to handle invalid certificates
|
||||
client.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
return true // HTTP server is available
|
||||
}
|
||||
}
|
||||
|
||||
return false // HTTP server is not available
|
||||
}
|
||||
|
||||
// Step 3
|
||||
func isDomainReachable(domain string) bool {
|
||||
_, err := net.LookupHost(domain)
|
||||
if err != nil {
|
||||
return false // Domain is not reachable
|
||||
}
|
||||
return true // Domain is reachable
|
||||
}
|
374
src/mod/acme/autorenew.go
Normal file
@ -0,0 +1,374 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
autorenew.go
|
||||
|
||||
This script handle auto renew
|
||||
*/
|
||||
|
||||
type AutoRenewConfig struct {
|
||||
Enabled bool //Automatic renew is enabled
|
||||
Email string //Email for acme
|
||||
RenewAll bool //Renew all or selective renew with the slice below
|
||||
FilesToRenew []string //If RenewAll is false, renew these certificate files
|
||||
}
|
||||
|
||||
type AutoRenewer struct {
|
||||
ConfigFilePath string
|
||||
CertFolder string
|
||||
AcmeHandler *ACMEHandler
|
||||
RenewerConfig *AutoRenewConfig
|
||||
RenewTickInterval int64
|
||||
TickerstopChan chan bool
|
||||
}
|
||||
|
||||
type ExpiredCerts struct {
|
||||
Domains []string
|
||||
Filepath string
|
||||
CA string
|
||||
}
|
||||
|
||||
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
|
||||
// Set renew check interval to 0 for auto (1 day)
|
||||
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, AcmeHandler *ACMEHandler) (*AutoRenewer, error) {
|
||||
if renewCheckInterval == 0 {
|
||||
renewCheckInterval = 86400 //1 day
|
||||
}
|
||||
|
||||
//Load the config file. If not found, create one
|
||||
if !utils.FileExists(config) {
|
||||
//Create one
|
||||
os.MkdirAll(filepath.Dir(config), 0775)
|
||||
newConfig := AutoRenewConfig{
|
||||
RenewAll: true,
|
||||
FilesToRenew: []string{},
|
||||
}
|
||||
js, _ := json.MarshalIndent(newConfig, "", " ")
|
||||
err := os.WriteFile(config, js, 0775)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to create acme auto renewer config: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
renewerConfig := AutoRenewConfig{}
|
||||
content, err := os.ReadFile(config)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to open acme auto renewer config: " + err.Error())
|
||||
}
|
||||
|
||||
err = json.Unmarshal(content, &renewerConfig)
|
||||
if err != nil {
|
||||
return nil, errors.New("Malformed acme config file: " + err.Error())
|
||||
}
|
||||
|
||||
//Create an Auto renew object
|
||||
thisRenewer := AutoRenewer{
|
||||
ConfigFilePath: config,
|
||||
CertFolder: certFolder,
|
||||
AcmeHandler: AcmeHandler,
|
||||
RenewerConfig: &renewerConfig,
|
||||
RenewTickInterval: renewCheckInterval,
|
||||
}
|
||||
|
||||
if thisRenewer.RenewerConfig.Enabled {
|
||||
//Start the renew ticker
|
||||
thisRenewer.StartAutoRenewTicker()
|
||||
|
||||
//Check and renew certificate on startup
|
||||
go thisRenewer.CheckAndRenewCertificates()
|
||||
}
|
||||
|
||||
return &thisRenewer, nil
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) StartAutoRenewTicker() {
|
||||
//Stop the previous ticker if still running
|
||||
if a.TickerstopChan != nil {
|
||||
a.TickerstopChan <- true
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
ticker := time.NewTicker(time.Duration(a.RenewTickInterval) * time.Second)
|
||||
done := make(chan bool)
|
||||
|
||||
//Start the ticker to check and renew every x seconds
|
||||
go func(a *AutoRenewer) {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
log.Println("Check and renew certificates in progress")
|
||||
a.CheckAndRenewCertificates()
|
||||
}
|
||||
}
|
||||
}(a)
|
||||
|
||||
a.TickerstopChan = done
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) StopAutoRenewTicker() {
|
||||
if a.TickerstopChan != nil {
|
||||
a.TickerstopChan <- true
|
||||
}
|
||||
|
||||
a.TickerstopChan = nil
|
||||
}
|
||||
|
||||
// Handle update auto renew domains
|
||||
// Set opr for different mode of operations
|
||||
// opr = setSelected -> Enter a list of file names (or matching rules) for auto renew
|
||||
// opr = setAuto -> Set to use auto detect certificates and renew
|
||||
func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
|
||||
opr, err := utils.GetPara(r, "opr")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Operation not set")
|
||||
return
|
||||
}
|
||||
|
||||
if opr == "setSelected" {
|
||||
files, err := utils.PostPara(r, "domains")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Domains is not defined")
|
||||
return
|
||||
}
|
||||
|
||||
//Parse it int array of string
|
||||
matchingRuleFiles := []string{}
|
||||
err = json.Unmarshal([]byte(files), &matchingRuleFiles)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Update the configs
|
||||
a.RenewerConfig.RenewAll = false
|
||||
a.RenewerConfig.FilesToRenew = matchingRuleFiles
|
||||
a.saveRenewConfigToFile()
|
||||
utils.SendOK(w)
|
||||
} else if opr == "setAuto" {
|
||||
a.RenewerConfig.RenewAll = true
|
||||
a.saveRenewConfigToFile()
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// if auto renew all is true (aka auto scan), it will return []string{"*"}
|
||||
func (a *AutoRenewer) HandleLoadAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
|
||||
results := []string{}
|
||||
if a.RenewerConfig.RenewAll {
|
||||
//Auto pick which cert to renew.
|
||||
results = append(results, "*")
|
||||
} else {
|
||||
//Manually set the files to renew
|
||||
results = a.RenewerConfig.FilesToRenew
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) HandleRenewPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
//Load the current value
|
||||
js, _ := json.Marshal(a.RenewerConfig.RenewAll)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
|
||||
renewedDomains, err := a.CheckAndRenewCertificates()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
message := "Domains renewed"
|
||||
if len(renewedDomains) == 0 {
|
||||
message = ("All certificates are up-to-date!")
|
||||
} else {
|
||||
message = ("The following domains have been renewed: " + strings.Join(renewedDomains, ","))
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(message)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
|
||||
val, err := utils.PostPara(r, "enable")
|
||||
if err != nil {
|
||||
js, _ := json.Marshal(a.RenewerConfig.Enabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if val == "true" {
|
||||
//Check if the email is not empty
|
||||
if a.RenewerConfig.Email == "" {
|
||||
utils.SendErrorResponse(w, "Email is not set")
|
||||
return
|
||||
}
|
||||
|
||||
a.RenewerConfig.Enabled = true
|
||||
a.saveRenewConfigToFile()
|
||||
log.Println("[ACME] ACME auto renew enabled")
|
||||
a.StartAutoRenewTicker()
|
||||
} else {
|
||||
a.RenewerConfig.Enabled = false
|
||||
a.saveRenewConfigToFile()
|
||||
log.Println("[ACME] ACME auto renew disabled")
|
||||
a.StopAutoRenewTicker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
email, err := utils.PostPara(r, "set")
|
||||
if err != nil {
|
||||
//Return the current email to user
|
||||
js, _ := json.Marshal(a.RenewerConfig.Email)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
//Check if the email is valid
|
||||
_, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Set the new config
|
||||
a.RenewerConfig.Email = email
|
||||
a.saveRenewConfigToFile()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Check and renew certificates. This check all the certificates in the
|
||||
// certificate folder and return a list of certs that is renewed in this call
|
||||
// Return string array with length 0 when no cert is expired
|
||||
func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
certFolder := a.CertFolder
|
||||
files, err := os.ReadDir(certFolder)
|
||||
if err != nil {
|
||||
log.Println("Unable to renew certificates: " + err.Error())
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
expiredCertList := []*ExpiredCerts{}
|
||||
if a.RenewerConfig.RenewAll {
|
||||
//Scan and renew all
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file.Name()) == ".crt" || filepath.Ext(file.Name()) == ".pem" {
|
||||
//This is a public key file
|
||||
certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
|
||||
//This cert is expired
|
||||
CAName, err := ExtractIssuerName(certBytes)
|
||||
if err != nil {
|
||||
//Maybe self signed. Ignore this
|
||||
log.Println("Unable to extract issuer name for cert " + file.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
DNSName, err := ExtractDomains(certBytes)
|
||||
if err != nil {
|
||||
//Maybe self signed. Ignore this
|
||||
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
expiredCertList = append(expiredCertList, &ExpiredCerts{
|
||||
Filepath: filepath.Join(certFolder, file.Name()),
|
||||
CA: CAName,
|
||||
Domains: DNSName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//Only renew those in the list
|
||||
for _, file := range files {
|
||||
fileName := file.Name()
|
||||
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
|
||||
if contains(a.RenewerConfig.FilesToRenew, certName) {
|
||||
//This is the one to auto renew
|
||||
certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
|
||||
//This cert is expired
|
||||
CAName, err := ExtractIssuerName(certBytes)
|
||||
if err != nil {
|
||||
//Maybe self signed. Ignore this
|
||||
log.Println("Unable to extract issuer name for cert " + file.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
DNSName, err := ExtractDomains(certBytes)
|
||||
if err != nil {
|
||||
//Maybe self signed. Ignore this
|
||||
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
expiredCertList = append(expiredCertList, &ExpiredCerts{
|
||||
Filepath: filepath.Join(certFolder, file.Name()),
|
||||
CA: CAName,
|
||||
Domains: DNSName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return a.renewExpiredDomains(expiredCertList)
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) Close() {
|
||||
if a.TickerstopChan != nil {
|
||||
a.TickerstopChan <- true
|
||||
}
|
||||
}
|
||||
|
||||
// Renew the certificate by filename extract all DNS name from the
|
||||
// certificate and renew them one by one by calling to the acmeHandler
|
||||
func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
|
||||
renewedCertFiles := []string{}
|
||||
for _, expiredCert := range certs {
|
||||
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
|
||||
fileName := filepath.Base(expiredCert.Filepath)
|
||||
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
|
||||
_, err := a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, expiredCert.CA)
|
||||
if err != nil {
|
||||
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
|
||||
} else {
|
||||
log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath))
|
||||
renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
|
||||
}
|
||||
}
|
||||
|
||||
return renewedCertFiles, nil
|
||||
}
|
||||
|
||||
// Write the current renewer config to file
|
||||
func (a *AutoRenewer) saveRenewConfigToFile() error {
|
||||
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
|
||||
return os.WriteFile(a.ConfigFilePath, js, 0775)
|
||||
}
|
45
src/mod/acme/ca.go
Normal file
@ -0,0 +1,45 @@
|
||||
package acme
|
||||
|
||||
/*
|
||||
CA.go
|
||||
|
||||
This script load CA defination from embedded ca.json
|
||||
*/
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
// CA Defination, load from embeded json when startup
|
||||
type CaDef struct {
|
||||
Production map[string]string
|
||||
Test map[string]string
|
||||
}
|
||||
|
||||
//go:embed ca.json
|
||||
var caJson []byte
|
||||
|
||||
var caDef CaDef = CaDef{}
|
||||
|
||||
func init() {
|
||||
runtimeCaDef := CaDef{}
|
||||
err := json.Unmarshal(caJson, &runtimeCaDef)
|
||||
if err != nil {
|
||||
log.Println("[ERR] Unable to unmarshal CA def from embedded file. You sure your ca.json is valid?")
|
||||
return
|
||||
}
|
||||
|
||||
caDef = runtimeCaDef
|
||||
|
||||
}
|
||||
|
||||
// Get the CA ACME server endpoint and error if not found
|
||||
func loadCAApiServerFromName(caName string) (string, error) {
|
||||
val, ok := caDef.Production[caName]
|
||||
if !ok {
|
||||
return "", errors.New("This CA is not supported")
|
||||
}
|
||||
return val, nil
|
||||
}
|
15
src/mod/acme/ca.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"production": {
|
||||
"Let's Encrypt": "https://acme-v02.api.letsencrypt.org/directory",
|
||||
"Buypass": "https://api.buypass.com/acme/directory",
|
||||
"ZeroSSL": "https://acme.zerossl.com/v2/DV90",
|
||||
"Google": "https://dv.acme-v02.api.pki.goog/directory"
|
||||
},
|
||||
"test":{
|
||||
"Let's Encrypt": "https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||
"Buypass": "https://api.test4.buypass.no/acme/directory",
|
||||
"Google": "https://dv.acme-v02.test-api.pki.goog/directory"
|
||||
}
|
||||
}
|
||||
|
||||
|
94
src/mod/acme/utils.go
Normal file
@ -0,0 +1,94 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Get the issuer name from pem file
|
||||
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
|
||||
// Read the PEM file
|
||||
pemData, err := ioutil.ReadFile(pemFilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ExtractIssuerName(pemData)
|
||||
}
|
||||
|
||||
// Get the DNSName in the cert
|
||||
func ExtractDomains(certBytes []byte) ([]string, error) {
|
||||
domains := []string{}
|
||||
block, _ := pem.Decode(certBytes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
for _, dnsName := range cert.DNSNames {
|
||||
if !contains(domains, dnsName) {
|
||||
domains = append(domains, dnsName)
|
||||
}
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
return []string{}, errors.New("decode cert bytes failed")
|
||||
}
|
||||
|
||||
func ExtractIssuerName(certBytes []byte) (string, error) {
|
||||
// Parse the PEM block
|
||||
block, _ := pem.Decode(certBytes)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
return "", fmt.Errorf("failed to decode PEM block containing certificate")
|
||||
}
|
||||
|
||||
// Parse the certificate
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse certificate: %v", err)
|
||||
}
|
||||
|
||||
// Extract the issuer name
|
||||
issuer := cert.Issuer.Organization[0]
|
||||
|
||||
return issuer, nil
|
||||
}
|
||||
|
||||
// Check if a cert is expired by public key
|
||||
func CertIsExpired(certBytes []byte) bool {
|
||||
block, _ := pem.Decode(certBytes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
elapsed := time.Since(cert.NotAfter)
|
||||
if elapsed > 0 {
|
||||
// if it is expired then add it in
|
||||
// make sure it's uniqueless
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CertExpireSoon(certBytes []byte) bool {
|
||||
block, _ := pem.Decode(certBytes)
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
expirationDate := cert.NotAfter
|
||||
threshold := 14 * 24 * time.Hour // 14 days
|
||||
|
||||
timeRemaining := time.Until(expirationDate)
|
||||
if timeRemaining <= threshold {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
76
src/mod/aroz/aroz.go
Normal file
@ -0,0 +1,76 @@
|
||||
package aroz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
//To be used with arozos system
|
||||
type ArozHandler struct {
|
||||
Port string
|
||||
restfulEndpoint string
|
||||
}
|
||||
|
||||
//Information required for registering this subservice to arozos
|
||||
type ServiceInfo struct {
|
||||
Name string //Name of this module. e.g. "Audio"
|
||||
Desc string //Description for this module
|
||||
Group string //Group of the module, e.g. "system" / "media" etc
|
||||
IconPath string //Module icon image path e.g. "Audio/img/function_icon.png"
|
||||
Version string //Version of the module. Format: [0-9]*.[0-9][0-9].[0-9]
|
||||
StartDir string //Default starting dir, e.g. "Audio/index.html"
|
||||
SupportFW bool //Support floatWindow. If yes, floatWindow dir will be loaded
|
||||
LaunchFWDir string //This link will be launched instead of 'StartDir' if fw mode
|
||||
SupportEmb bool //Support embedded mode
|
||||
LaunchEmb string //This link will be launched instead of StartDir / Fw if a file is opened with this module
|
||||
InitFWSize []int //Floatwindow init size. [0] => Width, [1] => Height
|
||||
InitEmbSize []int //Embedded mode init size. [0] => Width, [1] => Height
|
||||
SupportedExt []string //Supported File Extensions. e.g. ".mp3", ".flac", ".wav"
|
||||
}
|
||||
|
||||
//This function will request the required flag from the startup paramters and parse it to the need of the arozos.
|
||||
func HandleFlagParse(info ServiceInfo) *ArozHandler {
|
||||
var infoRequestMode = flag.Bool("info", false, "Show information about this program in JSON")
|
||||
var port = flag.String("port", ":8000", "Management web interface listening port")
|
||||
var restful = flag.String("rpt", "", "Reserved")
|
||||
//Parse the flags
|
||||
flag.Parse()
|
||||
if *infoRequestMode {
|
||||
//Information request mode
|
||||
jsonString, _ := json.MarshalIndent(info, "", " ")
|
||||
fmt.Println(string(jsonString))
|
||||
os.Exit(0)
|
||||
}
|
||||
return &ArozHandler{
|
||||
Port: *port,
|
||||
restfulEndpoint: *restful,
|
||||
}
|
||||
}
|
||||
|
||||
//Get the username and resources access token from the request, return username, token
|
||||
func (a *ArozHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Request) (string, string) {
|
||||
username := r.Header.Get("aouser")
|
||||
token := r.Header.Get("aotoken")
|
||||
|
||||
return username, token
|
||||
}
|
||||
|
||||
func (a *ArozHandler) IsUsingExternalPermissionManager() bool {
|
||||
return !(a.restfulEndpoint == "")
|
||||
}
|
||||
|
||||
//Request gateway interface for advance permission sandbox control
|
||||
func (a *ArozHandler) RequestGatewayInterface(token string, script string) (*http.Response, error) {
|
||||
resp, err := http.PostForm(a.restfulEndpoint,
|
||||
url.Values{"token": {token}, "script": {script}})
|
||||
if err != nil {
|
||||
// handle error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
BIN
src/mod/aroz/doc.txt
Normal file
478
src/mod/auth/auth.go
Normal file
@ -0,0 +1,478 @@
|
||||
package auth
|
||||
|
||||
/*
|
||||
|
||||
author: tobychui
|
||||
*/
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"encoding/hex"
|
||||
"log"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
db "imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type AuthAgent struct {
|
||||
//Session related
|
||||
SessionName string
|
||||
SessionStore *sessions.CookieStore
|
||||
Database *db.Database
|
||||
LoginRedirectionHandler func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type AuthEndpoints struct {
|
||||
Login string
|
||||
Logout string
|
||||
Register string
|
||||
CheckLoggedIn string
|
||||
Autologin string
|
||||
}
|
||||
|
||||
//Constructor
|
||||
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {
|
||||
store := sessions.NewCookieStore(key)
|
||||
err := sysdb.NewTable("auth")
|
||||
if err != nil {
|
||||
log.Println("Failed to create auth database. Terminating.")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a new AuthAgent object
|
||||
newAuthAgent := AuthAgent{
|
||||
SessionName: sessionName,
|
||||
SessionStore: store,
|
||||
Database: sysdb,
|
||||
LoginRedirectionHandler: loginRedirectionHandler,
|
||||
}
|
||||
|
||||
//Return the authAgent
|
||||
return &newAuthAgent
|
||||
}
|
||||
|
||||
func GetSessionKey(sysdb *db.Database) (string, error) {
|
||||
sysdb.NewTable("auth")
|
||||
sessionKey := ""
|
||||
if !sysdb.KeyExists("auth", "sessionkey") {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
sessionKey = string(key)
|
||||
sysdb.Write("auth", "sessionkey", sessionKey)
|
||||
log.Println("[Auth] New authentication session key generated")
|
||||
} else {
|
||||
log.Println("[Auth] Authentication session key loaded from database")
|
||||
err := sysdb.Read("auth", "sessionkey", &sessionKey)
|
||||
if err != nil {
|
||||
return "", errors.New("database read error. Is the database file corrupted?")
|
||||
}
|
||||
}
|
||||
return sessionKey, nil
|
||||
}
|
||||
|
||||
//This function will handle an http request and redirect to the given login address if not logged in
|
||||
func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
|
||||
if a.CheckAuth(r) {
|
||||
//User already logged in
|
||||
handler(w, r)
|
||||
} else {
|
||||
//User not logged in
|
||||
a.LoginRedirectionHandler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
//Handle login request, require POST username and password
|
||||
func (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
//Get username from request using POST mode
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
//Username not defined
|
||||
log.Println("[Auth] " + r.RemoteAddr + " trying to login with username: " + username)
|
||||
utils.SendErrorResponse(w, "Username not defined or empty.")
|
||||
return
|
||||
}
|
||||
|
||||
//Get password from request using POST mode
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
//Password not defined
|
||||
utils.SendErrorResponse(w, "Password not defined or empty.")
|
||||
return
|
||||
}
|
||||
|
||||
//Get rememberme settings
|
||||
rememberme := false
|
||||
rmbme, _ := utils.PostPara(r, "rmbme")
|
||||
if rmbme == "true" {
|
||||
rememberme = true
|
||||
}
|
||||
|
||||
//Check the database and see if this user is in the database
|
||||
passwordCorrect, rejectionReason := a.ValidateUsernameAndPasswordWithReason(username, password)
|
||||
//The database contain this user information. Check its password if it is correct
|
||||
if passwordCorrect {
|
||||
//Password correct
|
||||
// Set user as authenticated
|
||||
a.LoginUserByRequest(w, r, username, rememberme)
|
||||
|
||||
//Print the login message to console
|
||||
log.Println(username + " logged in.")
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
//Password incorrect
|
||||
log.Println(username + " login request rejected: " + rejectionReason)
|
||||
|
||||
utils.SendErrorResponse(w, rejectionReason)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string) bool {
|
||||
succ, _ := a.ValidateUsernameAndPasswordWithReason(username, password)
|
||||
return succ
|
||||
}
|
||||
|
||||
//validate the username and password, return reasons if the auth failed
|
||||
func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, password string) (bool, string) {
|
||||
hashedPassword := Hash(password)
|
||||
var passwordInDB string
|
||||
err := a.Database.Read("auth", "passhash/"+username, &passwordInDB)
|
||||
if err != nil {
|
||||
//User not found or db exception
|
||||
log.Println("[Auth] " + username + " login with incorrect password")
|
||||
return false, "Invalid username or password"
|
||||
}
|
||||
|
||||
if passwordInDB == hashedPassword {
|
||||
return true, ""
|
||||
} else {
|
||||
return false, "Invalid username or password"
|
||||
}
|
||||
}
|
||||
|
||||
//Login the user by creating a valid session for this user
|
||||
func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, username string, rememberme bool) {
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
|
||||
session.Values["authenticated"] = true
|
||||
session.Values["username"] = username
|
||||
session.Values["rememberMe"] = rememberme
|
||||
|
||||
//Check if remember me is clicked. If yes, set the maxage to 1 week.
|
||||
if rememberme {
|
||||
session.Options = &sessions.Options{
|
||||
MaxAge: 3600 * 24 * 7, //One week
|
||||
Path: "/",
|
||||
}
|
||||
} else {
|
||||
session.Options = &sessions.Options{
|
||||
MaxAge: 3600 * 1, //One hour
|
||||
Path: "/",
|
||||
}
|
||||
}
|
||||
session.Save(r, w)
|
||||
}
|
||||
|
||||
//Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION
|
||||
func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := a.GetUserName(w, r)
|
||||
if username != "" {
|
||||
log.Println(username + " logged out.")
|
||||
}
|
||||
// Revoke users authentication
|
||||
err = a.Logout(w, r)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Logout failed")
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
session, err := a.SessionStore.Get(r, a.SessionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.Values["authenticated"] = false
|
||||
session.Values["username"] = nil
|
||||
session.Save(r, w)
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get the current session username from request
|
||||
func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
if a.CheckAuth(r) {
|
||||
//This user has logged in.
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
return session.Values["username"].(string), nil
|
||||
} else {
|
||||
//This user has not logged in.
|
||||
return "", errors.New("user not logged in")
|
||||
}
|
||||
}
|
||||
|
||||
//Get the current session user email from request
|
||||
func (a *AuthAgent) GetUserEmail(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
if a.CheckAuth(r) {
|
||||
//This user has logged in.
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
username := session.Values["username"].(string)
|
||||
userEmail := ""
|
||||
err := a.Database.Read("auth", "email/"+username, &userEmail)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return userEmail, nil
|
||||
} else {
|
||||
//This user has not logged in.
|
||||
return "", errors.New("user not logged in")
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the user has logged in, return true / false in JSON
|
||||
func (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if a.CheckAuth(r) {
|
||||
utils.SendJSONResponse(w, "true")
|
||||
} else {
|
||||
utils.SendJSONResponse(w, "false")
|
||||
}
|
||||
}
|
||||
|
||||
//Handle new user register. Require POST username, password, group.
|
||||
func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request, callback func(string, string)) {
|
||||
//Get username from request
|
||||
newusername, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'username' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
//Get password from request
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'password' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
//Get email from request
|
||||
email, err := utils.PostPara(r, "email")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'email' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid or malformed email")
|
||||
return
|
||||
}
|
||||
|
||||
//Ok to proceed create this user
|
||||
err = a.CreateUserAccount(newusername, password, email)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Do callback if exists
|
||||
if callback != nil {
|
||||
callback(newusername, email)
|
||||
}
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] New user " + newusername + " added to system.")
|
||||
}
|
||||
|
||||
//Handle new user register without confirmation email. Require POST username, password, group.
|
||||
func (a *AuthAgent) HandleRegisterWithoutEmail(w http.ResponseWriter, r *http.Request, callback func(string, string)) {
|
||||
//Get username from request
|
||||
newusername, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'username' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
//Get password from request
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'password' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
//Ok to proceed create this user
|
||||
err = a.CreateUserAccount(newusername, password, "")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Do callback if exists
|
||||
if callback != nil {
|
||||
callback(newusername, "")
|
||||
}
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] Admin account created: " + newusername)
|
||||
}
|
||||
|
||||
//Check authentication from request header's session value
|
||||
func (a *AuthAgent) CheckAuth(r *http.Request) bool {
|
||||
session, err := a.SessionStore.Get(r, a.SessionName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Check if user is authenticated
|
||||
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
//Handle de-register of users. Require POST username.
|
||||
//THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER
|
||||
func (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) {
|
||||
//Check if the user is logged in
|
||||
if !a.CheckAuth(r) {
|
||||
//This user has not logged in
|
||||
utils.SendErrorResponse(w, "Login required to remove user from the system.")
|
||||
return
|
||||
}
|
||||
|
||||
//Get username from request
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'username' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
err = a.UnregisterUser(username)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] User " + username + " has been removed from the system.")
|
||||
}
|
||||
|
||||
func (a *AuthAgent) UnregisterUser(username string) error {
|
||||
//Check if the user exists in the system database.
|
||||
if !a.Database.KeyExists("auth", "passhash/"+username) {
|
||||
//This user do not exists.
|
||||
return errors.New("this user does not exists")
|
||||
}
|
||||
|
||||
//OK! Remove the user from the database
|
||||
a.Database.Delete("auth", "passhash/"+username)
|
||||
a.Database.Delete("auth", "email/"+username)
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get the number of users in the system
|
||||
func (a *AuthAgent) GetUserCounts() int {
|
||||
entries, _ := a.Database.ListTable("auth")
|
||||
usercount := 0
|
||||
for _, keypairs := range entries {
|
||||
if strings.Contains(string(keypairs[0]), "passhash/") {
|
||||
//This is a user registry
|
||||
usercount++
|
||||
}
|
||||
}
|
||||
|
||||
if usercount == 0 {
|
||||
log.Println("There are no user in the database.")
|
||||
}
|
||||
return usercount
|
||||
}
|
||||
|
||||
//List all username within the system
|
||||
func (a *AuthAgent) ListUsers() []string {
|
||||
entries, _ := a.Database.ListTable("auth")
|
||||
results := []string{}
|
||||
for _, keypairs := range entries {
|
||||
if strings.Contains(string(keypairs[0]), "passhash/") {
|
||||
username := strings.Split(string(keypairs[0]), "/")[1]
|
||||
results = append(results, username)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
//Check if the given username exists
|
||||
func (a *AuthAgent) UserExists(username string) bool {
|
||||
userpasswordhash := ""
|
||||
err := a.Database.Read("auth", "passhash/"+username, &userpasswordhash)
|
||||
if err != nil || userpasswordhash == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
//Update the session expire time given the request header.
|
||||
func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Request) bool {
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
if session.Values["authenticated"].(bool) {
|
||||
//User authenticated. Extend its expire time
|
||||
rememberme := session.Values["rememberMe"].(bool)
|
||||
//Extend the session expire time
|
||||
if rememberme {
|
||||
session.Options = &sessions.Options{
|
||||
MaxAge: 3600 * 24 * 7, //One week
|
||||
Path: "/",
|
||||
}
|
||||
} else {
|
||||
session.Options = &sessions.Options{
|
||||
MaxAge: 3600 * 1, //One hour
|
||||
Path: "/",
|
||||
}
|
||||
}
|
||||
session.Save(r, w)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//Create user account
|
||||
func (a *AuthAgent) CreateUserAccount(newusername string, password string, email string) error {
|
||||
//Check user already exists
|
||||
if a.UserExists(newusername) {
|
||||
return errors.New("user with same name already exists")
|
||||
}
|
||||
|
||||
key := newusername
|
||||
hashedPassword := Hash(password)
|
||||
err := a.Database.Write("auth", "passhash/"+key, hashedPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if email != "" {
|
||||
err = a.Database.Write("auth", "email/"+key, email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Hash the given raw string into sha512 hash
|
||||
func Hash(raw string) string {
|
||||
h := sha512.New()
|
||||
h.Write([]byte(raw))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
53
src/mod/auth/router.go
Normal file
@ -0,0 +1,53 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type RouterOption struct {
|
||||
AuthAgent *AuthAgent
|
||||
RequireAuth bool //This router require authentication
|
||||
DeniedHandler func(http.ResponseWriter, *http.Request) //Things to do when request is rejected
|
||||
|
||||
}
|
||||
|
||||
type RouterDef struct {
|
||||
option RouterOption
|
||||
endpoints map[string]func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewManagedHTTPRouter(option RouterOption) *RouterDef {
|
||||
return &RouterDef{
|
||||
option: option,
|
||||
endpoints: map[string]func(http.ResponseWriter, *http.Request){},
|
||||
}
|
||||
}
|
||||
|
||||
func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request)) error {
|
||||
//Check if the endpoint already registered
|
||||
if _, exist := router.endpoints[endpoint]; exist {
|
||||
log.Println("WARNING! Duplicated registering of web endpoint: " + endpoint)
|
||||
return errors.New("endpoint register duplicated")
|
||||
}
|
||||
|
||||
authAgent := router.option.AuthAgent
|
||||
|
||||
//OK. Register handler
|
||||
http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
//Check authentication of the user
|
||||
if router.option.RequireAuth {
|
||||
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(w, r)
|
||||
})
|
||||
} else {
|
||||
handler(w, r)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
router.endpoints[endpoint] = handler
|
||||
|
||||
return nil
|
||||
}
|
120
src/mod/database/database.go
Normal file
@ -0,0 +1,120 @@
|
||||
package database
|
||||
|
||||
/*
|
||||
ArOZ Online Database Access Module
|
||||
author: tobychui
|
||||
|
||||
This is an improved Object oriented base solution to the original
|
||||
aroz online database script.
|
||||
*/
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Db interface{} //This will be nil on openwrt and *bolt.DB in the rest of the systems
|
||||
Tables sync.Map
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
||||
return newDatabase(dbfile, readOnlyMode)
|
||||
}
|
||||
|
||||
/*
|
||||
Create / Drop a table
|
||||
Usage:
|
||||
err := sysdb.NewTable("MyTable")
|
||||
err := sysdb.DropTable("MyTable")
|
||||
*/
|
||||
|
||||
func (d *Database) UpdateReadWriteMode(readOnly bool) {
|
||||
d.ReadOnly = readOnly
|
||||
}
|
||||
|
||||
//Dump the whole db into a log file
|
||||
func (d *Database) Dump(filename string) ([]string, error) {
|
||||
return d.dump(filename)
|
||||
}
|
||||
|
||||
//Create a new table
|
||||
func (d *Database) NewTable(tableName string) error {
|
||||
return d.newTable(tableName)
|
||||
}
|
||||
|
||||
//Check is table exists
|
||||
func (d *Database) TableExists(tableName string) bool {
|
||||
return d.tableExists(tableName)
|
||||
}
|
||||
|
||||
//Drop the given table
|
||||
func (d *Database) DropTable(tableName string) error {
|
||||
return d.dropTable(tableName)
|
||||
}
|
||||
|
||||
/*
|
||||
Write to database with given tablename and key. Example Usage:
|
||||
type demo struct{
|
||||
content string
|
||||
}
|
||||
thisDemo := demo{
|
||||
content: "Hello World",
|
||||
}
|
||||
err := sysdb.Write("MyTable", "username/message",thisDemo);
|
||||
*/
|
||||
func (d *Database) Write(tableName string, key string, value interface{}) error {
|
||||
return d.write(tableName, key, value)
|
||||
}
|
||||
|
||||
/*
|
||||
Read from database and assign the content to a given datatype. Example Usage:
|
||||
|
||||
type demo struct{
|
||||
content string
|
||||
}
|
||||
thisDemo := new(demo)
|
||||
err := sysdb.Read("MyTable", "username/message",&thisDemo);
|
||||
*/
|
||||
|
||||
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
|
||||
return d.read(tableName, key, assignee)
|
||||
}
|
||||
|
||||
func (d *Database) KeyExists(tableName string, key string) bool {
|
||||
return d.keyExists(tableName, key)
|
||||
}
|
||||
|
||||
/*
|
||||
Delete a value from the database table given tablename and key
|
||||
|
||||
err := sysdb.Delete("MyTable", "username/message");
|
||||
*/
|
||||
func (d *Database) Delete(tableName string, key string) error {
|
||||
return d.delete(tableName, key)
|
||||
}
|
||||
|
||||
/*
|
||||
//List table example usage
|
||||
//Assume the value is stored as a struct named "groupstruct"
|
||||
|
||||
entries, err := sysdb.ListTable("test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, keypairs := range entries{
|
||||
log.Println(string(keypairs[0]))
|
||||
group := new(groupstruct)
|
||||
json.Unmarshal(keypairs[1], &group)
|
||||
log.Println(group);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
|
||||
return d.listTable(tableName)
|
||||
}
|
||||
|
||||
func (d *Database) Close() {
|
||||
d.close()
|
||||
}
|
186
src/mod/database/database_core.go
Normal file
@ -0,0 +1,186 @@
|
||||
//go:build !mipsle && !riscv64
|
||||
// +build !mipsle,!riscv64
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
||||
db, err := bolt.Open(dbfile, 0600, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tableMap := sync.Map{}
|
||||
//Build the table list from database
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
return tx.ForEach(func(name []byte, _ *bolt.Bucket) error {
|
||||
tableMap.Store(string(name), "")
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
return &Database{
|
||||
Db: db,
|
||||
Tables: tableMap,
|
||||
ReadOnly: readOnlyMode,
|
||||
}, err
|
||||
}
|
||||
|
||||
//Dump the whole db into a log file
|
||||
func (d *Database) dump(filename string) ([]string, error) {
|
||||
results := []string{}
|
||||
|
||||
d.Tables.Range(func(tableName, v interface{}) bool {
|
||||
entries, err := d.ListTable(tableName.(string))
|
||||
if err != nil {
|
||||
log.Println("Reading table " + tableName.(string) + " failed: " + err.Error())
|
||||
return false
|
||||
}
|
||||
for _, keypairs := range entries {
|
||||
results = append(results, string(keypairs[0])+":"+string(keypairs[1])+"\n")
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
//Create a new table
|
||||
func (d *Database) newTable(tableName string) error {
|
||||
if d.ReadOnly == true {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
|
||||
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
d.Tables.Store(tableName, "")
|
||||
return err
|
||||
}
|
||||
|
||||
//Check is table exists
|
||||
func (d *Database) tableExists(tableName string) bool {
|
||||
if _, ok := d.Tables.Load(tableName); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//Drop the given table
|
||||
func (d *Database) dropTable(tableName string) error {
|
||||
if d.ReadOnly == true {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
|
||||
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
//Write to table
|
||||
func (d *Database) write(tableName string, key string, value interface{}) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
|
||||
jsonString, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
err = b.Put([]byte(key), jsonString)
|
||||
return err
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) read(tableName string, key string, assignee interface{}) error {
|
||||
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
v := b.Get([]byte(key))
|
||||
json.Unmarshal(v, &assignee)
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) keyExists(tableName string, key string) bool {
|
||||
resultIsNil := false
|
||||
if !d.TableExists(tableName) {
|
||||
//Table not exists. Do not proceed accessing key
|
||||
log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
|
||||
return false
|
||||
}
|
||||
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
v := b.Get([]byte(key))
|
||||
if v == nil {
|
||||
resultIsNil = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
} else {
|
||||
if resultIsNil {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Database) delete(tableName string, key string) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
|
||||
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
tx.Bucket([]byte(tableName)).Delete([]byte(key))
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) listTable(tableName string) ([][][]byte, error) {
|
||||
var results [][][]byte
|
||||
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
c := b.Cursor()
|
||||
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
results = append(results, [][]byte{k, v})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (d *Database) close() {
|
||||
d.Db.(*bolt.DB).Close()
|
||||
}
|
208
src/mod/database/database_openwrt.go
Normal file
@ -0,0 +1,208 @@
|
||||
//go:build mipsle || riscv64
|
||||
// +build mipsle riscv64
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
||||
dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
|
||||
dbRootPath = "fsdb/" + dbRootPath
|
||||
err := os.MkdirAll(dbRootPath, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tableMap := sync.Map{}
|
||||
//build the table list from file system
|
||||
files, err := filepath.Glob(filepath.Join(dbRootPath, "/*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if isDirectory(file) {
|
||||
tableMap.Store(filepath.Base(file), "")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
|
||||
return &Database{
|
||||
Db: dbRootPath,
|
||||
Tables: tableMap,
|
||||
ReadOnly: readOnlyMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Database) dump(filename string) ([]string, error) {
|
||||
//Get all file objects from root
|
||||
rootfiles, err := filepath.Glob(filepath.Join(d.Db.(string), "/*"))
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
//Filter out the folders
|
||||
rootFolders := []string{}
|
||||
for _, file := range rootfiles {
|
||||
if !isDirectory(file) {
|
||||
rootFolders = append(rootFolders, filepath.Base(file))
|
||||
}
|
||||
}
|
||||
|
||||
return rootFolders, nil
|
||||
}
|
||||
|
||||
func (d *Database) newTable(tableName string) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
if !fileExists(tablePath) {
|
||||
return os.MkdirAll(tablePath, 0755)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) tableExists(tableName string) bool {
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
if _, err := os.Stat(tablePath); errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !isDirectory(tablePath) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Database) dropTable(tableName string) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
if d.tableExists(tableName) {
|
||||
return os.RemoveAll(tablePath)
|
||||
} else {
|
||||
return errors.New("table not exists")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d *Database) write(tableName string, key string, value interface{}) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
js, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
|
||||
return os.WriteFile(filepath.Join(tablePath, key+".entry"), js, 0755)
|
||||
}
|
||||
|
||||
func (d *Database) read(tableName string, key string, assignee interface{}) error {
|
||||
if !d.keyExists(tableName, key) {
|
||||
return errors.New("key not exists")
|
||||
}
|
||||
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entryPath := filepath.Join(tablePath, key+".entry")
|
||||
content, err := os.ReadFile(entryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(content, &assignee)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) keyExists(tableName string, key string) bool {
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entryPath := filepath.Join(tablePath, key+".entry")
|
||||
return fileExists(entryPath)
|
||||
}
|
||||
|
||||
func (d *Database) delete(tableName string, key string) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
if !d.keyExists(tableName, key) {
|
||||
return errors.New("key not exists")
|
||||
}
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entryPath := filepath.Join(tablePath, key+".entry")
|
||||
|
||||
return os.Remove(entryPath)
|
||||
}
|
||||
|
||||
func (d *Database) listTable(tableName string) ([][][]byte, error) {
|
||||
if !d.tableExists(tableName) {
|
||||
return [][][]byte{}, errors.New("table not exists")
|
||||
}
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entries, err := filepath.Glob(filepath.Join(tablePath, "/*.entry"))
|
||||
if err != nil {
|
||||
return [][][]byte{}, err
|
||||
}
|
||||
|
||||
var results [][][]byte = [][][]byte{}
|
||||
for _, entry := range entries {
|
||||
if !isDirectory(entry) {
|
||||
//Read it
|
||||
key := filepath.Base(entry)
|
||||
key = strings.TrimSuffix(key, filepath.Ext(key))
|
||||
key = strings.ReplaceAll(key, "-SLASH_SIGN-", "/")
|
||||
|
||||
bkey := []byte(key)
|
||||
bval := []byte("")
|
||||
c, err := os.ReadFile(entry)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
bval = c
|
||||
results = append(results, [][]byte{bkey, bval})
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (d *Database) close() {
|
||||
//Nothing to close as it is file system
|
||||
}
|
||||
|
||||
func isDirectory(path string) bool {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return fileInfo.IsDir()
|
||||
}
|
||||
|
||||
func fileExists(name string) bool {
|
||||
_, err := os.Stat(name)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
152
src/mod/dynamicproxy/Server.go
Normal file
@ -0,0 +1,152 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
)
|
||||
|
||||
/*
|
||||
Server.go
|
||||
|
||||
Main server for dynamic proxy core
|
||||
|
||||
Routing Handler Priority (High to Low)
|
||||
- Blacklist
|
||||
- Whitelist
|
||||
- Redirectable
|
||||
- Subdomain Routing
|
||||
- Vitrual Directory Routing
|
||||
*/
|
||||
|
||||
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
/*
|
||||
Special Routing Rules, bypass most of the limitations
|
||||
*/
|
||||
|
||||
//Check if there are external routing rule matches.
|
||||
//If yes, route them via external rr
|
||||
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
|
||||
if matchedRoutingRule != nil {
|
||||
//Matching routing rule found. Let the sub-router handle it
|
||||
if matchedRoutingRule.UseSystemAccessControl {
|
||||
//This matching rule request system access control.
|
||||
//check access logic
|
||||
respWritten := h.handleAccessRouting(w, r)
|
||||
if respWritten {
|
||||
return
|
||||
}
|
||||
}
|
||||
matchedRoutingRule.Route(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
General Access Check
|
||||
*/
|
||||
|
||||
respWritten := h.handleAccessRouting(w, r)
|
||||
if respWritten {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Redirection Routing
|
||||
*/
|
||||
//Check if this is a redirection url
|
||||
if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) {
|
||||
statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r)
|
||||
h.logRequest(r, statusCode != 500, statusCode, "redirect", "")
|
||||
return
|
||||
}
|
||||
|
||||
//Extract request host to see if it is virtual directory or subdomain
|
||||
domainOnly := r.Host
|
||||
if strings.Contains(r.Host, ":") {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
|
||||
/*
|
||||
Subdomain Routing
|
||||
*/
|
||||
if strings.Contains(r.Host, ".") {
|
||||
//This might be a subdomain. See if there are any subdomain proxy router for this
|
||||
sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil {
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
h.subdomainRequest(w, r, sep)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Virtual Directory Routing
|
||||
*/
|
||||
//Clean up the request URI
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil {
|
||||
if targetProxyEndpoint.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, targetProxyEndpoint)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
h.proxyRequest(w, r, targetProxyEndpoint)
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") {
|
||||
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
//Passthrough the request to root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
}
|
||||
} else {
|
||||
//No routing rules found. Route to root.
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle access routing logic. Return true if the request is handled or blocked by the access control logic
|
||||
// if the return value is false, you can continue process the response writer
|
||||
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool {
|
||||
//Check if this ip is in blacklist
|
||||
clientIpAddr := geodb.GetRequesterIP(r)
|
||||
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
template, err := os.ReadFile("./web/forbidden.html")
|
||||
if err != nil {
|
||||
w.Write([]byte("403 - Forbidden"))
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
h.logRequest(r, false, 403, "blacklist", "")
|
||||
return true
|
||||
}
|
||||
|
||||
//Check if this ip is in whitelist
|
||||
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
template, err := os.ReadFile("./web/forbidden.html")
|
||||
if err != nil {
|
||||
w.Write([]byte("403 - Forbidden"))
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
h.logRequest(r, false, 403, "whitelist", "")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
47
src/mod/dynamicproxy/basicAuth.go
Normal file
@ -0,0 +1,47 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
)
|
||||
|
||||
/*
|
||||
BasicAuth.go
|
||||
|
||||
This file handles the basic auth on proxy endpoints
|
||||
if RequireBasicAuth is set to true
|
||||
*/
|
||||
|
||||
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
||||
proxyType := "vdir-auth"
|
||||
if pe.ProxyType == ProxyType_Subdomain {
|
||||
proxyType = "subd-auth"
|
||||
}
|
||||
u, p, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
//Check for the credentials to see if there is one matching
|
||||
hashedPassword := auth.Hash(p)
|
||||
matchingFound := false
|
||||
for _, cred := range pe.BasicAuthCredentials {
|
||||
if u == cred.Username && hashedPassword == cred.PasswordHash {
|
||||
matchingFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !matchingFound {
|
||||
h.logRequest(r, false, 401, proxyType, pe.Domain)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
23
src/mod/dynamicproxy/domainsniff/domainsniff.go
Normal file
@ -0,0 +1,23 @@
|
||||
package domainsniff
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
//Check if the domain is reachable and return err if not reachable
|
||||
func DomainReachableWithError(domain string) error {
|
||||
timeout := 1 * time.Second
|
||||
conn, err := net.DialTimeout("tcp", domain, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
//Check if domain reachable
|
||||
func DomainReachable(domain string) bool {
|
||||
return DomainReachableWithError(domain) == nil
|
||||
}
|
21
src/mod/dynamicproxy/dpcore/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-present tobychui
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
497
src/mod/dynamicproxy/dpcore/dpcore.go
Normal file
@ -0,0 +1,497 @@
|
||||
package dpcore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var onExitFlushLoop func()
|
||||
|
||||
const (
|
||||
defaultTimeout = time.Minute * 5
|
||||
)
|
||||
|
||||
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||
// sends it to another server, proxying the response back to the
|
||||
// client, support http, also support https tunnel using http.hijacker
|
||||
type ReverseProxy struct {
|
||||
// Set the timeout of the proxy server, default is 5 minutes
|
||||
Timeout time.Duration
|
||||
|
||||
// Director must be a function which modifies
|
||||
// the request into a new request to be sent
|
||||
// using Transport. Its response is then copied
|
||||
// back to the original client unmodified.
|
||||
// Director must not access the provided Request
|
||||
// after returning.
|
||||
Director func(*http.Request)
|
||||
|
||||
// The transport used to perform proxy requests.
|
||||
// default is http.DefaultTransport.
|
||||
Transport http.RoundTripper
|
||||
|
||||
// FlushInterval specifies the flush interval
|
||||
// to flush to the client while copying the
|
||||
// response body. If zero, no periodic flushing is done.
|
||||
FlushInterval time.Duration
|
||||
|
||||
// ErrorLog specifies an optional logger for errors
|
||||
// that occur when attempting to proxy the request.
|
||||
// If nil, logging goes to os.Stderr via the log package's
|
||||
// standard logger.
|
||||
ErrorLog *log.Logger
|
||||
|
||||
// ModifyResponse is an optional function that
|
||||
// modifies the Response from the backend.
|
||||
// If it returns an error, the proxy returns a StatusBadGateway error.
|
||||
ModifyResponse func(*http.Response) error
|
||||
|
||||
//Prepender is an optional prepend text for URL rewrite
|
||||
//
|
||||
Prepender string
|
||||
|
||||
Verbal bool
|
||||
}
|
||||
|
||||
type ResponseRewriteRuleSet struct {
|
||||
ProxyDomain string
|
||||
OriginalHost string
|
||||
UseTLS bool
|
||||
PathPrefix string //Vdir prefix for root, / will be rewrite to this
|
||||
}
|
||||
|
||||
type requestCanceler interface {
|
||||
CancelRequest(req *http.Request)
|
||||
}
|
||||
|
||||
func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerification bool) *ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Hack the default transporter to handle more connections
|
||||
thisTransporter := http.DefaultTransport
|
||||
optimalConcurrentConnection := 32
|
||||
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
|
||||
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
|
||||
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
|
||||
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
|
||||
thisTransporter.(*http.Transport).DisableCompression = true
|
||||
|
||||
if ignoreTLSVerification {
|
||||
//Ignore TLS certificate validation error
|
||||
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
return &ReverseProxy{
|
||||
Director: director,
|
||||
Prepender: prepender,
|
||||
Verbal: false,
|
||||
Transport: thisTransporter,
|
||||
}
|
||||
}
|
||||
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
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, "/")
|
||||
|
||||
switch {
|
||||
|
||||
case aslash && bslash:
|
||||
|
||||
return a.Path + b.Path[1:], apath + bpath[1:]
|
||||
|
||||
case !aslash && !bslash:
|
||||
|
||||
return a.Path + "/" + b.Path, apath + "/" + bpath
|
||||
|
||||
}
|
||||
|
||||
return a.Path + b.Path, apath + bpath
|
||||
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hop-by-hop headers. These are removed when sent to the backend.
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
|
||||
var hopHeaders = []string{
|
||||
//"Connection",
|
||||
"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
|
||||
"Keep-Alive",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Authorization",
|
||||
"Te", // canonicalized version of "TE"
|
||||
"Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522
|
||||
"Transfer-Encoding",
|
||||
//"Upgrade",
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
|
||||
if p.FlushInterval != 0 {
|
||||
if wf, ok := dst.(writeFlusher); ok {
|
||||
mlw := &maxLatencyWriter{
|
||||
dst: wf,
|
||||
latency: p.FlushInterval,
|
||||
done: make(chan bool),
|
||||
}
|
||||
|
||||
go mlw.flushLoop()
|
||||
defer mlw.stop()
|
||||
dst = mlw
|
||||
}
|
||||
}
|
||||
|
||||
io.Copy(dst, src)
|
||||
}
|
||||
|
||||
type writeFlusher interface {
|
||||
io.Writer
|
||||
http.Flusher
|
||||
}
|
||||
|
||||
type maxLatencyWriter struct {
|
||||
dst writeFlusher
|
||||
latency time.Duration
|
||||
mu sync.Mutex
|
||||
done chan bool
|
||||
}
|
||||
|
||||
func (m *maxLatencyWriter) Write(b []byte) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.dst.Write(b)
|
||||
}
|
||||
|
||||
func (m *maxLatencyWriter) flushLoop() {
|
||||
t := time.NewTicker(m.latency)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-m.done:
|
||||
if onExitFlushLoop != nil {
|
||||
onExitFlushLoop()
|
||||
}
|
||||
return
|
||||
case <-t.C:
|
||||
m.mu.Lock()
|
||||
m.dst.Flush()
|
||||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *maxLatencyWriter) stop() {
|
||||
m.done <- true
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) logf(format string, args ...interface{}) {
|
||||
if p.ErrorLog != nil {
|
||||
p.ErrorLog.Printf(format, args...)
|
||||
} else {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func removeHeaders(header http.Header) {
|
||||
// Remove hop-by-hop headers listed in the "Connection" header.
|
||||
if c := header.Get("Connection"); c != "" {
|
||||
for _, f := range strings.Split(c, ",") {
|
||||
if f = strings.TrimSpace(f); f != "" {
|
||||
header.Del(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove hop-by-hop headers
|
||||
for _, h := range hopHeaders {
|
||||
if header.Get(h) != "" {
|
||||
header.Del(h)
|
||||
}
|
||||
}
|
||||
|
||||
if header.Get("A-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
||||
header.Del("A-Upgrade")
|
||||
}
|
||||
}
|
||||
|
||||
func addXForwardedForHeader(req *http.Request) {
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
// If we aren't the first proxy retain prior
|
||||
// X-Forwarded-For information as a comma+space
|
||||
// separated list and fold multiple headers into one.
|
||||
if prior, ok := req.Header["X-Forwarded-For"]; ok {
|
||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||
}
|
||||
req.Header.Set("X-Forwarded-For", clientIP)
|
||||
if req.TLS != nil {
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
} else {
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
|
||||
transport := p.Transport
|
||||
|
||||
outreq := new(http.Request)
|
||||
// Shallow copies of maps, like header
|
||||
*outreq = *req
|
||||
|
||||
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()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-clientGone:
|
||||
requestCanceler.CancelRequest(outreq)
|
||||
case <-reqDone:
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
p.Director(outreq)
|
||||
outreq.Close = false
|
||||
|
||||
if !rrr.UseTLS {
|
||||
//This seems to be routing to external sites
|
||||
//Do not keep the original host
|
||||
outreq.Host = rrr.OriginalHost
|
||||
}
|
||||
|
||||
// We may modify the header (shallow copied above), so we only copy it.
|
||||
outreq.Header = make(http.Header)
|
||||
copyHeader(outreq.Header, req.Header)
|
||||
|
||||
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
|
||||
removeHeaders(outreq.Header)
|
||||
|
||||
// Add X-Forwarded-For Header.
|
||||
addXForwardedForHeader(outreq)
|
||||
|
||||
res, err := transport.RoundTrip(outreq)
|
||||
if err != nil {
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
//rw.WriteHeader(http.StatusBadGateway)
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
|
||||
removeHeaders(res.Header)
|
||||
|
||||
if p.ModifyResponse != nil {
|
||||
if err := p.ModifyResponse(res); err != nil {
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
//rw.WriteHeader(http.StatusBadGateway)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//Custom header rewriter functions
|
||||
if res.Header.Get("Location") != "" {
|
||||
locationRewrite := res.Header.Get("Location")
|
||||
originLocation := res.Header.Get("Location")
|
||||
res.Header.Set("zr-origin-location", originLocation)
|
||||
|
||||
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
|
||||
//Full path
|
||||
//Replace the forwarded target with expected Host
|
||||
lr, err := replaceLocationHost(locationRewrite, rrr, req.TLS != nil)
|
||||
if err == nil {
|
||||
locationRewrite = lr
|
||||
}
|
||||
} else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" {
|
||||
//Back to the root of this proxy object
|
||||
//fmt.Println(rrr.ProxyDomain, rrr.OriginalHost)
|
||||
locationRewrite = strings.TrimSuffix(rrr.PathPrefix, "/") + originLocation
|
||||
} else {
|
||||
//Relative path. Do not modifiy location header
|
||||
|
||||
}
|
||||
|
||||
//Custom redirection to this rproxy relative path
|
||||
res.Header.Set("Location", locationRewrite)
|
||||
}
|
||||
|
||||
// Copy header from response to client.
|
||||
copyHeader(rw.Header(), res.Header)
|
||||
|
||||
// The "Trailer" header isn't included in the Transport's response, Build it up from Trailer.
|
||||
if len(res.Trailer) > 0 {
|
||||
trailerKeys := make([]string, 0, len(res.Trailer))
|
||||
for k := range res.Trailer {
|
||||
trailerKeys = append(trailerKeys, k)
|
||||
}
|
||||
rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
|
||||
}
|
||||
|
||||
rw.WriteHeader(res.StatusCode)
|
||||
if len(res.Trailer) > 0 {
|
||||
// Force chunking if we saw a response trailer.
|
||||
// This prevents net/http from calculating the length for short
|
||||
// bodies and adding a Content-Length.
|
||||
if fl, ok := rw.(http.Flusher); ok {
|
||||
fl.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
p.copyResponse(rw, res.Body)
|
||||
// close now, instead of defer, to populate res.Trailer
|
||||
res.Body.Close()
|
||||
copyHeader(rw.Header(), res.Trailer)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) error {
|
||||
hij, ok := rw.(http.Hijacker)
|
||||
if !ok {
|
||||
p.logf("http server does not support hijacker")
|
||||
return errors.New("http server does not support hijacker")
|
||||
}
|
||||
|
||||
clientConn, _, err := hij.Hijack()
|
||||
if err != nil {
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
proxyConn, err := net.Dial("tcp", req.URL.Host)
|
||||
if err != nil {
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// The returned net.Conn may have read or write deadlines
|
||||
// already set, depending on the configuration of the
|
||||
// Server, to set or clear those deadlines as needed
|
||||
// we set timeout to 5 minutes
|
||||
deadline := time.Now()
|
||||
if p.Timeout == 0 {
|
||||
deadline = deadline.Add(time.Minute * 5)
|
||||
} else {
|
||||
deadline = deadline.Add(p.Timeout)
|
||||
}
|
||||
|
||||
err = clientConn.SetDeadline(deadline)
|
||||
if err != nil {
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = proxyConn.SetDeadline(deadline)
|
||||
if err != nil {
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = clientConn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
|
||||
if err != nil {
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
io.Copy(clientConn, proxyConn)
|
||||
clientConn.Close()
|
||||
proxyConn.Close()
|
||||
}()
|
||||
|
||||
io.Copy(proxyConn, clientConn)
|
||||
proxyConn.Close()
|
||||
clientConn.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
|
||||
if req.Method == "CONNECT" {
|
||||
err := p.ProxyHTTPS(rw, req)
|
||||
return err
|
||||
} else {
|
||||
err := p.ProxyHTTP(rw, req, rrr)
|
||||
return err
|
||||
}
|
||||
}
|
49
src/mod/dynamicproxy/dpcore/dpcore_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package dpcore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
func TestReplaceLocationHost(t *testing.T) {
|
||||
urlString := "http://private.com/test/newtarget/"
|
||||
rrr := &dpcore.ResponseRewriteRuleSet{
|
||||
OriginalHost: "test.example.com",
|
||||
ProxyDomain: "private.com/test",
|
||||
UseTLS: true,
|
||||
}
|
||||
useTLS := true
|
||||
|
||||
expectedResult := "https://test.example.com/newtarget/"
|
||||
|
||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred: %v", err)
|
||||
}
|
||||
|
||||
if result != expectedResult {
|
||||
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceLocationHostRelative(t *testing.T) {
|
||||
urlString := "api/"
|
||||
rrr := &dpcore.ResponseRewriteRuleSet{
|
||||
OriginalHost: "test.example.com",
|
||||
ProxyDomain: "private.com/test",
|
||||
UseTLS: true,
|
||||
}
|
||||
useTLS := true
|
||||
|
||||
expectedResult := "https://test.example.com/api/"
|
||||
|
||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred: %v", err)
|
||||
}
|
||||
|
||||
if result != expectedResult {
|
||||
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
|
||||
}
|
||||
}
|
51
src/mod/dynamicproxy/dpcore/utils.go
Normal file
@ -0,0 +1,51 @@
|
||||
package dpcore
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// replaceLocationHost rewrite the backend server's location header to a new URL based on the given proxy rules
|
||||
// If you have issues with tailing slash, you can try to fix them here (and remember to PR :D )
|
||||
func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
||||
u, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
//Update the schemetic if the proxying target is http
|
||||
//but exposed as https to the internet via Zoraxy
|
||||
if useTLS {
|
||||
u.Scheme = "https"
|
||||
} else {
|
||||
u.Scheme = "http"
|
||||
}
|
||||
|
||||
u.Host = rrr.OriginalHost
|
||||
|
||||
if strings.Contains(rrr.ProxyDomain, "/") {
|
||||
//The proxy domain itself seems contain subpath.
|
||||
//Trim it off from Location header to prevent URL segment duplicate
|
||||
//E.g. Proxy config: blog.example.com -> example.com/blog
|
||||
//Location Header: /blog/post?id=1
|
||||
//Expected Location Header send to client:
|
||||
// blog.example.com/post?id=1 instead of blog.example.com/blog/post?id=1
|
||||
|
||||
ProxyDomainURL := "http://" + rrr.ProxyDomain
|
||||
if rrr.UseTLS {
|
||||
ProxyDomainURL = "https://" + rrr.ProxyDomain
|
||||
}
|
||||
ru, err := url.Parse(ProxyDomainURL)
|
||||
if err == nil {
|
||||
//Trim off the subpath
|
||||
u.Path = strings.TrimPrefix(u.Path, ru.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// Debug functions
|
||||
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
||||
return replaceLocationHost(urlString, rrr, useTLS)
|
||||
}
|
385
src/mod/dynamicproxy/dynamicproxy.go
Normal file
@ -0,0 +1,385 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
/*
|
||||
Zoraxy Dynamic Proxy
|
||||
*/
|
||||
|
||||
func NewDynamicProxy(option RouterOption) (*Router, error) {
|
||||
proxyMap := sync.Map{}
|
||||
domainMap := sync.Map{}
|
||||
thisRouter := Router{
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
SubdomainEndpoint: &domainMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
}
|
||||
|
||||
thisRouter.mux = &ProxyHandler{
|
||||
Parent: &thisRouter,
|
||||
}
|
||||
|
||||
return &thisRouter, nil
|
||||
}
|
||||
|
||||
// Update TLS setting in runtime. Will restart the proxy server
|
||||
// if it is already running in the background
|
||||
func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
|
||||
router.Option.UseTls = tlsEnabled
|
||||
router.Restart()
|
||||
}
|
||||
|
||||
// Update TLS Version in runtime. Will restart proxy server if running.
|
||||
// Set this to true to force TLS 1.2 or above
|
||||
func (router *Router) UpdateTLSVersion(requireLatest bool) {
|
||||
router.Option.ForceTLSLatest = requireLatest
|
||||
router.Restart()
|
||||
}
|
||||
|
||||
// Update https redirect, which will require updates
|
||||
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
||||
router.Option.ForceHttpsRedirect = useRedirect
|
||||
router.Restart()
|
||||
}
|
||||
|
||||
// Start the dynamic routing
|
||||
func (router *Router) StartProxyService() error {
|
||||
//Create a new server object
|
||||
if router.server != nil {
|
||||
return errors.New("Reverse proxy server already running")
|
||||
}
|
||||
|
||||
if router.Root == nil {
|
||||
return errors.New("Reverse proxy router root not set")
|
||||
}
|
||||
|
||||
minVersion := tls.VersionTLS10
|
||||
if router.Option.ForceTLSLatest {
|
||||
minVersion = tls.VersionTLS12
|
||||
}
|
||||
config := &tls.Config{
|
||||
GetCertificate: router.Option.TlsManager.GetCert,
|
||||
MinVersion: uint16(minVersion),
|
||||
}
|
||||
|
||||
if router.Option.UseTls {
|
||||
//Serve with TLS mode
|
||||
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
router.Running = false
|
||||
return err
|
||||
}
|
||||
router.tlsListener = ln
|
||||
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
|
||||
router.Running = true
|
||||
|
||||
if router.Option.Port != 80 && router.Option.ForceHttpsRedirect {
|
||||
//Add a 80 to 443 redirector
|
||||
httpServer := &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
protocol := "https://"
|
||||
if router.Option.Port == 443 {
|
||||
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
}),
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
log.Println("Starting HTTP-to-HTTPS redirector (port 80)")
|
||||
|
||||
//Create a redirection stop channel
|
||||
stopChan := make(chan bool)
|
||||
|
||||
//Start a blocking wait for shutting down the http to https redirection server
|
||||
go func() {
|
||||
<-stopChan
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
httpServer.Shutdown(ctx)
|
||||
log.Println("HTTP to HTTPS redirection listener stopped")
|
||||
}()
|
||||
|
||||
//Start the http server that listens to port 80 and redirect to 443
|
||||
go func() {
|
||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
//Unable to startup port 80 listener. Handle shutdown process gracefully
|
||||
stopChan <- true
|
||||
log.Fatalf("Could not start server: %v\n", err)
|
||||
}
|
||||
}()
|
||||
router.tlsRedirectStop = stopChan
|
||||
}
|
||||
|
||||
//Start the TLS server
|
||||
log.Println("Reverse proxy service started in the background (TLS mode)")
|
||||
go func() {
|
||||
if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Could not start server: %v\n", err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
//Serve with non TLS mode
|
||||
router.tlsListener = nil
|
||||
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
|
||||
router.Running = true
|
||||
log.Println("Reverse proxy service started in the background (Plain HTTP mode)")
|
||||
go func() {
|
||||
router.server.ListenAndServe()
|
||||
//log.Println("[DynamicProxy] " + err.Error())
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (router *Router) StopProxyService() error {
|
||||
if router.server == nil {
|
||||
return errors.New("Reverse proxy server already stopped")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := router.server.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if router.tlsListener != nil {
|
||||
router.tlsListener.Close()
|
||||
}
|
||||
|
||||
if router.tlsRedirectStop != nil {
|
||||
router.tlsRedirectStop <- true
|
||||
}
|
||||
|
||||
//Discard the server object
|
||||
router.tlsListener = nil
|
||||
router.server = nil
|
||||
router.Running = false
|
||||
router.tlsRedirectStop = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restart the current router if it is running.
|
||||
func (router *Router) Restart() error {
|
||||
//Stop the router if it is already running
|
||||
var err error = nil
|
||||
if router.Running {
|
||||
err := router.StopProxyService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start the server
|
||||
err = router.StartProxyService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
Check if a given request is accessed via a proxied subdomain
|
||||
*/
|
||||
|
||||
func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
|
||||
hostname := r.Header.Get("X-Forwarded-Host")
|
||||
if hostname == "" {
|
||||
hostname = r.Host
|
||||
}
|
||||
hostname = strings.Split(hostname, ":")[0]
|
||||
subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname)
|
||||
return subdEndpoint != nil
|
||||
}
|
||||
|
||||
/*
|
||||
Add an URL into a custom proxy services
|
||||
*/
|
||||
func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) error {
|
||||
domain := options.Domain
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
/*
|
||||
if rootname[len(rootname)-1:] == "/" {
|
||||
rootname = rootname[:len(rootname)-1]
|
||||
}
|
||||
*/
|
||||
|
||||
webProxyEndpoint := domain
|
||||
if options.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, options.RootName, options.SkipCertValidations)
|
||||
|
||||
endpointObject := ProxyEndpoint{
|
||||
ProxyType: ProxyType_Vdir,
|
||||
RootOrMatchingDomain: options.RootName,
|
||||
Domain: domain,
|
||||
RequireTLS: options.RequireTLS,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
|
||||
|
||||
log.Println("Registered Proxy Rule: ", options.RootName+" to "+domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Load routing from RP
|
||||
*/
|
||||
func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error) {
|
||||
if ptype == "vdir" {
|
||||
proxy, ok := router.ProxyEndpoints.Load(key)
|
||||
if !ok {
|
||||
return nil, errors.New("target proxy not found")
|
||||
}
|
||||
return proxy.(*ProxyEndpoint), nil
|
||||
} else if ptype == "subd" {
|
||||
proxy, ok := router.SubdomainEndpoint.Load(key)
|
||||
if !ok {
|
||||
return nil, errors.New("target proxy not found")
|
||||
}
|
||||
return proxy.(*ProxyEndpoint), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported ptype")
|
||||
}
|
||||
|
||||
/*
|
||||
Save routing from RP
|
||||
*/
|
||||
func (router *Router) SaveProxy(ptype string, key string, newConfig *ProxyEndpoint) {
|
||||
if ptype == "vdir" {
|
||||
router.ProxyEndpoints.Store(key, newConfig)
|
||||
|
||||
} else if ptype == "subd" {
|
||||
router.SubdomainEndpoint.Store(key, newConfig)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Remove routing from RP
|
||||
*/
|
||||
func (router *Router) RemoveProxy(ptype string, key string) error {
|
||||
//fmt.Println(ptype, key)
|
||||
if ptype == "vdir" {
|
||||
router.ProxyEndpoints.Delete(key)
|
||||
return nil
|
||||
} else if ptype == "subd" {
|
||||
router.SubdomainEndpoint.Delete(key)
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid ptype")
|
||||
}
|
||||
|
||||
/*
|
||||
Add an default router for the proxy server
|
||||
*/
|
||||
func (router *Router) SetRootProxy(options *RootOptions) error {
|
||||
proxyLocation := options.ProxyLocation
|
||||
if proxyLocation[len(proxyLocation)-1:] == "/" {
|
||||
proxyLocation = proxyLocation[:len(proxyLocation)-1]
|
||||
}
|
||||
|
||||
webProxyEndpoint := proxyLocation
|
||||
if options.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
|
||||
|
||||
rootEndpoint := ProxyEndpoint{
|
||||
ProxyType: ProxyType_Vdir,
|
||||
RootOrMatchingDomain: "/",
|
||||
Domain: proxyLocation,
|
||||
RequireTLS: options.RequireTLS,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.Root = &rootEndpoint
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers to export the syncmap for easier processing
|
||||
func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
m := make(map[string]*ProxyEndpoint)
|
||||
r.SubdomainEndpoint.Range(func(key, value interface{}) bool {
|
||||
k, ok := key.(string)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
v, ok := value.(*ProxyEndpoint)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
m[k] = v
|
||||
return true
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
func (r *Router) GetVDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
m := make(map[string]*ProxyEndpoint)
|
||||
r.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
k, ok := key.(string)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
v, ok := value.(*ProxyEndpoint)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
m[k] = v
|
||||
return true
|
||||
})
|
||||
return m
|
||||
}
|
188
src/mod/dynamicproxy/proxyRequestHandler.go
Normal file
@ -0,0 +1,188 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/websocketproxy"
|
||||
)
|
||||
|
||||
func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *ProxyEndpoint {
|
||||
var targetProxyEndpoint *ProxyEndpoint = nil
|
||||
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
rootname := key.(string)
|
||||
if strings.HasPrefix(requestURI, rootname) {
|
||||
thisProxyEndpoint := value.(*ProxyEndpoint)
|
||||
targetProxyEndpoint = thisProxyEndpoint
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return targetProxyEndpoint
|
||||
}
|
||||
|
||||
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||
ep, ok := router.SubdomainEndpoint.Load(hostname)
|
||||
if ok {
|
||||
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
|
||||
}
|
||||
|
||||
return targetSubdomainEndpoint
|
||||
}
|
||||
|
||||
// Clearn URL Path (without the http:// part) replaces // in a URL to /
|
||||
func (router *Router) clearnURL(targetUrlOPath string) string {
|
||||
return strings.ReplaceAll(targetUrlOPath, "//", "/")
|
||||
}
|
||||
|
||||
// Rewrite URL rewrite the prefix part of a virtual directory URL with /
|
||||
func (router *Router) rewriteURL(rooturl string, requestURL string) string {
|
||||
rewrittenURL := requestURL
|
||||
rewrittenURL = strings.TrimPrefix(rewrittenURL, strings.TrimSuffix(rooturl, "/"))
|
||||
|
||||
if strings.Contains(rewrittenURL, "//") {
|
||||
rewrittenURL = router.clearnURL(rewrittenURL)
|
||||
}
|
||||
return rewrittenURL
|
||||
}
|
||||
|
||||
// Handle subdomain request
|
||||
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
requestURL := r.URL.String()
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
//Append / to the end of the redirection endpoint if not exists
|
||||
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
|
||||
}
|
||||
if len(requestURL) > 0 && requestURL[:1] == "/" {
|
||||
//Remove starting / from request URL if exists
|
||||
requestURL = requestURL[1:]
|
||||
}
|
||||
u, _ := url.Parse("ws://" + wsRedirectionEndpoint + requestURL)
|
||||
if target.RequireTLS {
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
|
||||
}
|
||||
h.logRequest(r, true, 101, "subdomain-websocket", target.Domain)
|
||||
wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations)
|
||||
wspHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
originalHostHeader := r.Host
|
||||
if r.URL != nil {
|
||||
r.Host = r.URL.Host
|
||||
} else {
|
||||
//Fallback when the upstream proxy screw something up in the header
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
PathPrefix: "",
|
||||
})
|
||||
|
||||
var dnsError *net.DNSError
|
||||
if err != nil {
|
||||
if errors.As(err, &dnsError) {
|
||||
http.ServeFile(w, r, "./web/hosterror.html")
|
||||
log.Println(err.Error())
|
||||
h.logRequest(r, false, 404, "subdomain-http", target.Domain)
|
||||
} else {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
log.Println(err.Error())
|
||||
h.logRequest(r, false, 521, "subdomain-http", target.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
h.logRequest(r, true, 200, "subdomain-http", target.Domain)
|
||||
}
|
||||
|
||||
// Handle vdir type request
|
||||
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.RootOrMatchingDomain, r.RequestURI)
|
||||
r.URL, _ = url.Parse(rewriteURL)
|
||||
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
|
||||
}
|
||||
u, _ := url.Parse("ws://" + wsRedirectionEndpoint + r.URL.String())
|
||||
if target.RequireTLS {
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
|
||||
}
|
||||
h.logRequest(r, true, 101, "vdir-websocket", target.Domain)
|
||||
wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations)
|
||||
wspHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
originalHostHeader := r.Host
|
||||
if r.URL != nil {
|
||||
r.Host = r.URL.Host
|
||||
} else {
|
||||
//Fallback when the upstream proxy screw something up in the header
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
PathPrefix: target.RootOrMatchingDomain,
|
||||
})
|
||||
|
||||
var dnsError *net.DNSError
|
||||
if err != nil {
|
||||
if errors.As(err, &dnsError) {
|
||||
http.ServeFile(w, r, "./web/hosterror.html")
|
||||
log.Println(err.Error())
|
||||
h.logRequest(r, false, 404, "vdir-http", target.Domain)
|
||||
} else {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
log.Println(err.Error())
|
||||
h.logRequest(r, false, 521, "vdir-http", target.Domain)
|
||||
}
|
||||
}
|
||||
h.logRequest(r, true, 200, "vdir-http", target.Domain)
|
||||
|
||||
}
|
||||
|
||||
func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) {
|
||||
if h.Parent.Option.StatisticCollector != nil {
|
||||
go func() {
|
||||
requestInfo := statistic.RequestInfo{
|
||||
IpAddr: geodb.GetRequesterIP(r),
|
||||
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
|
||||
Succ: succ,
|
||||
StatusCode: statusCode,
|
||||
ForwardType: forwardType,
|
||||
Referer: r.Referer(),
|
||||
UserAgent: r.UserAgent(),
|
||||
RequestURL: r.Host + r.RequestURI,
|
||||
Target: target,
|
||||
}
|
||||
h.Parent.Option.StatisticCollector.RecordRequest(requestInfo)
|
||||
}()
|
||||
|
||||
}
|
||||
}
|
58
src/mod/dynamicproxy/redirection/handler.go
Normal file
@ -0,0 +1,58 @@
|
||||
package redirection
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
handler.go
|
||||
|
||||
This script store the handlers use for handling
|
||||
redirection request
|
||||
*/
|
||||
|
||||
// Check if a request URL is a redirectable URI
|
||||
func (t *RuleTable) IsRedirectable(r *http.Request) bool {
|
||||
requestPath := r.Host + r.URL.Path
|
||||
rr := t.MatchRedirectRule(requestPath)
|
||||
return rr != nil
|
||||
}
|
||||
|
||||
// Handle the redirect request, return after calling this function to prevent
|
||||
// multiple write to the response writer
|
||||
// Return the status code of the redirection handling
|
||||
func (t *RuleTable) HandleRedirect(w http.ResponseWriter, r *http.Request) int {
|
||||
requestPath := r.Host + r.URL.Path
|
||||
rr := t.MatchRedirectRule(requestPath)
|
||||
if rr != nil {
|
||||
redirectTarget := rr.TargetURL
|
||||
|
||||
if rr.ForwardChildpath {
|
||||
//Remove the first / in the path if the redirect target already have tailing slash
|
||||
if strings.HasSuffix(redirectTarget, "/") {
|
||||
redirectTarget += strings.TrimPrefix(r.URL.Path, "/")
|
||||
} else {
|
||||
redirectTarget += r.URL.Path
|
||||
}
|
||||
|
||||
if r.URL.RawQuery != "" {
|
||||
redirectTarget += "?" + r.URL.RawQuery
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(redirectTarget, "http://") && !strings.HasPrefix(redirectTarget, "https://") {
|
||||
redirectTarget = "http://" + redirectTarget
|
||||
}
|
||||
|
||||
http.Redirect(w, r, redirectTarget, rr.StatusCode)
|
||||
return rr.StatusCode
|
||||
} else {
|
||||
//Invalid usage
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("500 - Internal Server Error"))
|
||||
log.Println("Target request URL do not have matching redirect rule. Check with IsRedirectable before calling HandleRedirect!")
|
||||
return 500
|
||||
}
|
||||
}
|
162
src/mod/dynamicproxy/redirection/redirection.go
Normal file
@ -0,0 +1,162 @@
|
||||
package redirection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type RuleTable struct {
|
||||
configPath string //The location where the redirection rules is stored
|
||||
rules sync.Map //Store the redirection rules for this reverse proxy instance
|
||||
}
|
||||
|
||||
type RedirectRules struct {
|
||||
RedirectURL string //The matching URL to redirect
|
||||
TargetURL string //The destination redirection url
|
||||
ForwardChildpath bool //Also redirect the pathname
|
||||
StatusCode int //Status Code for redirection
|
||||
}
|
||||
|
||||
func NewRuleTable(configPath string) (*RuleTable, error) {
|
||||
thisRuleTable := RuleTable{
|
||||
rules: sync.Map{},
|
||||
configPath: configPath,
|
||||
}
|
||||
//Load all the rules from the config path
|
||||
if !utils.FileExists(configPath) {
|
||||
os.MkdirAll(configPath, 0775)
|
||||
}
|
||||
|
||||
// Load all the *.json from the configPath
|
||||
files, err := filepath.Glob(filepath.Join(configPath, "*.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse the json content into RedirectRules
|
||||
var rules []*RedirectRules
|
||||
for _, file := range files {
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
thisRule := RedirectRules{}
|
||||
|
||||
err = json.Unmarshal(b, &thisRule)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
rules = append(rules, &thisRule)
|
||||
}
|
||||
|
||||
//Map the rules into the sync map
|
||||
for _, rule := range rules {
|
||||
log.Println("Redirection rule added: " + rule.RedirectURL + " -> " + rule.TargetURL)
|
||||
thisRuleTable.rules.Store(rule.RedirectURL, rule)
|
||||
}
|
||||
|
||||
return &thisRuleTable, nil
|
||||
}
|
||||
|
||||
func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardPathname bool, statusCode int) error {
|
||||
// Create a new RedirectRules object with the given parameters
|
||||
newRule := &RedirectRules{
|
||||
RedirectURL: redirectURL,
|
||||
TargetURL: destURL,
|
||||
ForwardChildpath: forwardPathname,
|
||||
StatusCode: statusCode,
|
||||
}
|
||||
|
||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json"
|
||||
|
||||
// Create the full file path by joining the t.configPath with the filename
|
||||
filepath := path.Join(t.configPath, filename)
|
||||
|
||||
// Create a new file for writing the JSON data
|
||||
file, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
log.Printf("Error creating file %s: %s", filepath, err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Encode the RedirectRules object to JSON and write it to the file
|
||||
err = json.NewEncoder(file).Encode(newRule)
|
||||
if err != nil {
|
||||
log.Printf("Error encoding JSON to file %s: %s", filepath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Store the RedirectRules object in the sync.Map
|
||||
t.rules.Store(redirectURL, newRule)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json"
|
||||
|
||||
// Create the full file path by joining the t.configPath with the filename
|
||||
filepath := path.Join(t.configPath, filename)
|
||||
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||
return nil // File doesn't exist, nothing to delete
|
||||
}
|
||||
|
||||
// Delete the file
|
||||
if err := os.Remove(filepath); err != nil {
|
||||
log.Printf("Error deleting file %s: %s", filepath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the key-value pair from the sync.Map
|
||||
t.rules.Delete(redirectURL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get a list of all the redirection rules
|
||||
func (t *RuleTable) GetAllRedirectRules() []*RedirectRules {
|
||||
rules := []*RedirectRules{}
|
||||
t.rules.Range(func(key, value interface{}) bool {
|
||||
r, ok := value.(*RedirectRules)
|
||||
if ok {
|
||||
rules = append(rules, r)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return rules
|
||||
}
|
||||
|
||||
// Check if a given request URL matched any of the redirection rule
|
||||
func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules {
|
||||
// Iterate through all the keys in the rules map
|
||||
var targetRedirectionRule *RedirectRules = nil
|
||||
var maxMatch int = 0
|
||||
|
||||
t.rules.Range(func(key interface{}, value interface{}) bool {
|
||||
// Check if the requested URL starts with the key as a prefix
|
||||
if strings.HasPrefix(requestedURL, key.(string)) {
|
||||
// This request URL matched the domain
|
||||
if len(key.(string)) > maxMatch {
|
||||
maxMatch = len(key.(string))
|
||||
targetRedirectionRule = value.(*RedirectRules)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return targetRedirectionRule
|
||||
}
|
86
src/mod/dynamicproxy/special.go
Normal file
@ -0,0 +1,86 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
/*
|
||||
Special.go
|
||||
|
||||
This script handle special routing rules
|
||||
by external modules
|
||||
*/
|
||||
|
||||
type RoutingRule struct {
|
||||
ID string //ID of the routing rule
|
||||
Enabled bool //If the routing rule enabled
|
||||
UseSystemAccessControl bool //Pass access control check to system white/black list, set this to false to bypass white/black list
|
||||
MatchRule func(r *http.Request) bool
|
||||
RoutingHandler func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// Router functions
|
||||
// Check if a routing rule exists given its id
|
||||
func (router *Router) GetRoutingRuleById(rrid string) (*RoutingRule, error) {
|
||||
for _, rr := range router.routingRules {
|
||||
if rr.ID == rrid {
|
||||
return rr, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("routing rule with given id not found")
|
||||
}
|
||||
|
||||
// Add a routing rule to the router
|
||||
func (router *Router) AddRoutingRules(rr *RoutingRule) error {
|
||||
_, err := router.GetRoutingRuleById(rr.ID)
|
||||
if err == nil {
|
||||
//routing rule with given id already exists
|
||||
return errors.New("routing rule with same id already exists")
|
||||
}
|
||||
|
||||
router.routingRules = append(router.routingRules, rr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove a routing rule from the router
|
||||
func (router *Router) RemoveRoutingRule(rrid string) {
|
||||
newRoutingRules := []*RoutingRule{}
|
||||
for _, rr := range router.routingRules {
|
||||
if rr.ID != rrid {
|
||||
newRoutingRules = append(newRoutingRules, rr)
|
||||
}
|
||||
}
|
||||
|
||||
router.routingRules = newRoutingRules
|
||||
}
|
||||
|
||||
// Get all routing rules
|
||||
func (router *Router) GetAllRoutingRules() []*RoutingRule {
|
||||
return router.routingRules
|
||||
}
|
||||
|
||||
// Get the matching routing rule that describe this request.
|
||||
// Return nil if no routing rule is match
|
||||
func (router *Router) GetMatchingRoutingRule(r *http.Request) *RoutingRule {
|
||||
for _, thisRr := range router.routingRules {
|
||||
if thisRr.IsMatch(r) {
|
||||
return thisRr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Routing Rule functions
|
||||
// Check if a request object match the
|
||||
func (e *RoutingRule) IsMatch(r *http.Request) bool {
|
||||
if !e.Enabled {
|
||||
return false
|
||||
}
|
||||
return e.MatchRule(r)
|
||||
}
|
||||
|
||||
func (e *RoutingRule) Route(w http.ResponseWriter, r *http.Request) {
|
||||
e.RoutingHandler(w, r)
|
||||
}
|
48
src/mod/dynamicproxy/subdomain.go
Normal file
@ -0,0 +1,48 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
/*
|
||||
Add an URL intoa custom subdomain service
|
||||
|
||||
*/
|
||||
|
||||
func (router *Router) AddSubdomainRoutingService(options *SubdOptions) error {
|
||||
domain := options.Domain
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
webProxyEndpoint := domain
|
||||
if options.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
|
||||
|
||||
router.SubdomainEndpoint.Store(options.MatchingDomain, &ProxyEndpoint{
|
||||
RootOrMatchingDomain: options.MatchingDomain,
|
||||
Domain: domain,
|
||||
RequireTLS: options.RequireTLS,
|
||||
Proxy: proxy,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
})
|
||||
|
||||
log.Println("Adding Subdomain Rule: ", options.MatchingDomain+" to "+domain)
|
||||
return nil
|
||||
}
|
114
src/mod/dynamicproxy/typedef.go
Normal file
@ -0,0 +1,114 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
)
|
||||
|
||||
const (
|
||||
ProxyType_Subdomain = 0
|
||||
ProxyType_Vdir = 1
|
||||
)
|
||||
|
||||
type ProxyHandler struct {
|
||||
Parent *Router
|
||||
}
|
||||
|
||||
type RouterOption struct {
|
||||
HostUUID string //The UUID of Zoraxy, use for heading mod
|
||||
Port int //Incoming port
|
||||
UseTls bool //Use TLS to serve incoming requsts
|
||||
ForceTLSLatest bool //Force TLS1.2 or above
|
||||
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
||||
TlsManager *tlscert.Manager
|
||||
RedirectRuleTable *redirection.RuleTable
|
||||
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
|
||||
StatisticCollector *statistic.Collector
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
Option *RouterOption
|
||||
ProxyEndpoints *sync.Map
|
||||
SubdomainEndpoint *sync.Map
|
||||
Running bool
|
||||
Root *ProxyEndpoint
|
||||
mux http.Handler
|
||||
server *http.Server
|
||||
tlsListener net.Listener
|
||||
routingRules []*RoutingRule
|
||||
|
||||
tlsRedirectStop chan bool
|
||||
}
|
||||
|
||||
// Auth credential for basic auth on certain endpoints
|
||||
type BasicAuthCredentials struct {
|
||||
Username string
|
||||
PasswordHash string
|
||||
}
|
||||
|
||||
// Auth credential for basic auth on certain endpoints
|
||||
type BasicAuthUnhashedCredentials struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// A proxy endpoint record
|
||||
type ProxyEndpoint struct {
|
||||
ProxyType int //The type of this proxy, see const def
|
||||
RootOrMatchingDomain string //Root for vdir or Matching domain for subd
|
||||
Domain string //Domain or IP to proxy to
|
||||
RequireTLS bool //Target domain require TLS
|
||||
SkipCertValidations bool //Set to true to accept self signed certs
|
||||
RequireBasicAuth bool //Set to true to request basic auth before proxy
|
||||
BasicAuthCredentials []*BasicAuthCredentials `json:"-"`
|
||||
Proxy *dpcore.ReverseProxy `json:"-"`
|
||||
}
|
||||
|
||||
type RootOptions struct {
|
||||
ProxyLocation string
|
||||
RequireTLS bool
|
||||
SkipCertValidations bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*BasicAuthCredentials
|
||||
}
|
||||
|
||||
type VdirOptions struct {
|
||||
RootName string
|
||||
Domain string
|
||||
RequireTLS bool
|
||||
SkipCertValidations bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*BasicAuthCredentials
|
||||
}
|
||||
|
||||
type SubdOptions struct {
|
||||
MatchingDomain string
|
||||
Domain string
|
||||
RequireTLS bool
|
||||
SkipCertValidations bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*BasicAuthCredentials
|
||||
}
|
||||
|
||||
/*
|
||||
type ProxyEndpoint struct {
|
||||
Root string
|
||||
Domain string
|
||||
RequireTLS bool
|
||||
Proxy *reverseproxy.ReverseProxy `json:"-"`
|
||||
}
|
||||
|
||||
type SubdomainEndpoint struct {
|
||||
MatchingDomain string
|
||||
Domain string
|
||||
RequireTLS bool
|
||||
Proxy *reverseproxy.ReverseProxy `json:"-"`
|
||||
}
|
||||
*/
|
60
src/mod/email/email.go
Normal file
@ -0,0 +1,60 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
/*
|
||||
Email.go
|
||||
|
||||
This script handle mailing services using SMTP protocol
|
||||
*/
|
||||
|
||||
type Sender struct {
|
||||
Hostname string //E.g. mail.gandi.net
|
||||
Domain string //E.g. arozos.com
|
||||
Port int //E.g. 587
|
||||
Username string //Username of the email account
|
||||
Password string //Password of the email account
|
||||
SenderAddr string //e.g. admin@arozos.com
|
||||
}
|
||||
|
||||
//Create a new email sender object
|
||||
func NewEmailSender(hostname string, domain string, port int, username string, password string, senderAddr string) *Sender {
|
||||
return &Sender{
|
||||
Hostname: hostname,
|
||||
Domain: domain,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Password: password,
|
||||
SenderAddr: senderAddr,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Send a email to a reciving addr
|
||||
Example Usage:
|
||||
SendEmail(
|
||||
test@example.com,
|
||||
"Free donuts",
|
||||
"Come get your free donuts on this Sunday!"
|
||||
)
|
||||
*/
|
||||
func (s *Sender) SendEmail(to string, subject string, content string) error {
|
||||
//Parse the email content
|
||||
msg := []byte("To: " + to + "\n" +
|
||||
"From: Zoraxy <" + s.SenderAddr + ">\n" +
|
||||
"Subject: " + subject + "\n" +
|
||||
"MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" +
|
||||
content + "\n\n")
|
||||
|
||||
//Login to the SMTP server
|
||||
auth := smtp.PlainAuth("", s.Username+"@"+s.Domain, s.Password, s.Hostname)
|
||||
err := smtp.SendMail(s.Hostname+":"+strconv.Itoa(s.Port), auth, s.SenderAddr, []string{to}, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
16
src/mod/expose/expose.go
Normal file
@ -0,0 +1,16 @@
|
||||
package expose
|
||||
|
||||
/*
|
||||
Service Expose Proxy
|
||||
|
||||
A tunnel for getting your local server online in one line
|
||||
(No, this is not ngrok)
|
||||
*/
|
||||
|
||||
type Router struct {
|
||||
}
|
||||
|
||||
//Create a new service expose router
|
||||
func NewServiceExposeRouter() {
|
||||
|
||||
}
|
111
src/mod/expose/security.go
Normal file
@ -0,0 +1,111 @@
|
||||
package expose
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha512"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
// GenerateKeyPair generates a new key pair
|
||||
func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
|
||||
privkey, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return privkey, &privkey.PublicKey, nil
|
||||
}
|
||||
|
||||
// PrivateKeyToBytes private key to bytes
|
||||
func PrivateKeyToBytes(priv *rsa.PrivateKey) []byte {
|
||||
privBytes := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(priv),
|
||||
},
|
||||
)
|
||||
|
||||
return privBytes
|
||||
}
|
||||
|
||||
// PublicKeyToBytes public key to bytes
|
||||
func PublicKeyToBytes(pub *rsa.PublicKey) ([]byte, error) {
|
||||
pubASN1, err := x509.MarshalPKIXPublicKey(pub)
|
||||
if err != nil {
|
||||
return []byte(""), err
|
||||
}
|
||||
|
||||
pubBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: pubASN1,
|
||||
})
|
||||
|
||||
return pubBytes, nil
|
||||
}
|
||||
|
||||
// BytesToPrivateKey bytes to private key
|
||||
func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode(priv)
|
||||
enc := x509.IsEncryptedPEMBlock(block)
|
||||
b := block.Bytes
|
||||
var err error
|
||||
if enc {
|
||||
log.Println("is encrypted pem block")
|
||||
b, err = x509.DecryptPEMBlock(block, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
key, err := x509.ParsePKCS1PrivateKey(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// BytesToPublicKey bytes to public key
|
||||
func BytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
|
||||
block, _ := pem.Decode(pub)
|
||||
enc := x509.IsEncryptedPEMBlock(block)
|
||||
b := block.Bytes
|
||||
var err error
|
||||
if enc {
|
||||
log.Println("is encrypted pem block")
|
||||
b, err = x509.DecryptPEMBlock(block, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
ifc, err := x509.ParsePKIXPublicKey(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, ok := ifc.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, errors.New("key not valid")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// EncryptWithPublicKey encrypts data with public key
|
||||
func EncryptWithPublicKey(msg []byte, pub *rsa.PublicKey) ([]byte, error) {
|
||||
hash := sha512.New()
|
||||
ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil)
|
||||
if err != nil {
|
||||
return []byte(""), err
|
||||
}
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// DecryptWithPrivateKey decrypts data with private key
|
||||
func DecryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) ([]byte, error) {
|
||||
hash := sha512.New()
|
||||
plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil)
|
||||
if err != nil {
|
||||
return []byte(""), err
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
80
src/mod/ganserv/authkey.go
Normal file
@ -0,0 +1,80 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func TryLoadorAskUserForAuthkey() (string, error) {
|
||||
//Check for zt auth token
|
||||
value, exists := os.LookupEnv("ZT_AUTH")
|
||||
if !exists {
|
||||
log.Println("Environment variable ZT_AUTH not defined. Trying to load authtoken from file.")
|
||||
} else {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
authKey := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
if isAdmin() {
|
||||
//Read the secret file directly
|
||||
b, err := os.ReadFile("C:\\ProgramData\\ZeroTier\\One\\authtoken.secret")
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = string(b)
|
||||
} else {
|
||||
log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
|
||||
}
|
||||
} else {
|
||||
//Elavate the permission to admin
|
||||
ak, err := readAuthTokenAsAdmin()
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = ak
|
||||
} else {
|
||||
log.Println("Unable to read authkey at C:\\ProgramData\\ZeroTier\\One\\authtoken.secret: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
} else if runtime.GOOS == "linux" {
|
||||
if isAdmin() {
|
||||
//Try to read from source using sudo
|
||||
ak, err := readAuthTokenAsAdmin()
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = strings.TrimSpace(ak)
|
||||
} else {
|
||||
log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
|
||||
}
|
||||
} else {
|
||||
//Try read from source
|
||||
b, err := os.ReadFile("/var/lib/zerotier-one/authtoken.secret")
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = string(b)
|
||||
} else {
|
||||
log.Println("Unable to read authkey at /var/lib/zerotier-one/authtoken.secret: ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
b, err := os.ReadFile("/Library/Application Support/ZeroTier/One/authtoken.secret")
|
||||
if err == nil {
|
||||
log.Println("Zerotier authkey loaded")
|
||||
authKey = string(b)
|
||||
} else {
|
||||
log.Println("Unable to read authkey at /Library/Application Support/ZeroTier/One/authtoken.secret ", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
authKey = strings.TrimSpace(authKey)
|
||||
|
||||
if authKey == "" {
|
||||
return "", errors.New("Unable to load authkey from file")
|
||||
}
|
||||
|
||||
return authKey, nil
|
||||
}
|
37
src/mod/ganserv/authkeyLinux.go
Normal file
@ -0,0 +1,37 @@
|
||||
//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"
|
||||
}
|
73
src/mod/ganserv/authkeyWin.go
Normal file
@ -0,0 +1,73 @@
|
||||
//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
|
||||
}
|
128
src/mod/ganserv/ganserv.go
Normal file
@ -0,0 +1,128 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"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 {
|
||||
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)
|
||||
}
|
428
src/mod/ganserv/handlers.go
Normal file
@ -0,0 +1,428 @@
|
||||
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)
|
||||
}
|
39
src/mod/ganserv/network.go
Normal file
@ -0,0 +1,39 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
//Get a random free IP from the pool
|
||||
func (n *Network) GetRandomFreeIP() (net.IP, error) {
|
||||
// Get all IP addresses in the subnet
|
||||
ips, err := GetAllAddressFromCIDR(n.CIDR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter out used IPs
|
||||
usedIPs := make(map[string]bool)
|
||||
for _, node := range n.Nodes {
|
||||
usedIPs[node.ManagedIP.String()] = true
|
||||
}
|
||||
availableIPs := []string{}
|
||||
for _, ip := range ips {
|
||||
if !usedIPs[ip] {
|
||||
availableIPs = append(availableIPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
// Randomly choose an available IP
|
||||
if len(availableIPs) == 0 {
|
||||
return nil, fmt.Errorf("no available IP")
|
||||
}
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
randIndex := rand.Intn(len(availableIPs))
|
||||
pickedFreeIP := availableIPs[randIndex]
|
||||
|
||||
return net.ParseIP(pickedFreeIP), nil
|
||||
}
|
55
src/mod/ganserv/network_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
55
src/mod/ganserv/utils.go
Normal file
@ -0,0 +1,55 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
//Generate all ip address from a CIDR
|
||||
func GetAllAddressFromCIDR(cidr string) ([]string, error) {
|
||||
ip, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ips []string
|
||||
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
// remove network address and broadcast address
|
||||
return ips[1 : len(ips)-1], nil
|
||||
}
|
||||
|
||||
func inc(ip net.IP) {
|
||||
for j := len(ip) - 1; j >= 0; j-- {
|
||||
ip[j]++
|
||||
if ip[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isValidIPAddr(ipAddr string) bool {
|
||||
ip := net.ParseIP(ipAddr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ipWithinCIDR(ipAddr string, cidr string) bool {
|
||||
// Parse the CIDR string
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse the IP address
|
||||
ip := net.ParseIP(ipAddr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the IP address is in the CIDR range
|
||||
return ipNet.Contains(ip)
|
||||
}
|
622
src/mod/ganserv/zerotier.go
Normal file
@ -0,0 +1,622 @@
|
||||
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"`
|
||||
PortMappingEnabled bool `json:"portMappingEnabled"`
|
||||
PrimaryPort int `json:"primaryPort"`
|
||||
SoftwareUpdate string `json:"softwareUpdate"`
|
||||
SoftwareUpdateChannel string `json:"softwareUpdateChannel"`
|
||||
} `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
|
||||
}
|
91
src/mod/geodb/blacklist.go
Normal file
@ -0,0 +1,91 @@
|
||||
package geodb
|
||||
|
||||
import "strings"
|
||||
|
||||
/*
|
||||
Blacklist.go
|
||||
|
||||
This script store the blacklist related functions
|
||||
*/
|
||||
|
||||
//Geo Blacklist
|
||||
|
||||
func (s *Store) AddCountryCodeToBlackList(countryCode string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
s.sysdb.Write("blacklist-cn", countryCode, true)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
s.sysdb.Delete("blacklist-cn", countryCode)
|
||||
}
|
||||
|
||||
func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
var isBlacklisted bool = false
|
||||
s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted)
|
||||
return isBlacklisted
|
||||
}
|
||||
|
||||
func (s *Store) GetAllBlacklistedCountryCode() []string {
|
||||
bannedCountryCodes := []string{}
|
||||
entries, err := s.sysdb.ListTable("blacklist-cn")
|
||||
if err != nil {
|
||||
return bannedCountryCodes
|
||||
}
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
bannedCountryCodes = append(bannedCountryCodes, ip)
|
||||
}
|
||||
|
||||
return bannedCountryCodes
|
||||
}
|
||||
|
||||
//IP Blacklsits
|
||||
|
||||
func (s *Store) AddIPToBlackList(ipAddr string) {
|
||||
s.sysdb.Write("blacklist-ip", ipAddr, true)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveIPFromBlackList(ipAddr string) {
|
||||
s.sysdb.Delete("blacklist-ip", ipAddr)
|
||||
}
|
||||
|
||||
func (s *Store) GetAllBlacklistedIp() []string {
|
||||
bannedIps := []string{}
|
||||
entries, err := s.sysdb.ListTable("blacklist-ip")
|
||||
if err != nil {
|
||||
return bannedIps
|
||||
}
|
||||
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
bannedIps = append(bannedIps, ip)
|
||||
}
|
||||
|
||||
return bannedIps
|
||||
}
|
||||
|
||||
func (s *Store) IsIPBlacklisted(ipAddr string) bool {
|
||||
var isBlacklisted bool = false
|
||||
s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted)
|
||||
if isBlacklisted {
|
||||
return true
|
||||
}
|
||||
|
||||
//Check for IP wildcard and CIRD rules
|
||||
AllBlacklistedIps := s.GetAllBlacklistedIp()
|
||||
for _, blacklistRule := range AllBlacklistedIps {
|
||||
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
|
||||
if wildcardMatch {
|
||||
return true
|
||||
}
|
||||
|
||||
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
|
||||
if cidrMatch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
208
src/mod/geodb/geodb.go
Normal file
@ -0,0 +1,208 @@
|
||||
package geodb
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
)
|
||||
|
||||
//go:embed geoipv4.csv
|
||||
var geoipv4 []byte //Geodb dataset for ipv4
|
||||
|
||||
//go:embed geoipv6.csv
|
||||
var geoipv6 []byte //Geodb dataset for ipv6
|
||||
|
||||
type Store struct {
|
||||
BlacklistEnabled bool
|
||||
WhitelistEnabled bool
|
||||
geodb [][]string //Parsed geodb list
|
||||
geodbIpv6 [][]string //Parsed geodb list for ipv6
|
||||
|
||||
geotrie *trie
|
||||
geotrieIpv6 *trie
|
||||
|
||||
//geoipCache sync.Map
|
||||
|
||||
sysdb *database.Database
|
||||
}
|
||||
|
||||
type CountryInfo struct {
|
||||
CountryIsoCode string
|
||||
ContinetCode string
|
||||
}
|
||||
|
||||
func NewGeoDb(sysdb *database.Database) (*Store, error) {
|
||||
parsedGeoData, err := parseCSV(geoipv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedGeoDataIpv6, err := parseCSV(geoipv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blacklistEnabled := false
|
||||
whitelistEnabled := false
|
||||
if sysdb != nil {
|
||||
err = sysdb.NewTable("blacklist-cn")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("blacklist-ip")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("whitelist-cn")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("whitelist-ip")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("blackwhitelist")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sysdb.Read("blackwhitelist", "blacklistEnabled", &blacklistEnabled)
|
||||
sysdb.Read("blackwhitelist", "whitelistEnabled", &whitelistEnabled)
|
||||
} else {
|
||||
log.Println("Database pointer set to nil: Entering debug mode")
|
||||
}
|
||||
|
||||
return &Store{
|
||||
BlacklistEnabled: blacklistEnabled,
|
||||
WhitelistEnabled: whitelistEnabled,
|
||||
geodb: parsedGeoData,
|
||||
geotrie: constrctTrieTree(parsedGeoData),
|
||||
geodbIpv6: parsedGeoDataIpv6,
|
||||
geotrieIpv6: constrctTrieTree(parsedGeoDataIpv6),
|
||||
sysdb: sysdb,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) ToggleBlacklist(enabled bool) {
|
||||
s.sysdb.Write("blackwhitelist", "blacklistEnabled", enabled)
|
||||
s.BlacklistEnabled = enabled
|
||||
}
|
||||
|
||||
func (s *Store) ToggleWhitelist(enabled bool) {
|
||||
s.sysdb.Write("blackwhitelist", "whitelistEnabled", enabled)
|
||||
s.WhitelistEnabled = enabled
|
||||
}
|
||||
|
||||
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
|
||||
cc := s.search(ipstring)
|
||||
return &CountryInfo{
|
||||
CountryIsoCode: cc,
|
||||
ContinetCode: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Check if a IP address is blacklisted, in either country or IP blacklist
|
||||
IsBlacklisted default return is false (allow access)
|
||||
*/
|
||||
func (s *Store) IsBlacklisted(ipAddr string) bool {
|
||||
if !s.BlacklistEnabled {
|
||||
//Blacklist not enabled. Always return false
|
||||
return false
|
||||
}
|
||||
|
||||
if ipAddr == "" {
|
||||
//Unable to get the target IP address
|
||||
return false
|
||||
}
|
||||
|
||||
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsIPBlacklisted(ipAddr) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
IsWhitelisted check if a given IP address is in the current
|
||||
server's white list.
|
||||
|
||||
Note that the Whitelist default result is true even
|
||||
when encountered error
|
||||
*/
|
||||
func (s *Store) IsWhitelisted(ipAddr string) bool {
|
||||
if !s.WhitelistEnabled {
|
||||
//Whitelist not enabled. Always return true (allow access)
|
||||
return true
|
||||
}
|
||||
|
||||
if ipAddr == "" {
|
||||
//Unable to get the target IP address, assume ok
|
||||
return true
|
||||
}
|
||||
|
||||
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsIPWhitelisted(ipAddr) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// A helper function that check both blacklist and whitelist for access
|
||||
// for both geoIP and ip / CIDR ranges
|
||||
func (s *Store) AllowIpAccess(ipaddr string) bool {
|
||||
if s.IsBlacklisted(ipaddr) {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.IsWhitelisted(ipaddr)
|
||||
}
|
||||
|
||||
func (s *Store) AllowConnectionAccess(conn net.Conn) bool {
|
||||
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
|
||||
return s.AllowIpAccess(addr.IP.String())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
|
||||
ipAddr := GetRequesterIP(r)
|
||||
if ipAddr == "" {
|
||||
return ""
|
||||
}
|
||||
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return countryCode.CountryIsoCode
|
||||
}
|
73
src/mod/geodb/geodb_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package geodb_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
)
|
||||
|
||||
/*
|
||||
func TestTrieConstruct(t *testing.T) {
|
||||
tt := geodb.NewTrie()
|
||||
data := [][]string{
|
||||
{"1.0.16.0", "1.0.31.255", "JP"},
|
||||
{"1.0.32.0", "1.0.63.255", "CN"},
|
||||
{"1.0.64.0", "1.0.127.255", "JP"},
|
||||
{"1.0.128.0", "1.0.255.255", "TH"},
|
||||
{"1.1.0.0", "1.1.0.255", "CN"},
|
||||
{"1.1.1.0", "1.1.1.255", "AU"},
|
||||
{"1.1.2.0", "1.1.63.255", "CN"},
|
||||
{"1.1.64.0", "1.1.127.255", "JP"},
|
||||
{"1.1.128.0", "1.1.255.255", "TH"},
|
||||
{"1.2.0.0", "1.2.2.255", "CN"},
|
||||
{"1.2.3.0", "1.2.3.255", "AU"},
|
||||
}
|
||||
|
||||
for _, entry := range data {
|
||||
startIp := entry[0]
|
||||
endIp := entry[1]
|
||||
cc := entry[2]
|
||||
tt.Insert(startIp, cc)
|
||||
tt.Insert(endIp, cc)
|
||||
}
|
||||
|
||||
t.Log(tt.Search("1.0.16.20"), "== JP") //JP
|
||||
t.Log(tt.Search("1.2.0.122"), "== CN") //CN
|
||||
t.Log(tt.Search("1.2.1.0"), "== CN") //CN
|
||||
t.Log(tt.Search("1.0.65.243"), "== JP") //JP
|
||||
t.Log(tt.Search("1.0.62.243"), "== CN") //CN
|
||||
}
|
||||
*/
|
||||
|
||||
func TestResolveCountryCodeFromIP(t *testing.T) {
|
||||
// Create a new store
|
||||
store, err := geodb.NewGeoDb(nil)
|
||||
if err != nil {
|
||||
t.Errorf("error creating store: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Test an IP address that should return a valid country code
|
||||
ip := "8.8.8.8"
|
||||
expected := "US"
|
||||
info, err := store.ResolveCountryCodeFromIP(ip)
|
||||
if err != nil {
|
||||
t.Errorf("error resolving country code for IP %s: %v", ip, err)
|
||||
return
|
||||
}
|
||||
if info.CountryIsoCode != expected {
|
||||
t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
|
||||
}
|
||||
|
||||
// Test an IP address that should return an empty country code
|
||||
ip = "127.0.0.1"
|
||||
expected = ""
|
||||
info, err = store.ResolveCountryCodeFromIP(ip)
|
||||
if err != nil {
|
||||
t.Errorf("error resolving country code for IP %s: %v", ip, err)
|
||||
return
|
||||
}
|
||||
if info.CountryIsoCode != expected {
|
||||
t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
|
||||
}
|
||||
}
|