This commit is contained in:
chermack
2026-07-26 00:09:57 +08:00
commit 059be96536
134 changed files with 19678 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/trojanZ.iml" filepath="$PROJECT_DIR$/trojanZ.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
BIN
View File
Binary file not shown.
+55
View File
@@ -0,0 +1,55 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
workflow_dispatch: #github页面手动触发
push:
tags: [ 'v*' ]
env:
# github.repository as <account>/<repo>
IMAGE_NAME: jrohy/trojan
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Get latest tag
uses: oprypin/find-latest-tag@v1
id: octokit # The step ID to refer to later.
with:
repository: ${{ github.repository }} # The repository to scan.
# https://github.com/docker/login-action
- name: Login
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://github.com/docker/build-push-action
- name: Build & Push
uses: docker/build-push-action@v2
with:
context: .
file: ./asset/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ steps.octokit.outputs.tag }}
${{ env.IMAGE_NAME }}:latest
+2
View File
@@ -0,0 +1,2 @@
.vscode
.idea
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is 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. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
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.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
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 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. Use with the GNU Affero General Public License.
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 Affero 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 special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU 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 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 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 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 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
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 GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+101
View File
@@ -0,0 +1,101 @@
# trojan
![](https://img.shields.io/github/v/release/Jrohy/trojan.svg)
![](https://img.shields.io/docker/pulls/jrohy/trojan.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/Jrohy/trojan)](https://goreportcard.com/report/github.com/Jrohy/trojan)
[![Downloads](https://img.shields.io/github/downloads/Jrohy/trojan/total.svg)](https://img.shields.io/github/downloads/Jrohy/trojan/total.svg)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
trojan多用户管理部署程序
## 功能
- 在线web页面和命令行两种方式管理trojan多用户
- 启动 / 停止 / 重启 trojan 服务端
- 支持流量统计和流量限制
- 命令行模式管理, 支持命令补全
- 集成acme.sh证书申请
- 生成客户端配置文件
- 在线实时查看trojan日志
- 在线trojan和trojan-go随时切换
- 支持trojan://分享链接和二维码分享(仅限web页面)
- 支持转化为clash订阅地址并导入到[clash_for_windows](https://github.com/Fndroid/clash_for_windows_pkg/releases)(仅限web页面)
- 限制用户使用期限
## 安装方式
*trojan使用请提前准备好服务器可用的域名*
### a. 一键脚本安装
```
#安装/更新
source <(curl -sL https://git.io/trojan-install)
#卸载
source <(curl -sL https://git.io/trojan-install) --remove
```
安装完后输入'trojan'可进入管理程序
浏览器访问 https://域名 可在线web页面管理trojan用户
前端页面源码地址: [trojan-web](https://github.com/Jrohy/trojan-web)
### b. docker运行
1. 安装mysql
因为mariadb内存使用比mysql至少减少一半, 所以推荐使用mariadb数据库
```
docker run --name trojan-mariadb --restart=always -p 3306:3306 -v /home/mariadb:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=trojan -e MYSQL_ROOT_HOST=% -e MYSQL_DATABASE=trojan -d mariadb:10.2
```
端口和root密码以及持久化目录都可以改成其他的
2. 安装trojan
```
docker run -it -d --name trojan --net=host --restart=always --privileged jrohy/trojan init
```
运行完后进入容器 `docker exec -it trojan bash`, 然后输入'trojan'即可进行初始化安装
启动web服务: `systemctl start trojan-web`
设置自启动: `systemctl enable trojan-web`
更新管理程序: `source <(curl -sL https://git.io/trojan-install)`
## 运行截图
![avatar](asset/1.png)
![avatar](asset/2.png)
## 命令行
```
Usage:
trojan [flags]
trojan [command]
Available Commands:
add 添加用户
clean 清空指定用户流量
completion 自动命令补全(支持bash和zsh)
del 删除用户
help Help about any command
info 用户信息列表
log 查看trojan日志
port 修改trojan端口
restart 重启trojan
start 启动trojan
status 查看trojan状态
stop 停止trojan
tls 证书安装
update 更新trojan
updateWeb 更新trojan管理程序
version 显示版本号
import [path] 导入sql文件
export [path] 导出sql文件
web 以web方式启动
Flags:
-h, --help help for trojan
```
## 注意
安装完trojan后强烈建议开启BBR等加速: [one_click_script](https://github.com/jinwyp/one_click_script)
## Thanks
感谢JetBrains提供的免费GoLand
[![avatar](asset/jetbrains.svg)](https://jb.gg/OpenSource)
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+23
View File
@@ -0,0 +1,23 @@
FROM --platform=$TARGETPLATFORM centos:7
LABEL maintainer="Jrohy <euvkzx@Jrohy.com>"
ARG TARGETARCH
ARG VERSION_CHECK="https://api.github.com/repos/Jrohy/trojan/releases/latest"
ARG DOWNLAOD_URL="https://github.com/Jrohy/trojan/releases/download/"
ARG SERVICE_URL="https://raw.githubusercontent.com/Jrohy/trojan/master/asset/trojan-web.service"
ARG SYSTEMCTL_URL="https://raw.githubusercontent.com/gdraheim/docker-systemctl-replacement/master/files/docker/systemctl.py"
RUN yum install socat bash-completion crontabs iptables openssl unzip -y && \
LATEST_VERSION=`curl -H 'Cache-Control: no-cache' -s "$VERSION_CHECK" | grep 'tag_name' | cut -d\" -f4` && \
[[ $TARGETARCH =~ "arm64" ]] && ARCH="arm64" || ARCH="amd64" && \
curl -fL "$DOWNLAOD_URL/$LATEST_VERSION/trojan-linux-$ARCH" -o /usr/local/bin/trojan && \
curl -L $SERVICE_URL -o /etc/systemd/system/trojan-web.service && \
curl -L $SYSTEMCTL_URL -o /usr/bin/systemctl && \
chmod +x /usr/local/bin/trojan /usr/bin/systemctl && \
echo "source <(trojan completion bash)" >> ~/.bashrc && \
yum clean all
+12
View File
@@ -0,0 +1,12 @@
package asset
import "embed"
//go:embed trojan-install.sh client.json clash-rules.yaml
var f embed.FS
// GetAsset 获取资源[]byte
func GetAsset(name string) []byte {
data, _ := f.ReadFile(name)
return data
}
+117
View File
@@ -0,0 +1,117 @@
# Source: https://github.com/Loyalsoldier/clash-rules
rules:
- RULE-SET,applications,DIRECT
- DOMAIN,clash.razord.top,DIRECT
- DOMAIN,yacd.haishan.me,DIRECT
- RULE-SET,private,DIRECT
- RULE-SET,reject,REJECT
- RULE-SET,icloud,DIRECT
- RULE-SET,apple,DIRECT
- RULE-SET,google,DIRECT
- RULE-SET,proxy,PROXY
- RULE-SET,direct,DIRECT
- RULE-SET,lancidr,DIRECT
- RULE-SET,cncidr,DIRECT
- RULE-SET,telegramcidr,PROXY
- GEOIP,LAN,DIRECT
- GEOIP,CN,DIRECT
- MATCH,PROXY
rule-providers:
reject:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/reject.txt"
path: ./ruleset/reject.yaml
interval: 86400
icloud:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/icloud.txt"
path: ./ruleset/icloud.yaml
interval: 86400
apple:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/apple.txt"
path: ./ruleset/apple.yaml
interval: 86400
google:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/google.txt"
path: ./ruleset/google.yaml
interval: 86400
proxy:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/proxy.txt"
path: ./ruleset/proxy.yaml
interval: 86400
direct:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/direct.txt"
path: ./ruleset/direct.yaml
interval: 86400
private:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/private.txt"
path: ./ruleset/private.yaml
interval: 86400
gfw:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/gfw.txt"
path: ./ruleset/gfw.yaml
interval: 86400
greatfire:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/greatfire.txt"
path: ./ruleset/greatfire.yaml
interval: 86400
tld-not-cn:
type: http
behavior: domain
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/tld-not-cn.txt"
path: ./ruleset/tld-not-cn.yaml
interval: 86400
telegramcidr:
type: http
behavior: ipcidr
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/telegramcidr.txt"
path: ./ruleset/telegramcidr.yaml
interval: 86400
cncidr:
type: http
behavior: ipcidr
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/cncidr.txt"
path: ./ruleset/cncidr.yaml
interval: 86400
lancidr:
type: http
behavior: ipcidr
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/lancidr.txt"
path: ./ruleset/lancidr.yaml
interval: 86400
applications:
type: http
behavior: classical
url: "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/applications.txt"
path: ./ruleset/applications.yaml
interval: 86400
+33
View File
@@ -0,0 +1,33 @@
{
"run_type": "client",
"local_addr": "127.0.0.1",
"local_port": 1080,
"remote_addr": "example.com",
"remote_port": 443,
"password": [
"password1"
],
"log_level": 1,
"ssl": {
"verify": true,
"verify_hostname": true,
"cert": "",
"cipher": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:AES128-SHA:AES256-SHA:DES-CBC3-SHA",
"cipher_tls13": "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384",
"sni": "",
"alpn": [
"h2",
"http/1.1"
],
"reuse_session": true,
"session_ticket": false,
"curves": ""
},
"tcp": {
"no_delay": true,
"keep_alive": true,
"reuse_port": false,
"fast_open": false,
"fast_open_qlen": 20
}
}
+66
View File
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve"
>
<g>
<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24">
<stop offset="0" style="stop-color:#FCEE39"/>
<stop offset="1" style="stop-color:#F37B3D"/>
</linearGradient>
<path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9
c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0
c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/>
<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546">
<stop offset="0" style="stop-color:#EF5A6B"/>
<stop offset="0.57" style="stop-color:#F26F4E"/>
<stop offset="1" style="stop-color:#F37B3D"/>
</linearGradient>
<path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0
c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3
c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/>
<linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562">
<stop offset="0" style="stop-color:#7C59A4"/>
<stop offset="0.3852" style="stop-color:#AF4C92"/>
<stop offset="0.7654" style="stop-color:#DC4183"/>
<stop offset="0.957" style="stop-color:#ED3D7D"/>
</linearGradient>
<path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9
c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0
c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/>
<linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971">
<stop offset="0" style="stop-color:#EF5A6B"/>
<stop offset="0.364" style="stop-color:#EE4E72"/>
<stop offset="1" style="stop-color:#ED3D7D"/>
</linearGradient>
<path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0
l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0
c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/>
<g id="XMLID_3008_">
<rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/>
<rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/>
<g id="XMLID_3009_">
<path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0
l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/>
<path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0
L45.3,43.8z"/>
<path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/>
<path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0
c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5
l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/>
<path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0
c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1
l-1.5,0v2H50.6z"/>
<path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z
M58.8,59l-0.9-2.3L57,59L58.8,59z"/>
<path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/>
<path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z"
/>
<path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0
c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6
c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7
C76.1,62.5,74.7,62,73.7,61.1z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

+200
View File
@@ -0,0 +1,200 @@
#!/bin/bash
# source: https://github.com/trojan-gfw/trojan-quickstart
set -eo pipefail
# trojan: 0, trojan-go: 1
TYPE=0
INSTALL_VERSION=""
while [[ $# > 0 ]];do
KEY="$1"
case $KEY in
-v|--version)
INSTALL_VERSION="$2"
echo -e "prepare install $INSTALL_VERSION version..\n"
shift
;;
-g|--go)
TYPE=1
;;
*)
# unknown option
;;
esac
shift # past argument or value
done
#############################
function prompt() {
while true; do
read -p "$1 [y/N] " yn
case $yn in
[Yy] ) return 0;;
[Nn]|"" ) return 1;;
esac
done
}
if [[ $(id -u) != 0 ]]; then
echo Please run this script as root.
exit 1
fi
ARCH=$(uname -m 2> /dev/null)
if [[ $ARCH != x86_64 && $ARCH != aarch64 ]];then
echo "not support $ARCH machine".
exit 1
fi
if [[ $TYPE == 0 && $ARCH != x86_64 ]];then
echo "trojan not support $ARCH machine"
exit 1
fi
if [[ $TYPE == 0 ]];then
CHECKVERSION="https://api.github.com/repos/trojan-gfw/trojan/releases/latest"
else
CHECKVERSION="https://api.github.com/repos/p4gefau1t/trojan-go/releases"
fi
NAME=trojan
if [[ -z $INSTALL_VERSION ]];then
VERSION=$(curl -H 'Cache-Control: no-cache' -s "$CHECKVERSION" | grep 'tag_name' | cut -d\" -f4 | sed 's/v//g' | head -n 1)
else
if [[ -z `curl -H 'Cache-Control: no-cache' -s "$CHECKVERSION"|grep 'tag_name'|grep $INSTALL_VERSION` ]];then
echo "no $INSTALL_VERSION version file!"
exit 1
fi
VERSION=`echo "$INSTALL_VERSION"|sed 's/v//g'`
fi
if [[ $TYPE == 0 ]];then
TARBALL="$NAME-$VERSION-linux-amd64.tar.xz"
DOWNLOADURL="https://github.com/trojan-gfw/$NAME/releases/download/v$VERSION/$TARBALL"
else
[[ $ARCH == x86_64 ]] && TARBALL="trojan-go-linux-amd64.zip" || TARBALL="trojan-go-linux-armv8.zip"
DOWNLOADURL="https://github.com/p4gefau1t/trojan-go/releases/download/v$VERSION/$TARBALL"
fi
TMPDIR="$(mktemp -d)"
INSTALLPREFIX="/usr/bin/$NAME"
SYSTEMDPREFIX=/etc/systemd/system
BINARYPATH="$INSTALLPREFIX/$NAME"
CONFIGPATH="/usr/local/etc/$NAME/config.json"
SYSTEMDPATH="$SYSTEMDPREFIX/$NAME.service"
echo Creating $NAME install directory
mkdir -p $INSTALLPREFIX /usr/local/etc/$NAME
echo Entering temp directory $TMPDIR...
cd "$TMPDIR"
echo Downloading $NAME $VERSION...
curl -LO --progress-bar "$DOWNLOADURL" || wget -q --show-progress "$DOWNLOADURL"
echo Unpacking $NAME $VERSION...
if [[ $TYPE == 0 ]];then
tar xf "$TARBALL"
cd "$NAME"
else
if [[ -z `command -v unzip` ]];then
if [[ `command -v dnf` ]];then
dnf install unzip -y
elif [[ `command -v yum` ]];then
yum install unzip -y
elif [[ `command -v apt-get` ]];then
apt-get install unzip -y
fi
fi
unzip "$TARBALL"
mv trojan-go trojan
fi
echo Installing $NAME $VERSION to $BINARYPATH...
install -Dm755 "$NAME" "$BINARYPATH"
echo Installing $NAME server config to $CONFIGPATH...
if ! [[ -f "$CONFIGPATH" ]] || prompt "The server config already exists in $CONFIGPATH, overwrite?"; then
cat > "$CONFIGPATH" << EOF
{
"run_type": "server",
"local_addr": "0.0.0.0",
"local_port": 443,
"remote_addr": "127.0.0.1",
"remote_port": 80,
"password": [
"password1",
"password2"
],
"log_level": 1,
"ssl": {
"cert": "/path/to/certificate.crt",
"key": "/path/to/private.key",
"key_password": "",
"cipher": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384",
"cipher_tls13": "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384",
"prefer_server_cipher": true,
"alpn": [
"http/1.1"
],
"alpn_port_override": {
"h2": 81
},
"reuse_session": true,
"session_ticket": false,
"session_timeout": 600,
"plain_http_response": "",
"curves": "",
"dhparam": ""
},
"tcp": {
"prefer_ipv4": false,
"no_delay": true,
"keep_alive": true,
"reuse_port": false,
"fast_open": false,
"fast_open_qlen": 20
},
"mysql": {
"enabled": false,
"server_addr": "127.0.0.1",
"server_port": 3306,
"database": "trojan",
"username": "trojan",
"password": "",
"key": "",
"cert": "",
"ca": ""
}
}
EOF
else
echo Skipping installing $NAME server config...
fi
if [[ -d "$SYSTEMDPREFIX" ]]; then
echo Installing $NAME systemd service to $SYSTEMDPATH...
[[ $TYPE == 1 ]] && { NAME="trojan-go"; FLAG="-config"; }
cat > "$SYSTEMDPATH" << EOF
[Unit]
Description=$NAME
After=network.target network-online.target nss-lookup.target mysql.service mariadb.service mysqld.service
[Service]
Type=simple
StandardError=journal
ExecStart=$BINARYPATH $FLAG $CONFIGPATH
ExecReload=/bin/kill -HUP \$MAINPID
Restart=on-failure
RestartSec=3s
[Install]
WantedBy=multi-user.target
EOF
echo Reloading systemd daemon...
systemctl daemon-reload
fi
echo Deleting temp directory $TMPDIR...
rm -rf "$TMPDIR"
echo Done!
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=trojan-web
Documentation=https://github.com/Jrohy/trojan
After=network.target network-online.target nss-lookup.target mysql.service mariadb.service mysqld.service docker.service
[Service]
Type=simple
StandardError=journal
ExecStart=/usr/local/bin/trojan web
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=3s
[Install]
WantedBy=multi-user.target
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
github_token=""
project="Jrohy/trojan"
#获取当前的这个脚本所在绝对路径
shell_path=$(cd `dirname $0`; pwd)
function uploadfile() {
file=$1
ctype=$(file -b --mime-type $file)
curl -H "Authorization: token ${github_token}" -H "Content-Type: ${ctype}" --data-binary @$file "https://uploads.github.com/repos/$project/releases/${release_id}/assets?name=$(basename $file)"
echo ""
}
function upload() {
file=$1
dgst=$1.dgst
openssl dgst -md5 $file | sed 's/([^)]*)//g' >> $dgst
openssl dgst -sha1 $file | sed 's/([^)]*)//g' >> $dgst
openssl dgst -sha256 $file | sed 's/([^)]*)//g' >> $dgst
openssl dgst -sha512 $file | sed 's/([^)]*)//g' >> $dgst
uploadfile $file
uploadfile $dgst
}
cd $shell_path
version=`git describe --tags $(git rev-list --tags --max-count=1)`
now=`TZ=Asia/Shanghai date "+%Y%m%d-%H%M"`
go_version=`go version|awk '{print $3,$4}'`
git_version=`git rev-parse HEAD`
ldflags="-w -s -X 'trojan/trojan.MVersion=$version' -X 'trojan/trojan.BuildDate=$now' -X 'trojan/trojan.GoVersion=$go_version' -X 'trojan/trojan.GitVersion=$git_version'"
GOOS=linux GOARCH=amd64 go build -ldflags "$ldflags" -o "result/trojan-linux-amd64" .
GOOS=linux GOARCH=arm64 go build -ldflags "$ldflags" -o "result/trojan-linux-arm64" .
if [[ $# == 0 ]];then
cd result
upload_item=($(ls -l|awk '{print $9}'|xargs -r))
curl -X POST -H "Authorization: token ${github_token}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/$project/releases -d '{"tag_name":"'$version'", "name":"'$version'"}'
sleep 2
release_id=`curl -H 'Cache-Control: no-cache' -s https://api.github.com/repos/$project/releases/latest|grep id|awk 'NR==1{print $2}'|sed 's/,//'`
for item in ${upload_item[@]}
do
upload $item
done
echo "upload completed!"
cd $shell_path
rm -rf result
fi
+19
View File
@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
"trojan/trojan"
)
// addCmd represents the add command
var addCmd = &cobra.Command{
Use: "add",
Short: "添加用户",
Run: func(cmd *cobra.Command, args []string) {
trojan.AddUser()
},
}
func init() {
rootCmd.AddCommand(addCmd)
}
+23
View File
@@ -0,0 +1,23 @@
package cmd
import (
"github.com/spf13/cobra"
"trojan/trojan"
)
// cleanCmd represents the clean command
var cleanCmd = &cobra.Command{
Use: "clean",
Short: "清空指定用户流量",
Long: `传入指定用户名来清空用户流量, 多个用户名空格隔开, 例如:
trojan clean zhangsan lisi
`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
trojan.CleanDataByName(args)
},
}
func init() {
rootCmd.AddCommand(cleanCmd)
}
+30
View File
@@ -0,0 +1,30 @@
package cmd
import (
"github.com/spf13/cobra"
"os"
)
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion",
Short: "自动命令补全(支持bash和zsh)",
Long: `
支持bash和zsh的命令补全
a. bash环境添加下面命令到 ~/.bashrc
source <(trojan completion bash)
b. zsh环境添加以下命令到~/.zshrc
source <(trojan completion zsh)
`,
}
func init() {
rootCmd.AddCommand(completionCmd)
completionCmd.AddCommand(&cobra.Command{Use: "bash", Short: "bash命令补全", Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenBashCompletion(os.Stdout)
}})
completionCmd.AddCommand(&cobra.Command{Use: "zsh", Short: "zsh命令补全", Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenZshCompletion(os.Stdout)
}})
}
+19
View File
@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
"trojan/trojan"
)
// delCmd represents the del command
var delCmd = &cobra.Command{
Use: "del",
Short: "删除用户",
Run: func(cmd *cobra.Command, args []string) {
trojan.DelUser()
},
}
func init() {
rootCmd.AddCommand(delCmd)
}
+27
View File
@@ -0,0 +1,27 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"trojan/core"
"trojan/util"
)
// exportCmd represents the export command
var exportCmd = &cobra.Command{
Use: "export",
Short: "导出数据库sql文件",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
mysql := core.GetMysql()
if err := mysql.DumpSql(args[0]); err != nil {
fmt.Println(util.Red(err.Error()))
} else {
fmt.Println(util.Green("导出sql成功!"))
}
},
}
func init() {
rootCmd.AddCommand(exportCmd)
}
+27
View File
@@ -0,0 +1,27 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"trojan/core"
"trojan/util"
)
// importCmd represents the import command
var importCmd = &cobra.Command{
Use: "import",
Short: "导入数据库sql文件",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
mysql := core.GetMysql()
if err := mysql.ExecSql(args[0]); err != nil {
fmt.Println(util.Red(err.Error()))
} else {
fmt.Println(util.Green("导入sql成功!"))
}
},
}
func init() {
rootCmd.AddCommand(importCmd)
}
+20
View File
@@ -0,0 +1,20 @@
package cmd
import (
"trojan/trojan"
"github.com/spf13/cobra"
)
// infoCmd represents the info command
var infoCmd = &cobra.Command{
Use: "info",
Short: "用户信息列表",
Run: func(cmd *cobra.Command, args []string) {
trojan.UserList()
},
}
func init() {
rootCmd.AddCommand(infoCmd)
}
+23
View File
@@ -0,0 +1,23 @@
package cmd
import (
"trojan/util"
"github.com/spf13/cobra"
)
var line int
// logCmd represents the log command
var logCmd = &cobra.Command{
Use: "log",
Short: "查看trojan日志",
Run: func(cmd *cobra.Command, args []string) {
util.Log("trojan", line)
},
}
func init() {
logCmd.Flags().IntVarP(&line, "line", "n", 300, "查看日志行数")
rootCmd.AddCommand(logCmd)
}
+20
View File
@@ -0,0 +1,20 @@
package cmd
import (
"trojan/trojan"
"github.com/spf13/cobra"
)
// portCmd represents the info command
var portCmd = &cobra.Command{
Use: "port",
Short: "修改trojan端口",
Run: func(cmd *cobra.Command, args []string) {
trojan.ChangePort()
},
}
func init() {
rootCmd.AddCommand(portCmd)
}
+19
View File
@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
"trojan/trojan"
)
// restartCmd represents the restart command
var restartCmd = &cobra.Command{
Use: "restart",
Short: "重启trojan",
Run: func(cmd *cobra.Command, args []string) {
trojan.Restart()
},
}
func init() {
rootCmd.AddCommand(restartCmd)
}
+65
View File
@@ -0,0 +1,65 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"os"
"trojan/core"
"trojan/trojan"
"trojan/util"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "trojan",
Run: func(cmd *cobra.Command, args []string) {
mainMenu()
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func check() {
if !util.IsExists("/usr/local/etc/trojan/config.json") {
fmt.Println("本机未安装trojan, 正在自动安装...")
trojan.InstallTrojan("")
core.WritePassword(nil)
trojan.InstallTls()
trojan.InstallMysql()
util.SystemctlRestart("trojan-web")
}
}
func mainMenu() {
check()
exit:
for {
fmt.Println()
fmt.Println(util.Cyan("欢迎使用trojan管理程序"))
fmt.Println()
menuList := []string{"trojan管理", "用户管理", "安装管理", "web管理", "查看配置", "生成json"}
switch util.LoopInput("请选择: ", menuList, false) {
case 1:
trojan.ControlMenu()
case 2:
trojan.UserMenu()
case 3:
trojan.InstallMenu()
case 4:
trojan.WebMenu()
case 5:
trojan.UserList()
case 6:
trojan.GenClientJson()
default:
break exit
}
}
}
+19
View File
@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
"trojan/trojan"
)
// startCmd represents the start command
var startCmd = &cobra.Command{
Use: "start",
Short: "启动trojan",
Run: func(cmd *cobra.Command, args []string) {
trojan.Start()
},
}
func init() {
rootCmd.AddCommand(startCmd)
}
+20
View File
@@ -0,0 +1,20 @@
package cmd
import (
"trojan/trojan"
"github.com/spf13/cobra"
)
// statusCmd represents the status command
var statusCmd = &cobra.Command{
Use: "status",
Short: "查看trojan状态",
Run: func(cmd *cobra.Command, args []string) {
trojan.Status(true)
},
}
func init() {
rootCmd.AddCommand(statusCmd)
}
+19
View File
@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
"trojan/trojan"
)
// stopCmd represents the stop command
var stopCmd = &cobra.Command{
Use: "stop",
Short: "停止trojan",
Run: func(cmd *cobra.Command, args []string) {
trojan.Stop()
},
}
func init() {
rootCmd.AddCommand(stopCmd)
}
+19
View File
@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
"trojan/trojan"
)
// tlsCmd represents the tls command
var tlsCmd = &cobra.Command{
Use: "tls",
Short: "证书安装",
Run: func(cmd *cobra.Command, args []string) {
trojan.InstallTls()
},
}
func init() {
rootCmd.AddCommand(tlsCmd)
}
+25
View File
@@ -0,0 +1,25 @@
package cmd
import (
"github.com/spf13/cobra"
"trojan/trojan"
)
// upgradeCmd represents the update command
var updateCmd = &cobra.Command{
Use: "update",
Short: "更新trojan",
Long: "可添加版本号更新特定版本, 比如'trojan update v0.10.0', 不添加版本号则安装最新版",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
version := ""
if len(args) == 1 {
version = args[0]
}
trojan.InstallTrojan(version)
},
}
func init() {
rootCmd.AddCommand(updateCmd)
}
+19
View File
@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
"trojan/util"
)
// updateWebCmd represents the update command
var updateWebCmd = &cobra.Command{
Use: "updateWeb",
Short: "更新trojan管理程序",
Run: func(cmd *cobra.Command, args []string) {
util.RunWebShell("https://git.io/trojan-install")
},
}
func init() {
rootCmd.AddCommand(updateWebCmd)
}
+35
View File
@@ -0,0 +1,35 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"trojan/core"
"trojan/trojan"
)
// upgradeCmd represents the update command
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "升级数据库和trojan配置文件",
}
func upgradeConfig() {
domain, _ := core.GetValue("domain")
if domain == "" {
return
}
core.WriteDomain(domain)
trojan.Restart()
}
func init() {
rootCmd.AddCommand(upgradeCmd)
upgradeCmd.AddCommand(&cobra.Command{Use: "db", Short: "升级数据库", Run: func(cmd *cobra.Command, args []string) {
if err := core.GetMysql().UpgradeDB(); err != nil {
fmt.Println(err)
}
}})
upgradeCmd.AddCommand(&cobra.Command{Use: "config", Short: "升级配置文件", Run: func(cmd *cobra.Command, args []string) {
upgradeConfig()
}})
}
+30
View File
@@ -0,0 +1,30 @@
package cmd
import (
"fmt"
"trojan/trojan"
"trojan/util"
"github.com/spf13/cobra"
)
// versionCmd represents the Version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "显示版本号",
Run: func(cmd *cobra.Command, args []string) {
upTime := trojan.UpTime()
trojanVersion := trojan.Version()
fmt.Println()
fmt.Printf("Version: %s\n\n", util.Cyan(trojan.MVersion))
fmt.Printf("BuildDate: %s\n\n", util.Cyan(trojan.BuildDate))
fmt.Printf("GoVersion: %s\n\n", util.Cyan(trojan.GoVersion))
fmt.Printf("GitVersion: %s\n\n", util.Cyan(trojan.GitVersion))
fmt.Printf("TrojanVersion: %s\n\n", util.Cyan(trojanVersion))
fmt.Printf("TrojanUpTime: %s\n\n", util.Cyan(upTime))
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
+47
View File
@@ -0,0 +1,47 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"trojan/util"
"trojan/web"
)
var (
host string
port int
ssl bool
timeout int
)
// webCmd represents the web command
var webCmd = &cobra.Command{
Use: "web",
Short: "以web方式启动",
Run: func(cmd *cobra.Command, args []string) {
web.Start(host, port, timeout, ssl)
},
}
func init() {
webCmd.Flags().StringVarP(&host, "host", "", "0.0.0.0", "web服务监听地址")
webCmd.Flags().IntVarP(&port, "port", "p", 80, "web服务启动端口")
webCmd.Flags().BoolVarP(&ssl, "ssl", "", false, "web服务是否以https方式运行")
webCmd.Flags().IntVarP(&timeout, "timeout", "t", 120, "登录超时时间(min)")
webCmd.AddCommand(&cobra.Command{Use: "stop", Short: "停止trojan-web", Run: func(cmd *cobra.Command, args []string) {
util.SystemctlStop("trojan-web")
}})
webCmd.AddCommand(&cobra.Command{Use: "start", Short: "启动trojan-web", Run: func(cmd *cobra.Command, args []string) {
util.SystemctlStart("trojan-web")
}})
webCmd.AddCommand(&cobra.Command{Use: "restart", Short: "重启trojan-web", Run: func(cmd *cobra.Command, args []string) {
util.SystemctlRestart("trojan-web")
}})
webCmd.AddCommand(&cobra.Command{Use: "status", Short: "查看trojan-web状态", Run: func(cmd *cobra.Command, args []string) {
fmt.Println(util.SystemctlStatus("trojan-web"))
}})
webCmd.AddCommand(&cobra.Command{Use: "log", Short: "查看trojan-web日志", Run: func(cmd *cobra.Command, args []string) {
util.Log("trojan-web", 300)
}})
rootCmd.AddCommand(webCmd)
}
+33
View File
@@ -0,0 +1,33 @@
package core
// Config 结构体
type Config struct {
RunType string `json:"run_type"`
LocalAddr string `json:"local_addr"`
LocalPort int `json:"local_port"`
RemoteAddr string `json:"remote_addr"`
RemotePort int `json:"remote_port"`
Password []string `json:"password"`
LogLevel int `json:"log_level"`
}
// SSL 结构体
type SSL struct {
Cert string `json:"cert"`
Cipher string `json:"cipher"`
CipherTls13 string `json:"cipher_tls13"`
Alpn []string `json:"alpn"`
ReuseSession bool `json:"reuse_session"`
SessionTicket bool `json:"session_ticket"`
Curves string `json:"curves"`
Sni string `json:"sni"`
}
// TCP 结构体
type TCP struct {
NoDelay bool `json:"no_delay"`
KeepAlive bool `json:"keep_alive"`
ReusePort bool `json:"reuse_port"`
FastOpen bool `json:"fast_open"`
FastOpenQlen int `json:"fast_open_qlen"`
}
+50
View File
@@ -0,0 +1,50 @@
package core
import (
"encoding/json"
"fmt"
"os"
"trojan/asset"
)
// ClientConfig 结构体
type ClientConfig struct {
Config
SSl ClientSSL `json:"ssl"`
Tcp ClientTCP `json:"tcp"`
}
// ClientSSL 结构体
type ClientSSL struct {
SSL
Verify bool `json:"verify"`
VerifyHostname bool `json:"verify_hostname"`
}
// ClientTCP 结构体
type ClientTCP struct {
TCP
}
// WriteClient 生成客户端json
func WriteClient(port int, password, domain, writePath string) bool {
data := asset.GetAsset("client.json")
config := ClientConfig{}
if err := json.Unmarshal(data, &config); err != nil {
fmt.Println(err)
return false
}
config.RemoteAddr = domain
config.RemotePort = port
config.Password = []string{password}
outData, err := json.MarshalIndent(config, "", " ")
if err != nil {
fmt.Println(err)
return false
}
if err = os.WriteFile(writePath, outData, 0644); err != nil {
fmt.Println(err)
return false
}
return true
}
+41
View File
@@ -0,0 +1,41 @@
package core
import (
"github.com/syndtr/goleveldb/leveldb"
)
var dbPath = "/var/lib/trojan-manager"
// GetValue 获取leveldb值
func GetValue(key string) (string, error) {
db, err := leveldb.OpenFile(dbPath, nil)
if err != nil {
return "", err
}
defer db.Close()
result, err := db.Get([]byte(key), nil)
if err != nil {
return "", err
}
return string(result), nil
}
// SetValue 设置leveldb值
func SetValue(key string, value string) error {
db, err := leveldb.OpenFile(dbPath, nil)
if err != nil {
return err
}
defer db.Close()
return db.Put([]byte(key), []byte(value), nil)
}
// DelValue 删除值
func DelValue(key string) error {
db, err := leveldb.OpenFile(dbPath, nil)
if err != nil {
return err
}
defer db.Close()
return db.Delete([]byte(key), nil)
}
+412
View File
@@ -0,0 +1,412 @@
package core
import (
"crypto/sha256"
"database/sql"
"errors"
"fmt"
mysqlDriver "github.com/go-sql-driver/mysql"
"io"
"log"
"time"
"strconv"
"strings"
// mysql sql驱动
_ "github.com/go-sql-driver/mysql"
)
// Mysql 结构体
type Mysql struct {
Enabled bool `json:"enabled"`
ServerAddr string `json:"server_addr"`
ServerPort int `json:"server_port"`
Database string `json:"database"`
Username string `json:"username"`
Password string `json:"password"`
Cafile string `json:"cafile"`
}
// User 用户表记录结构体
type User struct {
ID uint
Username string
Password string
EncryptPass string
Quota int64
Download uint64
Upload uint64
UseDays uint
ExpiryDate string
}
// PageQuery 分页查询的结构体
type PageQuery struct {
PageNum int
CurPage int
Total int
PageSize int
DataList []*User
}
// CreateTableSql 创表sql
var CreateTableSql = `
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(64) NOT NULL,
password CHAR(56) NOT NULL,
passwordShow VARCHAR(255) NOT NULL,
quota BIGINT NOT NULL DEFAULT 0,
download BIGINT UNSIGNED NOT NULL DEFAULT 0,
upload BIGINT UNSIGNED NOT NULL DEFAULT 0,
useDays int(10) DEFAULT 0,
expiryDate char(10) DEFAULT '',
PRIMARY KEY (id),
INDEX (password)
) DEFAULT CHARSET=utf8mb4;
`
// GetDB 获取mysql数据库连接
func (mysql *Mysql) GetDB() *sql.DB {
// 屏蔽mysql驱动包的日志输出
mysqlDriver.SetLogger(log.New(io.Discard, "", 0))
conn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", mysql.Username, mysql.Password, mysql.ServerAddr, mysql.ServerPort, mysql.Database)
db, err := sql.Open("mysql", conn)
if err != nil {
fmt.Println(err.Error())
return nil
}
return db
}
// CreateTable 不存在trojan user表则自动创建
func (mysql *Mysql) CreateTable() {
db := mysql.GetDB()
defer db.Close()
if _, err := db.Exec(CreateTableSql); err != nil {
fmt.Println(err)
}
}
func queryUserList(db *sql.DB, sql string) ([]*User, error) {
var (
username string
encryptPass string
passShow string
download uint64
upload uint64
quota int64
id uint
useDays uint
expiryDate string
)
var userList []*User
rows, err := db.Query(sql)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
if err := rows.Scan(&id, &username, &encryptPass, &passShow, &quota, &download, &upload, &useDays, &expiryDate); err != nil {
return nil, err
}
userList = append(userList, &User{
ID: id,
Username: username,
Password: passShow,
EncryptPass: encryptPass,
Download: download,
Upload: upload,
Quota: quota,
UseDays: useDays,
ExpiryDate: expiryDate,
})
}
return userList, nil
}
func queryUser(db *sql.DB, sql string) (*User, error) {
var (
username string
encryptPass string
passShow string
download uint64
upload uint64
quota int64
id uint
useDays uint
expiryDate string
)
row := db.QueryRow(sql)
if err := row.Scan(&id, &username, &encryptPass, &passShow, &quota, &download, &upload, &useDays, &expiryDate); err != nil {
return nil, err
}
return &User{ID: id, Username: username, Password: passShow, EncryptPass: encryptPass, Download: download, Upload: upload, Quota: quota, UseDays: useDays, ExpiryDate: expiryDate}, nil
}
// CreateUser 创建Trojan用户
func (mysql *Mysql) CreateUser(username string, base64Pass string, originPass string) error {
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
defer db.Close()
encryPass := sha256.Sum224([]byte(originPass))
if _, err := db.Exec(fmt.Sprintf("INSERT INTO users(username, password, passwordShow, quota) VALUES ('%s', '%x', '%s', -1);", username, encryPass, base64Pass)); err != nil {
fmt.Println(err)
return err
}
return nil
}
// UpdateUser 更新Trojan用户名和密码
func (mysql *Mysql) UpdateUser(id uint, username string, base64Pass string, originPass string) error {
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
defer db.Close()
encryPass := sha256.Sum224([]byte(originPass))
if _, err := db.Exec(fmt.Sprintf("UPDATE users SET username='%s', password='%x', passwordShow='%s' WHERE id=%d;", username, encryPass, base64Pass, id)); err != nil {
fmt.Println(err)
return err
}
return nil
}
// DeleteUser 删除用户
func (mysql *Mysql) DeleteUser(id uint) error {
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
defer db.Close()
if userList, err := mysql.GetData(strconv.Itoa(int(id))); err != nil {
return err
} else if userList != nil && len(userList) == 0 {
return fmt.Errorf("不存在id为%d的用户", id)
}
if _, err := db.Exec(fmt.Sprintf("DELETE FROM users WHERE id=%d;", id)); err != nil {
fmt.Println(err)
return err
}
return nil
}
// MonthlyResetData 设置了过期时间的用户,每月定时清空使用流量
func (mysql *Mysql) MonthlyResetData() error {
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
defer db.Close()
userList, err := queryUserList(db, "SELECT * FROM users WHERE useDays != 0 AND quota != 0")
if err != nil {
return err
}
for _, user := range userList {
if _, err := db.Exec(fmt.Sprintf("UPDATE users SET download=0, upload=0 WHERE id=%d;", user.ID)); err != nil {
return err
}
}
return nil
}
// DailyCheckExpire 检查是否有过期,过期了设置流量上限为0
func (mysql *Mysql) DailyCheckExpire() (bool, error) {
needRestart := false
now := time.Now()
utc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
return false, err
}
addDay, _ := time.ParseDuration("-24h")
yesterdayStr := now.Add(addDay).In(utc).Format("2006-01-02")
yesterday, _ := time.Parse("2006-01-02", yesterdayStr)
db := mysql.GetDB()
if db == nil {
return false, errors.New("can't connect mysql")
}
defer db.Close()
userList, err := queryUserList(db, "SELECT * FROM users WHERE quota != 0")
if err != nil {
return false, err
}
for _, user := range userList {
if expireDate, err := time.Parse("2006-01-02", user.ExpiryDate); err == nil {
if yesterday.Sub(expireDate).Seconds() >= 0 {
if _, err := db.Exec(fmt.Sprintf("UPDATE users SET quota=0 WHERE id=%d;", user.ID)); err != nil {
return false, err
}
if !needRestart {
needRestart = true
}
}
}
}
return needRestart, nil
}
// CancelExpire 取消过期时间
func (mysql *Mysql) CancelExpire(id uint) error {
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
defer db.Close()
if _, err := db.Exec(fmt.Sprintf("UPDATE users SET useDays=0, expiryDate='' WHERE id=%d;", id)); err != nil {
fmt.Println(err)
return err
}
return nil
}
// SetExpire 设置过期时间
func (mysql *Mysql) SetExpire(id uint, useDays uint) error {
now := time.Now()
utc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
fmt.Println(err)
return err
}
addDay, _ := time.ParseDuration(strconv.Itoa(int(24*useDays)) + "h")
expiryDate := now.Add(addDay).In(utc).Format("2006-01-02")
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
defer db.Close()
if _, err := db.Exec(fmt.Sprintf("UPDATE users SET useDays=%d, expiryDate='%s' WHERE id=%d;", useDays, expiryDate, id)); err != nil {
fmt.Println(err)
return err
}
return nil
}
// SetQuota 限制流量
func (mysql *Mysql) SetQuota(id uint, quota int) error {
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
defer db.Close()
if _, err := db.Exec(fmt.Sprintf("UPDATE users SET quota=%d WHERE id=%d;", quota, id)); err != nil {
fmt.Println(err)
return err
}
return nil
}
// CleanData 清空流量统计
func (mysql *Mysql) CleanData(id uint) error {
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
defer db.Close()
if _, err := db.Exec(fmt.Sprintf("UPDATE users SET download=0, upload=0 WHERE id=%d;", id)); err != nil {
fmt.Println(err)
return err
}
return nil
}
// CleanDataByName 清空指定用户名流量统计数据
func (mysql *Mysql) CleanDataByName(usernames []string) error {
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
defer db.Close()
runSql := "UPDATE users SET download=0, upload=0 WHERE BINARY username in ("
for i, name := range usernames {
runSql = runSql + "'" + name + "'"
if i == len(usernames)-1 {
runSql = runSql + ")"
} else {
runSql = runSql + ","
}
}
if _, err := db.Exec(runSql); err != nil {
fmt.Println(err)
return err
}
return nil
}
// GetUserByName 通过用户名来获取用户
func (mysql *Mysql) GetUserByName(name string) *User {
db := mysql.GetDB()
if db == nil {
return nil
}
defer db.Close()
user, err := queryUser(db, fmt.Sprintf("SELECT * FROM users WHERE BINARY username='%s'", name))
if err != nil {
return nil
}
return user
}
// GetUserByPass 通过密码来获取用户
func (mysql *Mysql) GetUserByPass(pass string) *User {
db := mysql.GetDB()
if db == nil {
return nil
}
defer db.Close()
user, err := queryUser(db, fmt.Sprintf("SELECT * FROM users WHERE BINARY passwordShow='%s'", pass))
if err != nil {
return nil
}
return user
}
// PageList 通过分页获取用户记录
func (mysql *Mysql) PageList(curPage int, pageSize int) (*PageQuery, error) {
var (
total int
)
db := mysql.GetDB()
if db == nil {
return nil, errors.New("连接mysql失败")
}
defer db.Close()
offset := (curPage - 1) * pageSize
querySQL := fmt.Sprintf("SELECT * FROM users LIMIT %d, %d", offset, pageSize)
userList, err := queryUserList(db, querySQL)
if err != nil {
fmt.Println(err)
return nil, err
}
db.QueryRow("SELECT COUNT(id) FROM users").Scan(&total)
return &PageQuery{
CurPage: curPage,
PageSize: pageSize,
Total: total,
DataList: userList,
PageNum: (total + pageSize - 1) / pageSize,
}, nil
}
// GetData 获取用户记录
func (mysql *Mysql) GetData(ids ...string) ([]*User, error) {
querySQL := "SELECT * FROM users"
db := mysql.GetDB()
if db == nil {
return nil, errors.New("连接mysql失败")
}
defer db.Close()
if len(ids) > 0 {
querySQL = querySQL + " WHERE id in (" + strings.Join(ids, ",") + ")"
}
userList, err := queryUserList(db, querySQL)
if err != nil {
fmt.Println(err)
return nil, err
}
return userList, nil
}
+122
View File
@@ -0,0 +1,122 @@
package core
import (
"encoding/json"
"fmt"
"github.com/tidwall/pretty"
"github.com/tidwall/sjson"
"os"
)
var configPath = "/usr/local/etc/trojan/config.json"
// ServerConfig 结构体
type ServerConfig struct {
Config
SSl ServerSSL `json:"ssl"`
Tcp ServerTCP `json:"tcp"`
Mysql Mysql `json:"mysql"`
}
// ServerSSL 结构体
type ServerSSL struct {
SSL
Key string `json:"key"`
KeyPassword string `json:"key_password"`
PreferServerCipher bool `json:"prefer_server_cipher"`
SessionTimeout int `json:"session_timeout"`
PlainHttpResponse string `json:"plain_http_response"`
Dhparam string `json:"dhparam"`
}
// ServerTCP 结构体
type ServerTCP struct {
TCP
PreferIPv4 bool `json:"prefer_ipv4"`
}
// Load 加载服务端配置文件
func Load(path string) []byte {
if path == "" {
path = configPath
}
data, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
return nil
}
return data
}
// Save 保存服务端配置文件
func Save(data []byte, path string) bool {
if path == "" {
path = configPath
}
if err := os.WriteFile(path, pretty.Pretty(data), 0644); err != nil {
fmt.Println(err)
return false
}
return true
}
// GetConfig 获取config配置
func GetConfig() *ServerConfig {
data := Load("")
config := ServerConfig{}
if err := json.Unmarshal(data, &config); err != nil {
fmt.Println(err)
return nil
}
return &config
}
// GetMysql 获取mysql连接
func GetMysql() *Mysql {
return &GetConfig().Mysql
}
// WriteMysql 写mysql配置
func WriteMysql(mysql *Mysql) bool {
mysql.Enabled = true
data := Load("")
result, _ := sjson.SetBytes(data, "mysql", mysql)
return Save(result, "")
}
// WriteTls 写tls配置
func WriteTls(cert, key, domain string) bool {
data := Load("")
data, _ = sjson.SetBytes(data, "ssl.cert", cert)
data, _ = sjson.SetBytes(data, "ssl.key", key)
data, _ = sjson.SetBytes(data, "ssl.sni", domain)
return Save(data, "")
}
// WriteDomain 写域名
func WriteDomain(domain string) bool {
data := Load("")
data, _ = sjson.SetBytes(data, "ssl.sni", domain)
return Save(data, "")
}
// WritePassword 写密码
func WritePassword(pass []string) bool {
data := Load("")
data, _ = sjson.SetBytes(data, "password", pass)
return Save(data, "")
}
// WritePort 写trojan端口
func WritePort(port int) bool {
data := Load("")
data, _ = sjson.SetBytes(data, "local_port", port)
return Save(data, "")
}
// WriteLogLevel 写日志等级
func WriteLogLevel(level int) bool {
data := Load("")
data, _ = sjson.SetBytes(data, "log_level", level)
return Save(data, "")
}
+113
View File
@@ -0,0 +1,113 @@
package core
import (
"bufio"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"os"
"strings"
"trojan/util"
)
// UpgradeDB 升级数据库表结构以及迁移数据
func (mysql *Mysql) UpgradeDB() error {
db := mysql.GetDB()
if db == nil {
return errors.New("can't connect mysql")
}
var field string
err := db.QueryRow("SHOW COLUMNS FROM users LIKE 'passwordShow';").Scan(&field)
if err == sql.ErrNoRows {
fmt.Println(util.Yellow("正在进行数据库升级, 请稍等.."))
if _, err := db.Exec("ALTER TABLE users ADD COLUMN passwordShow VARCHAR(255) NOT NULL AFTER password;"); err != nil {
fmt.Println(err)
return err
}
userList, err := mysql.GetData()
if err != nil {
fmt.Println(err)
return err
}
for _, user := range userList {
pass, _ := GetValue(fmt.Sprintf("%s_pass", user.Username))
if pass != "" {
base64Pass := base64.StdEncoding.EncodeToString([]byte(pass))
if _, err := db.Exec(fmt.Sprintf("UPDATE users SET passwordShow='%s' WHERE id=%d;", base64Pass, user.ID)); err != nil {
fmt.Println(err)
return err
}
DelValue(fmt.Sprintf("%s_pass", user.Username))
}
}
}
err = db.QueryRow("SHOW COLUMNS FROM users LIKE 'useDays';").Scan(&field)
if err == sql.ErrNoRows {
fmt.Println(util.Yellow("正在进行数据库升级, 请稍等.."))
if _, err := db.Exec(`
ALTER TABLE users
ADD COLUMN useDays int(10) DEFAULT 0,
ADD COLUMN expiryDate char(10) DEFAULT '';
`); err != nil {
fmt.Println(err)
return err
}
}
var tableName string
err = db.QueryRow(fmt.Sprintf(
"SELECT * FROM information_schema.TABLES WHERE TABLE_NAME = 'users' AND TABLE_SCHEMA = '%s' ",
mysql.Database) + " AND TABLE_COLLATION LIKE 'utf8%';").Scan(&tableName)
if err == sql.ErrNoRows {
tempFile := "temp.sql"
mysql.DumpSql(tempFile)
mysql.ExecSql(tempFile)
os.Remove(tempFile)
}
return nil
}
// DumpSql 导出sql
func (mysql *Mysql) DumpSql(filePath string) error {
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer file.Close()
writer := bufio.NewWriter(file)
writer.WriteString("DROP TABLE IF EXISTS users;")
writer.WriteString(CreateTableSql)
db := mysql.GetDB()
userList, err := queryUserList(db, "SELECT * FROM users;")
if err != nil {
return err
}
for _, user := range userList {
writer.WriteString(fmt.Sprintf(`
INSERT INTO users(username, password, passwordShow, quota, download, upload, useDays, expiryDate) VALUES ('%s','%s','%s', %d, %d, %d, %d, '%s');`,
user.Username, user.EncryptPass, user.Password, user.Quota, user.Download, user.Upload, user.UseDays, user.ExpiryDate))
}
writer.WriteString("\n")
writer.Flush()
return nil
}
// ExecSql 执行sql
func (mysql *Mysql) ExecSql(filePath string) error {
db := mysql.GetDB()
fileByte, err := os.ReadFile(filePath)
if err != nil {
return err
}
sqlStr := string(fileByte)
sqls := strings.Split(strings.Replace(sqlStr, "\r\n", "\n", -1), ";\n")
for _, s := range sqls {
s = strings.TrimSpace(s)
if s != "" {
if _, err = db.Exec(s); err != nil {
return err
}
}
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
module trojan
go 1.21
require (
github.com/appleboy/gin-jwt/v2 v2.9.1
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
github.com/gin-contrib/gzip v0.0.6
github.com/gin-gonic/gin v1.9.1
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-sql-driver/mysql v1.7.1
github.com/gorilla/websocket v1.5.0
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/cobra v1.7.0
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
github.com/tidwall/gjson v1.17.0
github.com/tidwall/pretty v1.2.1
github.com/tidwall/sjson v1.2.5
github.com/tklauser/go-sysconf v0.3.12 // indirect
)
require (
github.com/bytedance/sonic v1.10.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.4 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+271
View File
@@ -0,0 +1,271 @@
github.com/appleboy/gin-jwt/v2 v2.9.1 h1:l29et8iLW6omcHltsOP6LLk4s3v4g2FbFs0koxGWVZs=
github.com/appleboy/gin-jwt/v2 v2.9.1/go.mod h1:jwcPZJ92uoC9nOUTOKWoN/f6JZOgMSKlFSHw5/FrRUk=
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+210
View File
@@ -0,0 +1,210 @@
#!/bin/bash
# Author: Jrohy
# github: https://github.com/Jrohy/trojan
# Supported invocation modes:
# bash install.sh [--remove]
# source <(curl -fsSL https://<your-domain>/install.sh) [--remove]
if (return 0 2>/dev/null); then
sourced=1
else
sourced=0
fi
#定义操作变量, 0为否, 1为是
help=0
remove=0
update=0
download_url="https://github.com/Jrohy/trojan/releases/download/"
version_check="https://api.github.com/repos/Jrohy/trojan/releases/latest"
service_url="https://raw.githubusercontent.com/Jrohy/trojan/master/asset/trojan-web.service"
[[ -e /var/lib/trojan-manager ]] && update=1
#Centos 临时取消别名
[[ -f /etc/redhat-release && -z $(echo $SHELL|grep zsh) ]] && unalias -a
[[ -z $(echo $SHELL|grep zsh) ]] && shell_way="bash" || shell_way="zsh"
#######color code########
red="31m"
green="32m"
yellow="33m"
blue="36m"
fuchsia="35m"
colorEcho(){
color=$1
echo -e "\033[${color}${@:2}\033[0m"
}
#######get params#########
while [[ $# > 0 ]];do
key="$1"
case $key in
--remove)
remove=1
;;
-h|--help)
help=1
;;
*)
# unknown option
;;
esac
shift # past argument or value
done
#############################
help(){
echo "Usage: bash install.sh [-h|--help] [--remove]"
echo " or: source <(curl -fsSL https://<your-domain>/install.sh) [--remove]"
echo " -h, --help Show help"
echo " --remove remove trojan"
return 0
}
removeTrojan() {
#移除trojan
rm -rf /usr/bin/trojan >/dev/null 2>&1
rm -rf /usr/local/etc/trojan >/dev/null 2>&1
rm -f /etc/systemd/system/trojan.service >/dev/null 2>&1
#移除trojan管理程序
rm -f /usr/local/bin/trojan >/dev/null 2>&1
rm -rf /var/lib/trojan-manager >/dev/null 2>&1
rm -f /etc/systemd/system/trojan-web.service >/dev/null 2>&1
systemctl daemon-reload
#移除trojan的专用db
docker rm -f trojan-mysql trojan-mariadb >/dev/null 2>&1
rm -rf /home/mysql /home/mariadb >/dev/null 2>&1
#移除环境变量
sed -i '/trojan/d' ~/.${shell_way}rc
source ~/.${shell_way}rc
colorEcho ${green} "uninstall success!"
}
checkSys() {
#检查是否为Root
[ $(id -u) != "0" ] && { colorEcho ${red} "Error: You must be root to run this script"; return 1; }
arch=$(uname -m 2> /dev/null)
if [[ $arch != x86_64 && $arch != aarch64 ]];then
colorEcho $yellow "not support $arch machine".
return 1
fi
if [[ `command -v apt-get` ]];then
package_manager='apt-get'
elif [[ `command -v dnf` ]];then
package_manager='dnf'
elif [[ `command -v yum` ]];then
package_manager='yum'
else
colorEcho $red "Not support OS!"
return 1
fi
# 缺失/usr/local/bin路径时自动添加
[[ -z `echo $PATH|grep /usr/local/bin` ]] && { echo 'export PATH=$PATH:/usr/local/bin' >> /etc/bashrc; source /etc/bashrc; }
}
#安装依赖
installDependent(){
if [[ ${package_manager} == 'dnf' || ${package_manager} == 'yum' ]];then
${package_manager} install socat crontabs bash-completion -y
else
${package_manager} update
${package_manager} install socat cron bash-completion xz-utils -y
fi
}
setupCron() {
if [[ `crontab -l 2>/dev/null|grep acme` ]]; then
if [[ -z `crontab -l 2>/dev/null|grep trojan-web` || `crontab -l 2>/dev/null|grep trojan-web|grep "&"` ]]; then
#计算北京时间早上3点时VPS的实际时间
origin_time_zone=$(date -R|awk '{printf"%d",$6}')
local_time_zone=${origin_time_zone%00}
beijing_zone=8
beijing_update_time=3
diff_zone=$[$beijing_zone-$local_time_zone]
local_time=$[$beijing_update_time-$diff_zone]
if [ $local_time -lt 0 ];then
local_time=$[24+$local_time]
elif [ $local_time -ge 24 ];then
local_time=$[$local_time-24]
fi
crontab -l 2>/dev/null|sed '/acme.sh/d' > crontab.txt
echo "0 ${local_time}"' * * * systemctl stop trojan-web; "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null; systemctl start trojan-web' >> crontab.txt
crontab crontab.txt
rm -f crontab.txt
fi
fi
}
installTrojan(){
local show_tip=0
if [[ $update == 1 ]];then
systemctl stop trojan-web >/dev/null 2>&1
rm -f /usr/local/bin/trojan
fi
lastest_version=$(curl -H 'Cache-Control: no-cache' -s "$version_check" | grep 'tag_name' | cut -d\" -f4)
echo "正在下载管理程序`colorEcho $blue $lastest_version`版本..."
[[ $arch == x86_64 ]] && bin="trojan-linux-amd64" || bin="trojan-linux-arm64"
curl -L "$download_url/$lastest_version/$bin" -o /usr/local/bin/trojan
chmod +x /usr/local/bin/trojan
if [[ ! -e /etc/systemd/system/trojan-web.service ]];then
show_tip=1
curl -L $service_url -o /etc/systemd/system/trojan-web.service
systemctl daemon-reload
systemctl enable trojan-web
fi
#命令补全环境变量
[[ -z $(grep trojan ~/.${shell_way}rc) ]] && echo "source <(trojan completion ${shell_way})" >> ~/.${shell_way}rc
source ~/.${shell_way}rc
if [[ $update == 0 ]];then
colorEcho $green "安装trojan管理程序成功!\n"
echo -e "运行命令`colorEcho $blue trojan`可进行trojan管理\n"
/usr/local/bin/trojan
else
if [[ `cat /usr/local/etc/trojan/config.json|grep -w "\"db\""` ]];then
sed -i "s/\"db\"/\"database\"/g" /usr/local/etc/trojan/config.json
systemctl restart trojan
fi
/usr/local/bin/trojan upgrade db
if [[ -z `cat /usr/local/etc/trojan/config.json|grep sni` ]];then
/usr/local/bin/trojan upgrade config
fi
systemctl restart trojan-web
colorEcho $green "更新trojan管理程序成功!\n"
fi
setupCron
[[ $show_tip == 1 ]] && echo "浏览器访问'`colorEcho $blue https://域名`'可在线trojan多用户管理"
}
main(){
[[ ${help} == 1 ]] && help && return
[[ ${remove} == 1 ]] && removeTrojan && return
[[ $update == 0 ]] && echo "正在安装trojan管理程序.." || echo "正在更新trojan管理程序.."
checkSys || return 1
[[ $update == 0 ]] && installDependent
installTrojan
}
main "$@"
result=$?
# Do not terminate the interactive shell when the script was invoked by source.
if [[ $sourced == 1 ]]; then
return $result
fi
exit $result
+7
View File
@@ -0,0 +1,7 @@
package main
import "trojan/cmd"
func main() {
cmd.Execute()
}
+43
View File
@@ -0,0 +1,43 @@
package trojan
import (
"encoding/base64"
"fmt"
"trojan/core"
"trojan/util"
)
var clientPath = "/root/config.json"
// GenClientJson 生成客户端json
func GenClientJson() {
fmt.Println()
var user core.User
domain, port := GetDomainAndPort()
mysql := core.GetMysql()
userList, err := mysql.GetData()
if err != nil {
fmt.Println(err.Error())
return
}
if len(userList) == 1 {
user = *userList[0]
} else {
UserList()
choice := util.LoopInput("请选择要生成配置文件的用户序号: ", userList, true)
if choice < 0 {
return
}
user = *userList[choice-1]
}
pass, err := base64.StdEncoding.DecodeString(user.Password)
if err != nil {
fmt.Println(util.Red("Base64解码失败: " + err.Error()))
return
}
if !core.WriteClient(port, string(pass), domain, clientPath) {
fmt.Println(util.Red("生成配置文件失败!"))
} else {
fmt.Println("成功生成配置文件: " + util.Green(clientPath))
}
}
+12
View File
@@ -0,0 +1,12 @@
package trojan
var (
// MVersion 程序版本号
MVersion string
// BuildDate 编译时间
BuildDate string
// GoVersion go版本
GoVersion string
// GitVersion git版本
GitVersion string
)
+224
View File
@@ -0,0 +1,224 @@
package trojan
import (
"fmt"
"net"
"runtime"
"strconv"
"strings"
"time"
"trojan/asset"
"trojan/core"
"trojan/util"
)
var (
dockerInstallUrl = "https://docker-install.netlify.app/install.sh"
dbDockerRun = "docker run --name trojan-mariadb --restart=always -p %d:3306 -v /home/mariadb:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=%s -e MYSQL_ROOT_HOST=%% -e MYSQL_DATABASE=trojan -d mariadb:10.2"
)
// InstallMenu 安装目录
func InstallMenu() {
fmt.Println()
menu := []string{"更新trojan", "证书申请", "安装mysql"}
switch util.LoopInput("请选择: ", menu, true) {
case 1:
InstallTrojan("")
case 2:
InstallTls()
case 3:
InstallMysql()
default:
return
}
}
// InstallDocker 安装docker
func InstallDocker() {
if !util.CheckCommandExists("docker") {
util.RunWebShell(dockerInstallUrl)
fmt.Println()
}
}
// InstallTrojan 安装trojan
func InstallTrojan(version string) {
fmt.Println()
data := string(asset.GetAsset("trojan-install.sh"))
checkTrojan := util.ExecCommandWithResult("systemctl list-unit-files|grep trojan.service")
if (checkTrojan == "" && runtime.GOARCH != "amd64") || Type() == "trojan-go" {
data = strings.ReplaceAll(data, "TYPE=0", "TYPE=1")
}
if version != "" {
data = strings.ReplaceAll(data, "INSTALL_VERSION=\"\"", "INSTALL_VERSION=\""+version+"\"")
}
util.ExecCommand(data)
util.OpenPort(443)
util.SystemctlRestart("trojan")
util.SystemctlEnable("trojan")
}
// InstallTls 安装证书
func InstallTls() {
domain := ""
server := "letsencrypt"
fmt.Println()
choice := util.LoopInput("请选择使用证书方式: ", []string{"Let's Encrypt 证书", "ZeroSSL 证书", "BuyPass 证书", "自定义证书路径"}, true)
if choice < 0 {
return
} else if choice == 4 {
crtFile := util.Input("请输入证书的cert文件路径: ", "")
keyFile := util.Input("请输入证书的key文件路径: ", "")
if !util.IsExists(crtFile) || !util.IsExists(keyFile) {
fmt.Println("输入的cert或者key文件不存在!")
} else {
domain = util.Input("请输入此证书对应的域名: ", "")
if domain == "" {
fmt.Println("输入域名为空!")
return
}
core.WriteTls(crtFile, keyFile, domain)
}
} else {
if choice == 2 {
server = "zerossl"
} else if choice == 3 {
server = "buypass"
}
localIP := util.GetLocalIP()
fmt.Printf("本机ip: %s\n", localIP)
for {
domain = util.Input("请输入申请证书的域名: ", "")
ipList, err := net.LookupIP(domain)
fmt.Printf("%s 解析到的ip: %v\n", domain, ipList)
if err != nil {
fmt.Println(err)
fmt.Println("域名有误,请重新输入")
continue
}
checkIp := false
for _, ip := range ipList {
if localIP == ip.String() {
checkIp = true
}
}
if checkIp {
break
} else {
fmt.Println("输入的域名和本机ip不一致, 请重新输入!")
}
}
util.InstallPack("socat")
if !util.IsExists("/root/.acme.sh/acme.sh") {
util.RunWebShell("https://get.acme.sh")
}
util.SystemctlStop("trojan-web")
util.OpenPort(80)
checkResult := util.ExecCommandWithResult("/root/.acme.sh/acme.sh -v|tr -cd '[0-9]'")
acmeVersion, _ := strconv.Atoi(checkResult)
if acmeVersion < 300 {
util.ExecCommand("/root/.acme.sh/acme.sh --upgrade")
}
if server != "letsencrypt" {
var email string
for {
email = util.Input(fmt.Sprintf("请输入申请%s域名所需的邮箱: ", server), "")
if email == "" {
fmt.Println("申请域名的邮箱地址为空!")
return
} else if util.VerifyEmailFormat(email) {
break
} else {
fmt.Println("邮箱格式不正确, 请重新输入!")
}
}
util.ExecCommand(fmt.Sprintf("bash /root/.acme.sh/acme.sh --server %s --register-account -m %s", server, email))
}
issueCommand := fmt.Sprintf("bash /root/.acme.sh/acme.sh --issue -d %s --debug --standalone --keylength ec-256 --force --server %s", domain, server)
if server == "buypass" {
issueCommand = issueCommand + " --days 170"
}
util.ExecCommand(issueCommand)
crtFile := "/root/.acme.sh/" + domain + "_ecc" + "/fullchain.cer"
keyFile := "/root/.acme.sh/" + domain + "_ecc" + "/" + domain + ".key"
core.WriteTls(crtFile, keyFile, domain)
}
Restart()
util.SystemctlRestart("trojan-web")
fmt.Println()
}
// InstallMysql 安装mysql
func InstallMysql() {
var (
mysql core.Mysql
choice int
)
fmt.Println()
if util.IsExists("/.dockerenv") {
choice = 2
} else {
choice = util.LoopInput("请选择: ", []string{"安装docker版mysql(mariadb)", "输入自定义mysql连接"}, true)
}
if choice < 0 {
return
} else if choice == 1 {
mysql = core.Mysql{ServerAddr: "127.0.0.1", ServerPort: util.RandomPort(), Password: util.RandString(8, util.LETTER+util.DIGITS), Username: "root", Database: "trojan"}
InstallDocker()
fmt.Println(fmt.Sprintf(dbDockerRun, mysql.ServerPort, mysql.Password))
if util.CheckCommandExists("setenforce") {
util.ExecCommand("setenforce 0")
}
util.OpenPort(mysql.ServerPort)
util.ExecCommand(fmt.Sprintf(dbDockerRun, mysql.ServerPort, mysql.Password))
db := mysql.GetDB()
for {
fmt.Printf("%s mariadb启动中,请稍等...\n", time.Now().Format("2006-01-02 15:04:05"))
err := db.Ping()
if err == nil {
db.Close()
break
} else {
time.Sleep(2 * time.Second)
}
}
fmt.Println("mariadb启动成功!")
} else if choice == 2 {
mysql = core.Mysql{}
for {
for {
mysqlUrl := util.Input("请输入mysql连接地址(格式: host:port), 默认连接地址为127.0.0.1:3306, 使用直接回车, 否则输入自定义连接地址: ",
"127.0.0.1:3306")
urlInfo := strings.Split(mysqlUrl, ":")
if len(urlInfo) != 2 {
fmt.Printf("输入的%s不符合匹配格式(host:port)\n", mysqlUrl)
continue
}
port, err := strconv.Atoi(urlInfo[1])
if err != nil {
fmt.Printf("%s不是数字\n", urlInfo[1])
continue
}
mysql.ServerAddr, mysql.ServerPort = urlInfo[0], port
break
}
mysql.Username = util.Input("请输入mysql的用户名(回车使用root): ", "root")
mysql.Password = util.Input(fmt.Sprintf("请输入mysql %s用户的密码: ", mysql.Username), "")
db := mysql.GetDB()
if db != nil && db.Ping() == nil {
mysql.Database = util.Input("请输入使用的数据库名(不存在可自动创建, 回车使用trojan): ", "trojan")
db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s;", mysql.Database))
break
} else {
fmt.Println("连接mysql失败, 请重新输入")
}
}
}
mysql.CreateTable()
core.WriteMysql(&mysql)
if userList, _ := mysql.GetData(); len(userList) == 0 {
AddUser()
}
Restart()
fmt.Println()
}
+150
View File
@@ -0,0 +1,150 @@
package trojan
import (
"errors"
"fmt"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"trojan/core"
"trojan/util"
)
// ControlMenu Trojan控制菜单
func ControlMenu() {
fmt.Println()
tType := Type()
if tType == "trojan" {
tType = "trojan-go"
} else {
tType = "trojan"
}
menu := []string{"启动trojan", "停止trojan", "重启trojan", "查看trojan状态", "查看trojan日志", "修改trojan端口"}
menu = append(menu, "切换为"+tType)
switch util.LoopInput("请选择: ", menu, true) {
case 1:
Start()
case 2:
Stop()
case 3:
Restart()
case 4:
Status(true)
case 5:
go util.Log("trojan", 300)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill)
//阻塞
<-c
case 6:
ChangePort()
case 7:
if err := SwitchType(tType); err != nil {
fmt.Println(err)
}
}
}
// Restart 重启trojan
func Restart() {
util.OpenPort(core.GetConfig().LocalPort)
util.SystemctlRestart("trojan")
}
// Start 启动trojan
func Start() {
util.OpenPort(core.GetConfig().LocalPort)
util.SystemctlStart("trojan")
}
// Stop 停止trojan
func Stop() {
util.SystemctlStop("trojan")
}
// Status 获取trojan状态
func Status(isPrint bool) string {
result := util.SystemctlStatus("trojan")
if isPrint {
fmt.Println(result)
}
return result
}
// UpTime Trojan运行时间
func UpTime() string {
result := strings.TrimSpace(util.ExecCommandWithResult("ps -Ao etime,args|grep -v grep|grep /usr/local/etc/trojan/config.json"))
resultSlice := strings.Split(result, " ")
if len(resultSlice) > 0 {
return resultSlice[0]
}
return ""
}
// ChangePort 修改trojan端口
func ChangePort() {
config := core.GetConfig()
oldPort := config.LocalPort
randomPort := util.RandomPort()
fmt.Println("当前trojan端口: " + util.Green(strconv.Itoa(oldPort)))
newPortStr := util.Input(fmt.Sprintf("请输入新的trojan端口(若要使用随机端口%s直接回车即可): ", util.Blue(strconv.Itoa(randomPort))), strconv.Itoa(randomPort))
newPort, err := strconv.Atoi(newPortStr)
if err != nil {
fmt.Println("修改端口失败: " + err.Error())
return
}
if core.WritePort(newPort) {
util.OpenPort(newPort)
fmt.Println(util.Green("端口修改成功!"))
Restart()
} else {
fmt.Println(util.Red("端口修改成功!"))
}
}
// Version Trojan版本
func Version() string {
flag := "-v"
if Type() == "trojan-go" {
flag = "-version"
}
result := strings.TrimSpace(util.ExecCommandWithResult("/usr/bin/trojan/trojan " + flag))
if len(result) == 0 {
return ""
}
firstLine := strings.Split(result, "\n")[0]
tempSlice := strings.Split(firstLine, " ")
return tempSlice[len(tempSlice)-1]
}
// SwitchType 切换Trojan类型
func SwitchType(tType string) error {
ARCH := runtime.GOARCH
if ARCH != "amd64" && ARCH != "arm64" {
return errors.New("not support " + ARCH + " machine")
}
if tType == "trojan" && ARCH != "amd64" {
return errors.New("trojan not support " + ARCH + " machine")
}
if err := core.SetValue("trojanType", tType); err != nil {
return err
}
InstallTrojan("")
return nil
}
// Type Trojan类型
func Type() string {
tType, _ := core.GetValue("trojanType")
if tType == "" {
if strings.Contains(Status(false), "trojan-go") {
tType = "trojan-go"
} else {
tType = "trojan"
}
_ = core.SetValue("trojanType", tType)
}
return tType
}
+196
View File
@@ -0,0 +1,196 @@
package trojan
import (
"encoding/base64"
"fmt"
"net/url"
"strconv"
"strings"
"trojan/core"
"trojan/util"
)
// UserMenu 用户管理菜单
func UserMenu() {
fmt.Println()
menu := []string{"新增用户", "删除用户", "限制流量", "清空流量", "设置限期", "取消限期"}
switch util.LoopInput("请选择: ", menu, false) {
case 1:
AddUser()
case 2:
DelUser()
case 3:
SetUserQuota()
case 4:
CleanData()
case 5:
SetupExpire()
case 6:
CancelExpire()
}
}
// AddUser 添加用户
func AddUser() {
randomUser := util.RandString(4, util.LETTER)
randomPass := util.RandString(8, util.LETTER+util.DIGITS)
inputUser := util.Input(fmt.Sprintf("生成随机用户名: %s, 使用直接回车, 否则输入自定义用户名: ", randomUser), randomUser)
if inputUser == "admin" {
fmt.Println(util.Yellow("不能新建用户名为'admin'的用户!"))
return
}
mysql := core.GetMysql()
if user := mysql.GetUserByName(inputUser); user != nil {
fmt.Println(util.Yellow("已存在用户名为: " + inputUser + " 的用户!"))
return
}
inputPass := util.Input(fmt.Sprintf("生成随机密码: %s, 使用直接回车, 否则输入自定义密码: ", randomPass), randomPass)
base64Pass := base64.StdEncoding.EncodeToString([]byte(inputPass))
if user := mysql.GetUserByPass(base64Pass); user != nil {
fmt.Println(util.Yellow("已存在密码为: " + inputPass + " 的用户!"))
return
}
if mysql.CreateUser(inputUser, base64Pass, inputPass) == nil {
fmt.Println("新增用户成功!")
}
}
// DelUser 删除用户
func DelUser() {
userList := UserList()
mysql := core.GetMysql()
choice := util.LoopInput("请选择要删除的用户序号: ", userList, true)
if choice == -1 {
return
}
if mysql.DeleteUser(userList[choice-1].ID) == nil {
fmt.Println("删除用户成功!")
Restart()
}
}
// SetUserQuota 限制用户流量
func SetUserQuota() {
var (
limit int
err error
)
userList := UserList()
mysql := core.GetMysql()
choice := util.LoopInput("请选择要限制流量的用户序号: ", userList, true)
if choice == -1 {
return
}
for {
quota := util.Input("请输入用户"+userList[choice-1].Username+"限制的流量大小(单位byte)", "")
limit, err = strconv.Atoi(quota)
if err != nil {
fmt.Printf("%s 不是数字, 请重新输入!\n", quota)
} else {
break
}
}
if mysql.SetQuota(userList[choice-1].ID, limit) == nil {
fmt.Println("成功设置用户" + userList[choice-1].Username + "限制流量" + util.Bytefmt(uint64(limit)))
}
}
// CleanData 清空用户流量
func CleanData() {
userList := UserList()
mysql := core.GetMysql()
choice := util.LoopInput("请选择要清空流量的用户序号: ", userList, true)
if choice == -1 {
return
}
if mysql.CleanData(userList[choice-1].ID) == nil {
fmt.Println("清空流量成功!")
}
}
// CancelExpire 取消限期
func CancelExpire() {
userList := UserList()
mysql := core.GetMysql()
choice := util.LoopInput("请选择要取消限期的用户序号: ", userList, true)
if choice == -1 {
return
}
if userList[choice-1].UseDays == 0 {
fmt.Println(util.Yellow("选择的用户未设置限期!"))
return
}
if mysql.CancelExpire(userList[choice-1].ID) == nil {
fmt.Println("取消限期成功!")
}
}
// SetupExpire 设置限期
func SetupExpire() {
userList := UserList()
mysql := core.GetMysql()
choice := util.LoopInput("请选择要设置限期的用户序号: ", userList, true)
if choice == -1 {
return
}
useDayStr := util.Input("请输入要限制使用的天数: ", "")
if useDayStr == "" {
return
} else if strings.Contains(useDayStr, "-") {
fmt.Println(util.Yellow("天数不能为负数"))
return
} else if !util.IsInteger(useDayStr) {
fmt.Println(util.Yellow("输入为非整数!"))
return
}
useDays, _ := strconv.Atoi(useDayStr)
if mysql.SetExpire(userList[choice-1].ID, uint(useDays)) == nil {
fmt.Println("设置限期成功!")
}
}
// CleanDataByName 清空指定用户流量
func CleanDataByName(usernames []string) {
mysql := core.GetMysql()
if err := mysql.CleanDataByName(usernames); err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("清空流量成功!")
}
}
// UserList 获取用户列表并打印显示
func UserList(ids ...string) []*core.User {
mysql := core.GetMysql()
userList, err := mysql.GetData(ids...)
if err != nil {
fmt.Println(err.Error())
return nil
}
domain, port := GetDomainAndPort()
for i, k := range userList {
pass, err := base64.StdEncoding.DecodeString(k.Password)
if err != nil {
pass = []byte("")
}
fmt.Printf("%d.\n", i+1)
fmt.Println("用户名: " + k.Username)
fmt.Println("密码: " + string(pass))
fmt.Println("上传流量: " + util.Cyan(util.Bytefmt(k.Upload)))
fmt.Println("下载流量: " + util.Cyan(util.Bytefmt(k.Download)))
if k.Quota < 0 {
fmt.Println("流量限额: " + util.Cyan("无限制"))
} else {
fmt.Println("流量限额: " + util.Cyan(util.Bytefmt(uint64(k.Quota))))
}
if k.UseDays == 0 {
fmt.Println("到期日期: " + util.Cyan("无限制"))
} else {
fmt.Println("到期日期: " + util.Cyan(k.ExpiryDate))
}
remark := url.QueryEscape(fmt.Sprintf("%s:%d", domain, port))
fmt.Println("分享链接: " + util.Green(fmt.Sprintf("trojan://%s@%s:%d#%s", string(pass), domain, port, remark)))
fmt.Println()
}
return userList
}
+56
View File
@@ -0,0 +1,56 @@
package trojan
import (
"crypto/sha256"
"fmt"
"trojan/core"
"trojan/util"
)
// WebMenu web管理菜单
func WebMenu() {
fmt.Println()
menu := []string{"重置web管理员密码", "修改显示的域名(非申请证书)"}
switch util.LoopInput("请选择: ", menu, true) {
case 1:
ResetAdminPass()
case 2:
SetDomain("")
}
}
// ResetAdminPass 重置管理员密码
func ResetAdminPass() {
inputPass := util.Input("请输入admin用户密码: ", "")
if inputPass == "" {
fmt.Println("撤销更改!")
} else {
encryPass := sha256.Sum224([]byte(inputPass))
err := core.SetValue("admin_pass", fmt.Sprintf("%x", encryPass))
if err == nil {
fmt.Println(util.Green("重置admin密码成功!"))
} else {
fmt.Println(err)
}
}
}
// SetDomain 设置显示的域名
func SetDomain(domain string) {
if domain == "" {
domain = util.Input("请输入要显示的域名地址: ", "")
}
if domain == "" {
fmt.Println("撤销更改!")
} else {
core.WriteDomain(domain)
Restart()
fmt.Println("修改domain成功!")
}
}
// GetDomainAndPort 获取域名和端口
func GetDomainAndPort() (string, int) {
config := core.GetConfig()
return config.SSl.Sni, config.LocalPort
}
+66
View File
@@ -0,0 +1,66 @@
package util
import (
"strconv"
"strings"
)
const (
// BYTE 字节
BYTE = 1 << (10 * iota)
// KILOBYTE 千字节
KILOBYTE
// MEGABYTE 兆字节
MEGABYTE
// GIGABYTE 吉字节
GIGABYTE
// TERABYTE 太字节
TERABYTE
// PETABYTE 拍字节
PETABYTE
// EXABYTE 艾字节
EXABYTE
)
// Bytefmt returns a human-readable byte string of the form 10M, 12.5K, and so forth. The following units are available:
// E: Exabyte
// P: Petabyte
// T: Terabyte
// G: Gigabyte
// M: Megabyte
// K: Kilobyte
// B: Byte
// The unit that results in the smallest number greater than or equal to 1 is always chosen.
func Bytefmt(bytes uint64) string {
unit := ""
value := float64(bytes)
switch {
case bytes >= EXABYTE:
unit = "E"
value = value / EXABYTE
case bytes >= PETABYTE:
unit = "P"
value = value / PETABYTE
case bytes >= TERABYTE:
unit = "T"
value = value / TERABYTE
case bytes >= GIGABYTE:
unit = "G"
value = value / GIGABYTE
case bytes >= MEGABYTE:
unit = "M"
value = value / MEGABYTE
case bytes >= KILOBYTE:
unit = "K"
value = value / KILOBYTE
case bytes >= BYTE:
unit = "B"
case bytes == 0:
return "0B"
}
result := strconv.FormatFloat(value, 'f', 2, 64)
result = strings.TrimSuffix(result, ".0")
return result + unit
}
+155
View File
@@ -0,0 +1,155 @@
package util
import (
"bufio"
"fmt"
"io"
"net/http"
"os/exec"
"strings"
)
func systemctlReplace(out string) (bool, error) {
var (
err error
isReplace bool
)
if IsExists("/.dockerenv") && strings.Contains(out, "Failed to get D-Bus") {
isReplace = true
fmt.Println(Yellow("正在下载并替换适配的systemctl。。"))
if err = ExecCommand("curl -L https://raw.githubusercontent.com/gdraheim/docker-systemctl-replacement/master/files/docker/systemctl.py -o /usr/bin/systemctl && chmod +x /usr/bin/systemctl"); err != nil {
return isReplace, err
}
fmt.Println()
}
return isReplace, err
}
func systemctlBase(name, operate string) (string, error) {
out, err := exec.Command("bash", "-c", fmt.Sprintf("systemctl %s %s", operate, name)).CombinedOutput()
if v, _ := systemctlReplace(string(out)); v {
out, err = exec.Command("bash", "-c", fmt.Sprintf("systemctl %s %s", operate, name)).CombinedOutput()
}
return string(out), err
}
// SystemctlStart 服务启动
func SystemctlStart(name string) {
if _, err := systemctlBase(name, "start"); err != nil {
fmt.Println(Red(fmt.Sprintf("启动%s失败!", name)))
} else {
fmt.Println(Green(fmt.Sprintf("启动%s成功!", name)))
}
}
// SystemctlStop 服务停止
func SystemctlStop(name string) {
if _, err := systemctlBase(name, "stop"); err != nil {
fmt.Println(Red(fmt.Sprintf("停止%s失败!", name)))
} else {
fmt.Println(Green(fmt.Sprintf("停止%s成功!", name)))
}
}
// SystemctlRestart 服务重启
func SystemctlRestart(name string) {
if _, err := systemctlBase(name, "restart"); err != nil {
fmt.Println(Red(fmt.Sprintf("重启%s失败!", name)))
} else {
fmt.Println(Green(fmt.Sprintf("重启%s成功!", name)))
}
}
// SystemctlEnable 服务设置开机自启
func SystemctlEnable(name string) {
if _, err := systemctlBase(name, "enable"); err != nil {
fmt.Println(Red(fmt.Sprintf("设置%s开机自启失败!", name)))
}
}
// SystemctlStatus 服务状态查看
func SystemctlStatus(name string) string {
out, _ := systemctlBase(name, "status")
return out
}
// CheckCommandExists 检查命令是否存在
func CheckCommandExists(command string) bool {
if _, err := exec.LookPath(command); err != nil {
return false
}
return true
}
// RunWebShell 运行网上的脚本
func RunWebShell(webShellPath string) {
if !strings.HasPrefix(webShellPath, "http") && !strings.HasPrefix(webShellPath, "https") {
fmt.Printf("shell path must start with http or https!")
return
}
resp, err := http.Get(webShellPath)
if err != nil {
fmt.Println(err.Error())
}
defer resp.Body.Close()
installShell, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
}
ExecCommand(string(installShell))
}
// ExecCommand 运行命令并实时查看运行结果
func ExecCommand(command string) error {
cmd := exec.Command("bash", "-c", command)
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
if err := cmd.Start(); err != nil {
fmt.Println("Error:The command is err: ", err.Error())
return err
}
ch := make(chan string, 100)
stdoutScan := bufio.NewScanner(stdout)
stderrScan := bufio.NewScanner(stderr)
go func() {
for stdoutScan.Scan() {
line := stdoutScan.Text()
ch <- line
}
}()
go func() {
for stderrScan.Scan() {
line := stderrScan.Text()
ch <- line
}
}()
var err error
go func() {
err = cmd.Wait()
if err != nil && !strings.Contains(err.Error(), "exit status") {
fmt.Println("wait:", err.Error())
}
close(ch)
}()
for line := range ch {
fmt.Println(line)
}
return err
}
// ExecCommandWithResult 运行命令并获取结果
func ExecCommandWithResult(command string) string {
out, err := exec.Command("bash", "-c", command).CombinedOutput()
if strings.Contains(command, "systemctl") {
if v, _ := systemctlReplace(string(out)); v {
out, err = exec.Command("bash", "-c", command).CombinedOutput()
}
}
if err != nil && !strings.Contains(err.Error(), "exit status") {
fmt.Println("err: " + err.Error())
return ""
}
return string(out)
}
+121
View File
@@ -0,0 +1,121 @@
package util
import (
"bufio"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"os"
"os/exec"
"strconv"
"time"
)
// PortIsUse 判断端口是否占用
func PortIsUse(port int) bool {
_, tcpError := net.DialTimeout("tcp", fmt.Sprintf(":%d", port), time.Millisecond*50)
udpAddr, _ := net.ResolveUDPAddr("udp4", fmt.Sprintf(":%d", port))
udpConn, udpError := net.ListenUDP("udp", udpAddr)
if udpConn != nil {
defer udpConn.Close()
}
return tcpError == nil || udpError != nil
}
// RandomPort 获取没占用的随机端口
func RandomPort() int {
for {
rand.New(rand.NewSource(time.Now().UnixNano()))
newPort := rand.Intn(65536)
if !PortIsUse(newPort) {
return newPort
}
}
}
// IsExists 检测指定路径文件或者文件夹是否存在
func IsExists(path string) bool {
_, err := os.Stat(path) //os.Stat获取文件信息
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
// GetLocalIP 获取本机ipv4地址
func GetLocalIP() string {
resp, err := http.Get("http://api.ipify.org")
if err != nil {
resp, _ = http.Get("http://icanhazip.com")
}
defer resp.Body.Close()
s, _ := io.ReadAll(resp.Body)
return string(s)
}
// InstallPack 安装指定名字软件
func InstallPack(name string) {
if !CheckCommandExists(name) {
if CheckCommandExists("yum") {
ExecCommand("yum install -y " + name)
} else if CheckCommandExists("apt-get") {
ExecCommand("apt-get update")
ExecCommand("apt-get install -y " + name)
}
}
}
// OpenPort 开通指定端口
func OpenPort(port int) {
if CheckCommandExists("firewall-cmd") {
ExecCommand(fmt.Sprintf("firewall-cmd --zone=public --add-port=%d/tcp --add-port=%d/udp --permanent >/dev/null 2>&1", port, port))
ExecCommand("firewall-cmd --reload >/dev/null 2>&1")
} else {
if len(ExecCommandWithResult(fmt.Sprintf(`iptables -nvL --line-number|grep -w "%d"`, port))) > 0 {
return
}
ExecCommand(fmt.Sprintf("iptables -I INPUT -p tcp --dport %d -j ACCEPT", port))
ExecCommand(fmt.Sprintf("iptables -I INPUT -p udp --dport %d -j ACCEPT", port))
ExecCommand(fmt.Sprintf("iptables -I OUTPUT -p udp --sport %d -j ACCEPT", port))
ExecCommand(fmt.Sprintf("iptables -I OUTPUT -p tcp --sport %d -j ACCEPT", port))
}
}
// Log 实时打印指定服务日志
func Log(serviceName string, line int) {
result, _ := LogChan(serviceName, "-n "+strconv.Itoa(line), make(chan byte))
for line := range result {
fmt.Println(line)
}
}
// LogChan 指定服务实时日志, 返回chan
func LogChan(serviceName, param string, closeChan chan byte) (chan string, error) {
cmd := exec.Command("bash", "-c", fmt.Sprintf("journalctl -f -u %s -o cat %s", serviceName, param))
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
fmt.Println("Error:The command is err: ", err.Error())
return nil, err
}
ch := make(chan string, 100)
stdoutScan := bufio.NewScanner(stdout)
go func() {
for stdoutScan.Scan() {
select {
case <-closeChan:
stdout.Close()
return
default:
ch <- stdoutScan.Text()
}
}
}()
return ch, nil
}
+168
View File
@@ -0,0 +1,168 @@
package util
import (
"fmt"
"github.com/eiannone/keyboard"
"math/rand"
"reflect"
"regexp"
"strconv"
"time"
)
const (
// RED 红色
RED = "\033[31m"
// GREEN 绿色
GREEN = "\033[32m"
// YELLOW 黄色
YELLOW = "\033[33m"
// BLUE 蓝色
BLUE = "\033[34m"
// FUCHSIA 紫红色
FUCHSIA = "\033[35m"
// CYAN 青色
CYAN = "\033[36m"
// WHITE 白色
WHITE = "\033[37m"
// RESET 重置颜色
RESET = "\033[0m"
// LETTER 大小写英文字母常量
LETTER = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// DIGITS 数字常量
DIGITS = "0123456789"
// SPECIALS 特殊字符常量
SPECIALS = "~=+%^*/()[]{}/!@#$?|"
// ALL 全部字符常量
ALL = LETTER + DIGITS + SPECIALS
)
// IsInteger 判断字符串是否为整数
func IsInteger(input string) bool {
_, err := strconv.Atoi(input)
return err == nil
}
// RandString 随机字符串
func RandString(length int, source string) string {
var runes = []rune(source)
b := make([]rune, length)
rand.New(rand.NewSource(time.Now().UnixNano()))
for i := range b {
b[i] = runes[rand.Intn(len(runes))]
}
return string(b)
}
// VerifyEmailFormat 邮箱验证
func VerifyEmailFormat(email string) bool {
pattern := `^[0-9a-z][_.0-9a-z-]{0,31}@([0-9a-z][0-9a-z-]{0,30}[0-9a-z]\.){1,4}[a-z]{2,4}$`
reg := regexp.MustCompile(pattern)
return reg.MatchString(email)
}
func getChar(str string) string {
err := keyboard.Open()
if err != nil {
panic(err)
}
defer keyboard.Close()
fmt.Print(str)
char, _, _ := keyboard.GetKey()
fmt.Printf("%c\n", char)
if char == 0 {
return ""
}
return string(char)
}
// LoopInput 循环输入选择, 或者直接回车退出
func LoopInput(tip string, choices interface{}, singleRowPrint bool) int {
reflectValue := reflect.ValueOf(choices)
if reflectValue.Kind() != reflect.Slice && reflectValue.Kind() != reflect.Array {
fmt.Println("only support slice or array type!")
return -1
}
length := reflectValue.Len()
if reflectValue.Type().String() == "[]string" {
if singleRowPrint {
for i := 0; i < length; i++ {
fmt.Printf("%d.%s\n\n", i+1, reflectValue.Index(i).Interface())
}
} else {
for i := 0; i < length; i++ {
if i%2 == 0 {
fmt.Printf("%d.%-15s\t", i+1, reflectValue.Index(i).Interface())
} else {
fmt.Printf("%d.%-15s\n\n", i+1, reflectValue.Index(i).Interface())
}
}
}
}
for {
inputString := ""
if length < 10 {
inputString = getChar(tip)
} else {
fmt.Print(tip)
_, _ = fmt.Scanln(&inputString)
}
if inputString == "" {
return -1
} else if !IsInteger(inputString) {
fmt.Println("输入有误,请重新输入")
continue
}
number, _ := strconv.Atoi(inputString)
if number <= length && number > 0 {
return number
}
fmt.Println("输入数字越界,请重新输入")
}
}
// Input 读取终端用户输入
func Input(tip string, defaultValue string) string {
input := ""
fmt.Print(tip)
_, _ = fmt.Scanln(&input)
if input == "" && defaultValue != "" {
input = defaultValue
}
return input
}
// Red 红色
func Red(str string) string {
return RED + str + RESET
}
// Green 绿色
func Green(str string) string {
return GREEN + str + RESET
}
// Yellow 黄色
func Yellow(str string) string {
return YELLOW + str + RESET
}
// Blue 蓝色
func Blue(str string) string {
return BLUE + str + RESET
}
// Fuchsia 紫红色
func Fuchsia(str string) string {
return FUCHSIA + str + RESET
}
// Cyan 青色
func Cyan(str string) string {
return CYAN + str + RESET
}
// White 白色
func White(str string) string {
return WHITE + str + RESET
}
+146
View File
@@ -0,0 +1,146 @@
package util
import (
"errors"
"fmt"
"github.com/gorilla/websocket"
"net/http"
"sync"
)
// http升级websocket协议的配置
var wsUpgrader = websocket.Upgrader{
// 允许所有CORS跨域请求
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// WsMessage websocket消息
type WsMessage struct {
MessageType int
Data []byte
}
// WsConnection 封装websocket连接
type WsConnection struct {
wsSocket *websocket.Conn // 底层websocket
inChan chan *WsMessage // 读取队列
outChan chan *WsMessage // 发送队列
mutex sync.Mutex // 避免重复关闭管道
isClosed bool
CloseChan chan byte // 关闭通知
}
// 读取协程
func (wsConn *WsConnection) wsReadLoop() {
var (
msgType int
data []byte
msg *WsMessage
err error
)
for {
// 读一个message
if msgType, data, err = wsConn.wsSocket.ReadMessage(); err != nil {
fmt.Println("Read error: " + err.Error())
goto CLOSED
}
msg = &WsMessage{
msgType,
data,
}
// 放入请求队列
select {
case wsConn.inChan <- msg:
if string(data) == "exit" {
goto CLOSED
}
case <-wsConn.CloseChan:
goto CLOSED
}
}
CLOSED:
wsConn.WsClose()
}
// 发送协程
func (wsConn *WsConnection) wsWriteLoop() {
var (
msg *WsMessage
err error
)
for {
select {
// 取一个应答
case msg = <-wsConn.outChan:
// 写给websocket
if err = wsConn.wsSocket.WriteMessage(msg.MessageType, msg.Data); err != nil {
fmt.Println(err)
goto CLOSED
}
case <-wsConn.CloseChan:
goto CLOSED
}
}
CLOSED:
wsConn.WsClose()
}
// InitWebsocket 初始化ws
func InitWebsocket(resp http.ResponseWriter, req *http.Request) (wsConn *WsConnection, err error) {
var (
wsSocket *websocket.Conn
)
// 应答客户端告知升级连接为websocket
if wsSocket, err = wsUpgrader.Upgrade(resp, req, nil); err != nil {
return
}
wsConn = &WsConnection{
wsSocket: wsSocket,
inChan: make(chan *WsMessage, 1000),
outChan: make(chan *WsMessage, 1000),
CloseChan: make(chan byte),
isClosed: false,
}
// 读协程
go wsConn.wsReadLoop()
// 写协程
go wsConn.wsWriteLoop()
return
}
// WsWrite 发送消息
func (wsConn *WsConnection) WsWrite(messageType int, data []byte) (err error) {
select {
case wsConn.outChan <- &WsMessage{messageType, data}:
case <-wsConn.CloseChan:
err = errors.New("websocket closed")
}
return
}
// WsRead 读取消息
func (wsConn *WsConnection) WsRead() (msg *WsMessage, err error) {
select {
case msg = <-wsConn.inChan:
return
case <-wsConn.CloseChan:
err = errors.New("websocket closed")
}
return
}
// WsClose 关闭连接
func (wsConn *WsConnection) WsClose() {
wsConn.wsSocket.Close()
wsConn.mutex.Lock()
defer wsConn.mutex.Unlock()
if !wsConn.isClosed {
wsConn.isClosed = true
close(wsConn.CloseChan)
}
}
+178
View File
@@ -0,0 +1,178 @@
package web
import (
"fmt"
"github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
"time"
"trojan/core"
"trojan/util"
"trojan/web/controller"
)
var (
identityKey = "id"
authMiddleware *jwt.GinJWTMiddleware
err error
)
// Login auth用户验证结构体
type Login struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func getSecretKey() string {
sk, _ := core.GetValue("secretKey")
if sk == "" {
sk = util.RandString(15, util.ALL)
core.SetValue("secretKey", sk)
}
return sk
}
func jwtInit(timeout int) {
authMiddleware, err = jwt.New(&jwt.GinJWTMiddleware{
Realm: "trojan-manager",
Key: []byte(getSecretKey()),
Timeout: time.Minute * time.Duration(timeout),
MaxRefresh: time.Minute * time.Duration(timeout),
IdentityKey: identityKey,
SendCookie: true,
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(*Login); ok {
return jwt.MapClaims{
identityKey: v.Username,
}
}
return jwt.MapClaims{}
},
IdentityHandler: func(c *gin.Context) interface{} {
claims := jwt.ExtractClaims(c)
return &Login{
Username: claims[identityKey].(string),
}
},
Authenticator: func(c *gin.Context) (interface{}, error) {
var (
password string
loginVals Login
)
if err := c.ShouldBind(&loginVals); err != nil {
return "", jwt.ErrMissingLoginValues
}
userID := loginVals.Username
pass := loginVals.Password
if err != nil {
return nil, err
}
if userID != "admin" {
mysql := core.GetMysql()
user := mysql.GetUserByName(userID)
if user == nil {
return nil, jwt.ErrFailedAuthentication
}
password = user.EncryptPass
} else {
if password, err = core.GetValue(userID + "_pass"); err != nil {
return nil, err
}
}
if password == pass {
return &loginVals, nil
}
return nil, jwt.ErrFailedAuthentication
},
Authorizator: func(data interface{}, c *gin.Context) bool {
if _, ok := data.(*Login); ok {
return true
}
return false
},
Unauthorized: func(c *gin.Context, code int, message string) {
c.JSON(code, gin.H{
"code": code,
"message": message,
})
},
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
})
if err != nil {
fmt.Println("JWT Error:" + err.Error())
}
}
func updateUser(c *gin.Context) {
responseBody := controller.ResponseBody{Msg: "success"}
defer controller.TimeCost(time.Now(), &responseBody)
username := c.DefaultPostForm("username", "admin")
pass := c.PostForm("password")
err := core.SetValue(fmt.Sprintf("%s_pass", username), pass)
if err != nil {
responseBody.Msg = err.Error()
}
c.JSON(200, responseBody)
}
// RequestUsername 获取请求接口的用户名
func RequestUsername(c *gin.Context) string {
claims := jwt.ExtractClaims(c)
return claims[identityKey].(string)
}
// Auth 权限router
func Auth(r *gin.Engine, timeout int) *jwt.GinJWTMiddleware {
jwtInit(timeout)
newInstall := gin.H{"code": 201, "message": "No administrator account found inside the database", "data": nil}
r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
claims := jwt.ExtractClaims(c)
fmt.Printf("NoRoute claims: %#v\n", claims)
c.JSON(404, gin.H{"code": 404, "message": "Page not found"})
})
r.GET("/auth/check", func(c *gin.Context) {
result, _ := core.GetValue("admin_pass")
if result == "" {
c.JSON(201, newInstall)
} else {
title, err := core.GetValue("login_title")
if err != nil {
title = "trojan 管理平台"
}
c.JSON(200, gin.H{
"code": 200,
"message": "success",
"data": map[string]string{
"title": title,
},
})
}
})
r.POST("/auth/login", authMiddleware.LoginHandler)
r.POST("/auth/register", updateUser)
authO := r.Group("/auth")
authO.Use(authMiddleware.MiddlewareFunc())
{
authO.GET("/loginUser", func(c *gin.Context) {
result, _ := core.GetValue("admin_pass")
if result == "" {
c.JSON(201, newInstall)
} else {
c.JSON(200, gin.H{
"code": 200,
"message": "success",
"data": map[string]string{
"username": RequestUsername(c),
},
})
}
})
authO.POST("/reset_pass", updateUser)
authO.POST("/logout", authMiddleware.LogoutHandler)
authO.POST("/refresh_token", authMiddleware.RefreshHandler)
}
return authMiddleware
}
+166
View File
@@ -0,0 +1,166 @@
package controller
import (
"github.com/robfig/cron/v3"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/net"
"time"
"trojan/asset"
"trojan/core"
"trojan/trojan"
)
// ResponseBody 结构体
type ResponseBody struct {
Duration string
Data interface{}
Msg string
}
type speedInfo struct {
Up uint64
Down uint64
}
var si *speedInfo
// TimeCost web函数执行用时统计方法
func TimeCost(start time.Time, body *ResponseBody) {
body.Duration = time.Since(start).String()
}
func clashRules() string {
rules, _ := core.GetValue("clash-rules")
if rules == "" {
rules = string(asset.GetAsset("clash-rules.yaml"))
}
return rules
}
// Version 获取版本信息
func Version() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
responseBody.Data = map[string]string{
"version": trojan.MVersion,
"buildDate": trojan.BuildDate,
"goVersion": trojan.GoVersion,
"gitVersion": trojan.GitVersion,
"trojanVersion": trojan.Version(),
"trojanUptime": trojan.UpTime(),
"trojanType": trojan.Type(),
}
return &responseBody
}
// SetLoginInfo 设置登录页信息
func SetLoginInfo(title string) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
err := core.SetValue("login_title", title)
if err != nil {
responseBody.Msg = err.Error()
}
return &responseBody
}
// SetDomain 设置域名
func SetDomain(domain string) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
trojan.SetDomain(domain)
return &responseBody
}
// SetClashRules 设置clash规则
func SetClashRules(rules string) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
core.SetValue("clash-rules", rules)
return &responseBody
}
// ResetClashRules 重置clash规则
func ResetClashRules() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
core.DelValue("clash-rules")
responseBody.Data = clashRules()
return &responseBody
}
// GetClashRules 获取clash规则
func GetClashRules() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
responseBody.Data = clashRules()
return &responseBody
}
// SetTrojanType 设置trojan类型
func SetTrojanType(tType string) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
err := trojan.SwitchType(tType)
if err != nil {
responseBody.Msg = err.Error()
}
return &responseBody
}
// CollectTask 启动收集主机信息任务
func CollectTask() {
var recvCount, sentCount uint64
c := cron.New()
lastIO, _ := net.IOCounters(true)
var lastRecvCount, lastSentCount uint64
for _, k := range lastIO {
lastRecvCount = lastRecvCount + k.BytesRecv
lastSentCount = lastSentCount + k.BytesSent
}
si = &speedInfo{}
c.AddFunc("@every 2s", func() {
result, _ := net.IOCounters(true)
recvCount, sentCount = 0, 0
for _, k := range result {
recvCount = recvCount + k.BytesRecv
sentCount = sentCount + k.BytesSent
}
si.Up = (sentCount - lastSentCount) / 2
si.Down = (recvCount - lastRecvCount) / 2
lastSentCount = sentCount
lastRecvCount = recvCount
lastIO = result
})
c.Start()
}
// ServerInfo 获取服务器信息
func ServerInfo() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
cpuPercent, _ := cpu.Percent(0, false)
vmInfo, _ := mem.VirtualMemory()
smInfo, _ := mem.SwapMemory()
diskInfo, _ := disk.Usage("/")
loadInfo, _ := load.Avg()
tcpCon, _ := net.Connections("tcp")
udpCon, _ := net.Connections("udp")
netCount := map[string]int{
"tcp": len(tcpCon),
"udp": len(udpCon),
}
responseBody.Data = map[string]interface{}{
"cpu": cpuPercent,
"memory": vmInfo,
"swap": smInfo,
"disk": diskInfo,
"load": loadInfo,
"speed": si,
"netCount": netCount,
}
return &responseBody
}
+105
View File
@@ -0,0 +1,105 @@
package controller
import (
"fmt"
"github.com/robfig/cron/v3"
"strconv"
"time"
"trojan/core"
"trojan/trojan"
)
var c *cron.Cron
// SetData 设置流量限制
func SetData(id uint, quota int) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
mysql := core.GetMysql()
if err := mysql.SetQuota(id, quota); err != nil {
responseBody.Msg = err.Error()
}
return &responseBody
}
// CleanData 清空流量
func CleanData(id uint) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
mysql := core.GetMysql()
if err := mysql.CleanData(id); err != nil {
responseBody.Msg = err.Error()
}
return &responseBody
}
func monthlyResetJob() {
mysql := core.GetMysql()
if err := mysql.MonthlyResetData(); err != nil {
fmt.Println("MonthlyResetError: " + err.Error())
}
}
// GetResetDay 获取重置日
func GetResetDay() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
dayStr, _ := core.GetValue("reset_day")
day, _ := strconv.Atoi(dayStr)
responseBody.Data = map[string]interface{}{
"resetDay": day,
}
return &responseBody
}
// UpdateResetDay 更新重置流量日
func UpdateResetDay(day uint) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
if day > 31 || day < 0 {
responseBody.Msg = fmt.Sprintf("%d为非正常日期", day)
return &responseBody
}
dayStr, _ := core.GetValue("reset_day")
oldDay, _ := strconv.Atoi(dayStr)
if day == uint(oldDay) {
return &responseBody
}
if len(c.Entries()) > 1 {
c.Remove(c.Entries()[len(c.Entries())-1].ID)
}
if day != 0 {
c.AddFunc(fmt.Sprintf("0 0 %d * *", day), func() {
monthlyResetJob()
})
}
core.SetValue("reset_day", strconv.Itoa(int(day)))
return &responseBody
}
// ScheduleTask 定时任务
func ScheduleTask() {
loc, _ := time.LoadLocation("Asia/Shanghai")
c = cron.New(cron.WithLocation(loc))
c.AddFunc("@daily", func() {
mysql := core.GetMysql()
if needRestart, err := mysql.DailyCheckExpire(); err != nil {
fmt.Println("DailyCheckError: " + err.Error())
} else if needRestart {
trojan.Restart()
}
})
dayStr, _ := core.GetValue("reset_day")
if dayStr == "" {
dayStr = "1"
core.SetValue("reset_day", dayStr)
}
day, _ := strconv.Atoi(dayStr)
if day != 0 {
c.AddFunc(fmt.Sprintf("0 0 %d * *", day), func() {
monthlyResetJob()
})
}
c.Start()
}
+200
View File
@@ -0,0 +1,200 @@
package controller
import (
"bufio"
"bytes"
"encoding/csv"
"fmt"
"github.com/gin-gonic/gin"
ws "github.com/gorilla/websocket"
"io"
"strconv"
"strings"
"time"
"trojan/core"
"trojan/trojan"
"trojan/util"
)
// Start 启动trojan
func Start() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
trojan.Start()
return &responseBody
}
// Stop 停止trojan
func Stop() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
trojan.Stop()
return &responseBody
}
// Restart 重启trojan
func Restart() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
trojan.Restart()
return &responseBody
}
// Update trojan更新
func Update() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
trojan.InstallTrojan("")
return &responseBody
}
// SetLogLevel 修改trojan日志等级
func SetLogLevel(level int) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
core.WriteLogLevel(level)
trojan.Restart()
return &responseBody
}
// GetLogLevel 获取trojan日志等级
func GetLogLevel() *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
config := core.GetConfig()
responseBody.Data = map[string]interface{}{
"loglevel": &config.LogLevel,
}
return &responseBody
}
// Log 通过ws查看trojan实时日志
func Log(c *gin.Context) {
var (
wsConn *util.WsConnection
err error
)
if wsConn, err = util.InitWebsocket(c.Writer, c.Request); err != nil {
fmt.Println(err)
return
}
defer wsConn.WsClose()
param := c.DefaultQuery("line", "300")
if !util.IsInteger(param) {
fmt.Println("invalid param: " + param)
return
}
if param == "-1" {
param = "--no-tail"
} else {
param = "-n " + param
}
result, err := util.LogChan("trojan", param, wsConn.CloseChan)
if err != nil {
fmt.Println(err)
return
}
for line := range result {
if err := wsConn.WsWrite(ws.TextMessage, []byte(line+"\n")); err != nil {
fmt.Println("can't send: ", line)
break
}
}
}
// ImportCsv 导入csv文件到trojan数据库
func ImportCsv(c *gin.Context) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
file, header, err := c.Request.FormFile("file")
if err != nil {
responseBody.Msg = err.Error()
return &responseBody
}
defer file.Close()
filename := header.Filename
if !strings.Contains(filename, ".csv") {
responseBody.Msg = "仅支持导入csv格式的文件"
return &responseBody
}
reader := csv.NewReader(bufio.NewReader(file))
var userList []*core.User
for {
line, readErr := reader.Read()
if readErr == io.EOF {
break
} else if readErr != nil {
responseBody.Msg = readErr.Error()
return &responseBody
}
quota, _ := strconv.Atoi(line[4])
download, _ := strconv.Atoi(line[5])
upload, _ := strconv.Atoi(line[6])
useDays, _ := strconv.Atoi(line[7])
userList = append(userList, &core.User{
Username: line[1],
Password: line[2],
EncryptPass: line[3],
Quota: int64(quota),
Download: uint64(download),
Upload: uint64(upload),
UseDays: uint(useDays),
ExpiryDate: line[8],
})
}
mysql := core.GetMysql()
db := mysql.GetDB()
if _, err = db.Exec("DROP TABLE IF EXISTS users;"); err != nil {
responseBody.Msg = err.Error()
return &responseBody
}
if _, err = db.Exec(core.CreateTableSql); err != nil {
responseBody.Msg = err.Error()
return &responseBody
}
for _, user := range userList {
if _, err = db.Exec(fmt.Sprintf(`
INSERT INTO users(username, password, passwordShow, quota, download, upload, useDays, expiryDate) VALUES ('%s','%s','%s', %d, %d, %d, %d, '%s');`,
user.Username, user.EncryptPass, user.Password, user.Quota, user.Download, user.Upload, user.UseDays, user.ExpiryDate)); err != nil {
responseBody.Msg = err.Error()
return &responseBody
}
}
return &responseBody
}
// ExportCsv 导出trojan表数据到csv文件
func ExportCsv(c *gin.Context) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
var dataBytes = new(bytes.Buffer)
//设置UTF-8 BOM, 防止中文乱码
dataBytes.WriteString("\xEF\xBB\xBF")
mysql := core.GetMysql()
userList, err := mysql.GetData()
if err != nil {
responseBody.Msg = err.Error()
return &responseBody
}
wr := csv.NewWriter(dataBytes)
for _, user := range userList {
singleUser := []string{
strconv.Itoa(int(user.ID)),
user.Username,
user.Password,
user.EncryptPass,
strconv.Itoa(int(user.Quota)),
strconv.Itoa(int(user.Download)),
strconv.Itoa(int(user.Upload)),
strconv.Itoa(int(user.UseDays)),
user.ExpiryDate,
}
wr.Write(singleUser)
}
wr.Flush()
c.Writer.Header().Set("Content-type", "application/octet-stream")
c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment;filename=%s", fmt.Sprintf("%s.csv", mysql.Database)))
c.String(200, dataBytes.String())
return nil
}
+233
View File
@@ -0,0 +1,233 @@
package controller
import (
"encoding/base64"
"fmt"
"github.com/gin-gonic/gin"
"github.com/tidwall/gjson"
"strconv"
"time"
"trojan/core"
"trojan/trojan"
)
// UserList 获取用户列表
func UserList(requestUser string) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
mysql := core.GetMysql()
userList, err := mysql.GetData()
if err != nil {
responseBody.Msg = err.Error()
return &responseBody
}
if requestUser != "admin" {
findUser := false
for _, user := range userList {
if user.Username == requestUser {
userList = []*core.User{user}
findUser = true
break
}
}
if !findUser {
userList = []*core.User{}
}
}
domain, port := trojan.GetDomainAndPort()
responseBody.Data = map[string]interface{}{
"domain": domain,
"port": port,
"userList": userList,
}
return &responseBody
}
// PageUserList 分页查询获取用户列表
func PageUserList(curPage int, pageSize int) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
mysql := core.GetMysql()
pageData, err := mysql.PageList(curPage, pageSize)
if err != nil {
responseBody.Msg = err.Error()
return &responseBody
}
domain, port := trojan.GetDomainAndPort()
responseBody.Data = map[string]interface{}{
"domain": domain,
"port": port,
"pageData": pageData,
}
return &responseBody
}
// CreateUser 创建用户
func CreateUser(username string, password string) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
if username == "admin" {
responseBody.Msg = "不能创建用户名为admin的用户!"
return &responseBody
}
mysql := core.GetMysql()
if user := mysql.GetUserByName(username); user != nil {
responseBody.Msg = "已存在用户名为: " + username + " 的用户!"
return &responseBody
}
pass, err := base64.StdEncoding.DecodeString(password)
if err != nil {
responseBody.Msg = "Base64解码失败: " + err.Error()
return &responseBody
}
if user := mysql.GetUserByPass(password); user != nil {
responseBody.Msg = "已存在密码为: " + string(pass) + " 的用户!"
return &responseBody
}
if err := mysql.CreateUser(username, password, string(pass)); err != nil {
responseBody.Msg = err.Error()
}
return &responseBody
}
// UpdateUser 更新用户
func UpdateUser(id uint, username string, password string) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
if username == "admin" {
responseBody.Msg = "不能更改用户名为admin的用户!"
return &responseBody
}
mysql := core.GetMysql()
userList, err := mysql.GetData(strconv.Itoa(int(id)))
if err != nil {
responseBody.Msg = err.Error()
return &responseBody
}
if userList[0].Username != username {
if user := mysql.GetUserByName(username); user != nil {
responseBody.Msg = "已存在用户名为: " + username + " 的用户!"
return &responseBody
}
}
pass, err := base64.StdEncoding.DecodeString(password)
if err != nil {
responseBody.Msg = "Base64解码失败: " + err.Error()
return &responseBody
}
if userList[0].Password != password {
if user := mysql.GetUserByPass(password); user != nil {
responseBody.Msg = "已存在密码为: " + string(pass) + " 的用户!"
return &responseBody
}
}
if err := mysql.UpdateUser(id, username, password, string(pass)); err != nil {
responseBody.Msg = err.Error()
}
return &responseBody
}
// DelUser 删除用户
func DelUser(id uint) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
mysql := core.GetMysql()
if err := mysql.DeleteUser(id); err != nil {
responseBody.Msg = err.Error()
} else {
trojan.Restart()
}
return &responseBody
}
// SetExpire 设置用户过期
func SetExpire(id uint, useDays uint) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
mysql := core.GetMysql()
if err := mysql.SetExpire(id, useDays); err != nil {
responseBody.Msg = err.Error()
}
return &responseBody
}
// CancelExpire 取消设置用户过期
func CancelExpire(id uint) *ResponseBody {
responseBody := ResponseBody{Msg: "success"}
defer TimeCost(time.Now(), &responseBody)
mysql := core.GetMysql()
if err := mysql.CancelExpire(id); err != nil {
responseBody.Msg = err.Error()
}
return &responseBody
}
// ClashSubInfo 获取clash订阅信息
func ClashSubInfo(c *gin.Context) {
token := c.Query("token")
if token == "" {
c.String(200, "token is null")
return
}
decodeByte, err := base64.StdEncoding.DecodeString(token)
if err != nil {
c.String(200, "token is error")
return
}
if !gjson.GetBytes(decodeByte, "user").Exists() || !gjson.GetBytes(decodeByte, "pass").Exists() {
c.String(200, "token is error")
return
}
username := gjson.GetBytes(decodeByte, "user").String()
password := gjson.GetBytes(decodeByte, "pass").String()
mysql := core.GetMysql()
user := mysql.GetUserByName(username)
if user != nil {
pass, _ := base64.StdEncoding.DecodeString(user.Password)
if password == string(pass) {
var wsData, wsHost string
userInfo := fmt.Sprintf("upload=%d, download=%d", user.Upload, user.Download)
if user.Quota != -1 {
userInfo = fmt.Sprintf("%s, total=%d", userInfo, user.Quota)
}
if user.ExpiryDate != "" {
utc, _ := time.LoadLocation("Asia/Shanghai")
t, _ := time.ParseInLocation("2006-01-02", user.ExpiryDate, utc)
userInfo = fmt.Sprintf("%s, expire=%d", userInfo, t.Unix())
}
c.Header("content-disposition", fmt.Sprintf("attachment; filename=%s", user.Username))
c.Header("subscription-userinfo", userInfo)
domain, port := trojan.GetDomainAndPort()
name := fmt.Sprintf("%s:%d", domain, port)
configData := string(core.Load(""))
if gjson.Get(configData, "websocket").Exists() && gjson.Get(configData, "websocket.enabled").Bool() {
if gjson.Get(configData, "websocket.host").Exists() {
hostTemp := gjson.Get(configData, "websocket.host").String()
if hostTemp != "" {
wsHost = fmt.Sprintf(", headers: {Host: %s}", hostTemp)
}
}
wsOpt := fmt.Sprintf("{path: %s%s}", gjson.Get(configData, "websocket.path").String(), wsHost)
wsData = fmt.Sprintf(", network: ws, udp: true, ws-opts: %s", wsOpt)
}
proxyData := fmt.Sprintf(" - {name: %s, server: %s, port: %d, type: trojan, password: %s, sni: %s%s}",
name, domain, port, password, domain, wsData)
result := fmt.Sprintf(`proxies:
%s
proxy-groups:
- name: PROXY
type: select
proxies:
- %s
%s
`, proxyData, name, clashRules())
c.String(200, result)
return
}
}
c.String(200, "token is error")
}
+196
View File
@@ -0,0 +1,196 @@
package web
import (
"embed"
"fmt"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"io/fs"
"net/http"
"strconv"
"trojan/core"
"trojan/util"
"trojan/web/controller"
)
//go:embed templates/*
var f embed.FS
func userRouter(router *gin.Engine) {
user := router.Group("/trojan/user")
{
user.GET("", func(c *gin.Context) {
requestUser := RequestUsername(c)
c.JSON(200, controller.UserList(requestUser))
})
user.GET("/page", func(c *gin.Context) {
curPageStr := c.DefaultQuery("curPage", "1")
pageSizeStr := c.DefaultQuery("pageSize", "10")
curPage, _ := strconv.Atoi(curPageStr)
pageSize, _ := strconv.Atoi(pageSizeStr)
c.JSON(200, controller.PageUserList(curPage, pageSize))
})
user.POST("", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
c.JSON(200, controller.CreateUser(username, password))
})
user.POST("/update", func(c *gin.Context) {
sid := c.PostForm("id")
username := c.PostForm("username")
password := c.PostForm("password")
id, _ := strconv.Atoi(sid)
c.JSON(200, controller.UpdateUser(uint(id), username, password))
})
user.POST("/expire", func(c *gin.Context) {
sid := c.PostForm("id")
sDays := c.PostForm("useDays")
id, _ := strconv.Atoi(sid)
useDays, _ := strconv.Atoi(sDays)
c.JSON(200, controller.SetExpire(uint(id), uint(useDays)))
})
user.DELETE("/expire", func(c *gin.Context) {
sid := c.Query("id")
id, _ := strconv.Atoi(sid)
c.JSON(200, controller.CancelExpire(uint(id)))
})
user.DELETE("", func(c *gin.Context) {
stringId := c.Query("id")
id, _ := strconv.Atoi(stringId)
c.JSON(200, controller.DelUser(uint(id)))
})
}
}
func trojanRouter(router *gin.Engine) {
router.POST("/trojan/start", func(c *gin.Context) {
c.JSON(200, controller.Start())
})
router.POST("/trojan/stop", func(c *gin.Context) {
c.JSON(200, controller.Stop())
})
router.POST("/trojan/restart", func(c *gin.Context) {
c.JSON(200, controller.Restart())
})
router.GET("/trojan/loglevel", func(c *gin.Context) {
c.JSON(200, controller.GetLogLevel())
})
router.GET("/trojan/export", func(c *gin.Context) {
result := controller.ExportCsv(c)
if result != nil {
c.JSON(200, result)
}
})
router.POST("/trojan/import", func(c *gin.Context) {
c.JSON(200, controller.ImportCsv(c))
})
router.POST("/trojan/update", func(c *gin.Context) {
c.JSON(200, controller.Update())
})
router.POST("/trojan/switch", func(c *gin.Context) {
tType := c.DefaultPostForm("type", "trojan")
c.JSON(200, controller.SetTrojanType(tType))
})
router.POST("/trojan/loglevel", func(c *gin.Context) {
slevel := c.DefaultPostForm("level", "1")
level, _ := strconv.Atoi(slevel)
c.JSON(200, controller.SetLogLevel(level))
})
router.POST("/trojan/domain", func(c *gin.Context) {
c.JSON(200, controller.SetDomain(c.PostForm("domain")))
})
router.GET("/trojan/log", func(c *gin.Context) {
controller.Log(c)
})
}
func dataRouter(router *gin.Engine) {
data := router.Group("/trojan/data")
{
data.POST("", func(c *gin.Context) {
sID := c.PostForm("id")
sQuota := c.PostForm("quota")
id, _ := strconv.Atoi(sID)
quota, _ := strconv.Atoi(sQuota)
c.JSON(200, controller.SetData(uint(id), quota))
})
data.DELETE("", func(c *gin.Context) {
sID := c.Query("id")
id, _ := strconv.Atoi(sID)
c.JSON(200, controller.CleanData(uint(id)))
})
data.POST("/resetDay", func(c *gin.Context) {
dayStr := c.DefaultPostForm("day", "1")
day, _ := strconv.Atoi(dayStr)
c.JSON(200, controller.UpdateResetDay(uint(day)))
})
data.GET("/resetDay", func(c *gin.Context) {
c.JSON(200, controller.GetResetDay())
})
}
}
func commonRouter(router *gin.Engine) {
common := router.Group("/common")
{
common.GET("/version", func(c *gin.Context) {
c.JSON(200, controller.Version())
})
common.GET("/serverInfo", func(c *gin.Context) {
c.JSON(200, controller.ServerInfo())
})
common.GET("/clashRules", func(c *gin.Context) {
c.JSON(200, controller.GetClashRules())
})
common.POST("/clashRules", func(c *gin.Context) {
rules := c.PostForm("rules")
c.JSON(200, controller.SetClashRules(rules))
})
common.DELETE("/clashRules", func(c *gin.Context) {
c.JSON(200, controller.ResetClashRules())
})
common.POST("/loginInfo", func(c *gin.Context) {
c.JSON(200, controller.SetLoginInfo(c.PostForm("title")))
})
}
}
func staticRouter(router *gin.Engine) {
staticFs, _ := fs.Sub(f, "templates/static")
router.StaticFS("/static", http.FS(staticFs))
router.GET("/", func(c *gin.Context) {
indexHTML, _ := f.ReadFile("templates/" + "index.html")
c.Writer.Write(indexHTML)
})
}
func noTokenRouter(router *gin.Engine) {
router.GET("/trojan/user/subscribe", func(c *gin.Context) {
controller.ClashSubInfo(c)
})
}
// Start web启动入口
func Start(host string, port, timeout int, isSSL bool) {
router := gin.Default()
router.SetTrustedProxies(nil)
router.Use(gzip.Gzip(gzip.DefaultCompression))
staticRouter(router)
noTokenRouter(router)
router.Use(Auth(router, timeout).MiddlewareFunc())
trojanRouter(router)
userRouter(router)
dataRouter(router)
commonRouter(router)
controller.ScheduleTask()
controller.CollectTask()
util.OpenPort(port)
if isSSL {
config := core.GetConfig()
ssl := &config.SSl
router.RunTLS(fmt.Sprintf("%s:%d", host, port), ssl.Cert, ssl.Key)
} else {
router.Run(fmt.Sprintf("%s:%d", host, port))
}
}
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
}
+23
View File
@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+5
View File
@@ -0,0 +1,5 @@
{
"tabWidth": 4,
"singleQuote":true,
"semi":false
}
+15
View File
@@ -0,0 +1,15 @@
# trojan-web
[trojan管理程序](https://github.com/Jrohy/trojan)前端页面
技术栈: vue3.0 + element-plus + vite
### 项目启动步骤
1. 安装包
`npm install`
2. 开发运行
`vite`
3. 生产打包
`vite build`
**欢迎fork以改进优化页面**
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.css">
<link rel="stylesheet" href="https://lib.baomitu.com/element-plus/2.3.14/index.min.css">
<script src="https://lib.baomitu.com/vue/latest/vue.global.prod.min.js"></script>
<script src="https://lib.baomitu.com/vue-router/4.2.5/vue-router.global.min.js"></script>
<script src="https://lib.baomitu.com/vuex/4.1.0/vuex.global.prod.min.js"></script>
<script src="https://lib.baomitu.com/vue-i18n/9.4.1/vue-i18n.global.prod.min.js"></script>
<script src="https://lib.baomitu.com/axios/1.5.0/axios.min.js"></script>
<script src="https://lib.baomitu.com/crypto-js/4.1.1/crypto-js.min.js"></script>
<script src="https://npm.elemecdn.com/easyqrcodejs@4.4.13/dist/easy.qrcode.min.js"></script>
<script src="https://lib.baomitu.com/nprogress/0.2.0/nprogress.min.js"></script>
<script src="https://lib.baomitu.com/dayjs/1.11.10/dayjs.min.js"></script>
<script src="https://lib.baomitu.com/element-plus/2.3.14/index.full.min.js"></script>
</head>
<body>
<noscript>
<strong>Please enable javascript to use.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"jsx": "preserve"
},
"include": [
"./src/**/*"
]
}
+9055
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "trojan-web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"axios": "^1.5.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.10",
"easyqrcodejs": "^4.4.13",
"element-plus": "^2.3.14",
"nprogress": "^0.2.0",
"vue": "^3.3.4",
"vue-i18n": "^9.4.1",
"vue-router": "^4.2.5",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.3.4",
"@vue/compiler-sfc": "^3.3.4",
"babel-eslint": "^10.1.0",
"eslint": "^8.50.0",
"eslint-plugin-vue": "^9.17.0",
"rollup-plugin-external-globals": "^0.8.0",
"sass": "^1.68.0",
"terser": "^5.20.0",
"vite": "^4.4.9",
"vite-plugin-svg-icons": "^1.1.0"
}
}
+3
View File
@@ -0,0 +1,3 @@
<template>
<router-view />
</template>
+25
View File
@@ -0,0 +1,25 @@
import request from '@/utils/request'
export function version() {
return request.get('/common/version')
}
export function serverInfo() {
return request.get('/common/serverInfo')
}
export function setLoginInfo(data) {
return request.post('/common/loginInfo', data)
}
export function setClashRules(data) {
return request.post('/common/clashRules', data)
}
export function getClashRules() {
return request.get('/common/clashRules')
}
export function resetClashRules() {
return request.delete('/common/clashRules')
}
+17
View File
@@ -0,0 +1,17 @@
import request from '@/utils/request'
export function setQuota(data) {
return request.post('/trojan/data', data)
}
export function cleanData(id) {
return request.delete(`/trojan/data?id=${id}`)
}
export function getResetDay() {
return request.get('/trojan/data/resetDay')
}
export function updateResetDay(data) {
return request.post('/trojan/data/resetDay', data)
}
+22
View File
@@ -0,0 +1,22 @@
import request from '@/utils/request'
// 登录接口
export function login(data) {
return request.post('/auth/login', data)
}
// 注册接口
export function register(data) {
return request.post('/auth/register', data)
}
// 检查有没有初始化接口
export function check() {
return request.get('/auth/check')
}
// 重置密码接口
export function resetPass(data) {
return request.post('/auth/reset_pass', data)
}
// 获取请求用户名
export function loginUser() {
return request.get('/auth/loginUser')
}
+33
View File
@@ -0,0 +1,33 @@
import request from '@/utils/request'
export function start() {
return request.post('/trojan/start')
}
export function stop() {
return request.post('/trojan/stop')
}
export function restart() {
return request.post('/trojan/restart')
}
export function update() {
return request.post('/trojan/update')
}
export function getLoglevel() {
return request.get('/trojan/loglevel')
}
export function setLoglevel(data) {
return request.post('/trojan/loglevel', data)
}
export function setDomain(data) {
return request.post('/trojan/domain', data)
}
export function trojanSwitch(data) {
return request.post('/trojan/switch', data)
}
+25
View File
@@ -0,0 +1,25 @@
import request from '@/utils/request'
export function userList() {
return request.get('/trojan/user')
}
export function addUser(data) {
return request.post('/trojan/user', data)
}
export function updateUser(data) {
return request.post('/trojan/user/update', data)
}
export function delUser(id) {
return request.delete(`/trojan/user?id=${id}`)
}
export function setExpire(data) {
return request.post('/trojan/user/expire', data)
}
export function cancelExpire(id) {
return request.delete(`/trojan/user/expire?id=${id}`)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@@ -0,0 +1,79 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">
{{ generateTitle(item.meta.title) }}
</span>
<a v-else @click.prevent="handleLink(item)">{{ generateTitle(item.meta.title) }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
export default {
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
generateTitle(title) {
const hasKey = this.$t('route.' + title)
if (hasKey) {
const translatedTitle = this.$t('route.' + title)
return translatedTitle
}
return title
},
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: 'dashboard' } }].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(path)
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
@@ -0,0 +1,44 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>
@@ -0,0 +1,45 @@
<template>
<svg
:class="svgClass"
aria-hidden="true"
>
<use :xlink:href="iconName"></use>
</svg>
</template>
<script>
export default {
name: 'svg-icon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String
}
},
computed: {
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
+4
View File
@@ -0,0 +1,4 @@
const Mixins = import.meta.globEager("./svg/*.svg")
export default {
mixins: Object.values(Mixins).map((v) => v.default)
}
@@ -0,0 +1 @@
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M71.984 44.815H115.9L71.984 9.642v35.173zM16.094.05h63.875l47.906 38.37v76.74c0 3.392-1.682 6.645-4.677 9.044-2.995 2.399-7.056 3.746-11.292 3.746H16.094c-4.236 0-8.297-1.347-11.292-3.746-2.995-2.399-4.677-5.652-4.677-9.044V12.84C.125 5.742 7.23.05 16.094.05zm71.86 102.32V89.58h-71.86v12.79h71.86zm23.952-25.58V64H16.094v12.79h95.812z"/></svg>

After

Width:  |  Height:  |  Size: 418 B

@@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

After

Width:  |  Height:  |  Size: 944 B

@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>

After

Width:  |  Height:  |  Size: 623 B

+1
View File
@@ -0,0 +1 @@
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>

After

Width:  |  Height:  |  Size: 440 B

+122
View File
@@ -0,0 +1,122 @@
export default {
name: 'en',
el: {
colorpicker: {
confirm: 'OK',
clear: 'Clear',
},
datepicker: {
now: 'Now',
today: 'Today',
cancel: 'Cancel',
clear: 'Clear',
confirm: 'OK',
selectDate: 'Select date',
selectTime: 'Select time',
startDate: 'Start Date',
startTime: 'Start Time',
endDate: 'End Date',
endTime: 'End Time',
prevYear: 'Previous Year',
nextYear: 'Next Year',
prevMonth: 'Previous Month',
nextMonth: 'Next Month',
year: '',
month1: 'January',
month2: 'February',
month3: 'March',
month4: 'April',
month5: 'May',
month6: 'June',
month7: 'July',
month8: 'August',
month9: 'September',
month10: 'October',
month11: 'November',
month12: 'December',
week: 'week',
weeks: {
sun: 'Sun',
mon: 'Mon',
tue: 'Tue',
wed: 'Wed',
thu: 'Thu',
fri: 'Fri',
sat: 'Sat',
},
months: {
jan: 'Jan',
feb: 'Feb',
mar: 'Mar',
apr: 'Apr',
may: 'May',
jun: 'Jun',
jul: 'Jul',
aug: 'Aug',
sep: 'Sep',
oct: 'Oct',
nov: 'Nov',
dec: 'Dec',
},
},
select: {
loading: 'Loading',
noMatch: 'No matching data',
noData: 'No data',
placeholder: 'Select',
},
cascader: {
noMatch: 'No matching data',
loading: 'Loading',
placeholder: 'Select',
noData: 'No data',
},
pagination: {
goto: 'Go to',
pagesize: '/page',
total: 'Total {total}',
pageClassifier: '',
deprecationWarning: 'Deprecated usages detected, please refer to the el-pagination documentation for more details',
},
messagebox: {
title: 'Message',
confirm: 'OK',
cancel: 'Cancel',
error: 'Illegal input',
},
upload: {
deleteTip: 'press delete to remove',
delete: 'Delete',
preview: 'Preview',
continue: 'Continue',
},
table: {
emptyText: 'No Data',
confirmFilter: 'Confirm',
resetFilter: 'Reset',
clearFilter: 'All',
sumText: 'Sum',
},
tree: {
emptyText: 'No Data',
},
transfer: {
noMatch: 'No matching data',
noData: 'No data',
titles: ['List 1', 'List 2'],
filterPlaceholder: 'Enter keyword',
noCheckedFormat: '{total} items',
hasCheckedFormat: '{checked}/{total} checked', // to be translated
},
image: {
error: 'FAILED',
},
pageHeader: {
title: 'Back', // to be translated
},
popconfirm: {
confirmButtonText: 'Yes',
cancelButtonText: 'No',
},
},
};
+122
View File
@@ -0,0 +1,122 @@
export default {
name: 'zh-cn',
el: {
colorpicker: {
confirm: '确定',
clear: '清空',
},
datepicker: {
now: '此刻',
today: '今天',
cancel: '取消',
clear: '清空',
confirm: '确定',
selectDate: '选择日期',
selectTime: '选择时间',
startDate: '开始日期',
startTime: '开始时间',
endDate: '结束日期',
endTime: '结束时间',
prevYear: '前一年',
nextYear: '后一年',
prevMonth: '上个月',
nextMonth: '下个月',
year: '年',
month1: '1 月',
month2: '2 月',
month3: '3 月',
month4: '4 月',
month5: '5 月',
month6: '6 月',
month7: '7 月',
month8: '8 月',
month9: '9 月',
month10: '10 月',
month11: '11 月',
month12: '12 月',
// week: '周次',
weeks: {
sun: '日',
mon: '一',
tue: '二',
wed: '三',
thu: '四',
fri: '五',
sat: '六',
},
months: {
jan: '一月',
feb: '二月',
mar: '三月',
apr: '四月',
may: '五月',
jun: '六月',
jul: '七月',
aug: '八月',
sep: '九月',
oct: '十月',
nov: '十一月',
dec: '十二月',
},
},
select: {
loading: '加载中',
noMatch: '无匹配数据',
noData: '无数据',
placeholder: '请选择',
},
cascader: {
noMatch: '无匹配数据',
loading: '加载中',
placeholder: '请选择',
noData: '暂无数据',
},
pagination: {
goto: '前往',
pagesize: '条/页',
total: '共 {total} 条',
pageClassifier: '页',
deprecationWarning: '你使用了一些已被废弃的用法,请参考 el-pagination 的官方文档',
},
messagebox: {
title: '提示',
confirm: '确定',
cancel: '取消',
error: '输入的数据不合法!',
},
upload: {
deleteTip: '按 delete 键可删除',
delete: '删除',
preview: '查看图片',
continue: '继续上传',
},
table: {
emptyText: '暂无数据',
confirmFilter: '筛选',
resetFilter: '重置',
clearFilter: '全部',
sumText: '合计',
},
tree: {
emptyText: '暂无数据',
},
transfer: {
noMatch: '无匹配数据',
noData: '无数据',
titles: ['列表 1', '列表 2'],
filterPlaceholder: '请输入搜索内容',
noCheckedFormat: '共 {total} 项',
hasCheckedFormat: '已选 {checked}/{total} 项',
},
image: {
error: '加载失败',
},
pageHeader: {
title: '返回',
},
popconfirm: {
confirmButtonText: '确定',
cancelButtonText: '取消',
},
},
};
+127
View File
@@ -0,0 +1,127 @@
export default {
ok: 'ok',
cancel: 'cancel',
reset: 'reset',
inputPass: 'input password',
inputPassAgain: 'input password again',
passLessError: "password number can't be less than 5",
passDifferentError: 'The two inputs are inconsistent!',
setupPass: 'setup admin password',
inputNotNull: "username or password can't be null!",
route: {
dashboard: 'Dashboard',
trojan: 'Trojan',
user: 'User'
},
navbar: {
version: 'System Version',
setting: 'System Setting',
password: 'Reset Password',
title: 'Change Title',
importExport: 'Import Export',
resetDay: 'Change Reset Day',
clashRules: 'Change Clash Rules',
resetTitle: 'Change data reset day of the month',
changeTitle: 'Change login page title',
changeRules: 'Change clash rules',
changeTitleSuccess: 'Change title success!',
changeRulesSuccess: 'Change clash rules success!',
resetRulesSuccess: 'Reset clash rules success!',
changeDaySuccess: 'Change data reset day success!',
closeResetSuccess: 'Close data reset success!',
inputTitle: 'Input login page title',
versionTitle: 'Trojan-manager Version',
passwordTitle: 'Change admin password',
resetSuccess: 'reset password success!',
meanClose: '0 means close data reset',
exportTip: 'export all database record to csv file',
importTip: 'import csv to database',
importSuccess: 'Import Success',
importCsv: 'import csv file',
exportCsv: 'export csv file'
},
dashboard: {
memory: 'memory',
disk: 'disk',
trojanVersion: 'Trojan Version',
trojanUser: 'Trojan User',
trojanUptime: 'Trojan Uptime',
netSpeed: 'Network Speed',
netCount: 'Connection Count',
load: 'Load',
upload: 'upload',
download: 'download',
total: 'total',
day: ' day',
hour: ' hour',
minute: ' minute',
second: ' second'
},
trojan: {
restartSuccess: 'restart trojan success!',
startSuccess: 'start trojan success!',
stopSuccess: 'stop trojan success!',
updateSuccess: 'update trojan success!',
loglevelSuccess: 'set loglevel success!',
switchSuccess: 'switch trojan type success!'
},
user: {
upload: 'upload',
download: 'download',
total: 'total',
quota: 'quota',
expiryDate: 'expiryDate',
setExpire: 'setExpire',
editExpire: 'editExpire',
cancelExpire: 'cancelExpire',
reset: 'reset data',
unlimit: 'unlimited',
search: 'search username',
limitData: 'limit data',
modifyUser: 'modify user',
trojanShareLink: 'trojan share link',
clashShareLink: 'clash share link',
importClash: 'import to clash',
meanUnlimit: '-1 means unlimited',
inputUsername: 'username',
inputPassword: 'password',
operate: 'operate',
addUser: 'add trojan user',
patchLimitUser: 'patch limit user',
patchDelUser: 'patch delete user?',
modifyUser2: 'modify user ',
userpass: 'username and password',
delUser: 'delete user ',
patchReset: 'patch reset data?',
resetUser: 'reset user ',
data: ' data?',
limitUser: 'limit user ',
limitUser2: 'limit user ',
setupUser: 'setup user ',
limitSuccess: ' data success!',
delUser1: 'delete user ',
success: ' success!',
resetUser1: 'reset user ',
limitAdmin: "can't create username is admin",
passLimit: "password doesn't support chinese!",
addUser2: 'add user ',
week: 'a week',
month: 'a month',
season: 'one season',
halfYear: 'half a year',
year: 'one year',
preset: 'preset',
days: 'days',
setExpireSuccess: 'setup expire success!',
cancelExpireSuccess: 'cancel expire success!',
remaining: 'remaining days'
},
request: {
requestError: 'request error',
authFail: 'auth fail, please login again',
accessDenied: 'access denied',
notFound: '404 not found',
serverError: 'server error',
connectError: 'connect error'
}
}
+42
View File
@@ -0,0 +1,42 @@
import { createI18n } from 'vue-i18n'
import elementEnLocale from './element/en' // element-ui lang
import elementZhLocale from './element/zh-cn'// element-ui lang
import enLocale from './en'
import zhLocale from './zh'
const messages = {
en: {
...enLocale,
...elementEnLocale
},
zh: {
...zhLocale,
...elementZhLocale
}
}
export function getLanguage() {
const chooseLanguage = localStorage.getItem('language')
if (chooseLanguage) return chooseLanguage
// if has not choose language
const language = (navigator.language || navigator.browserLanguage).toLowerCase()
const locales = Object.keys(messages)
for (const locale of locales) {
if (language.indexOf(locale) > -1) {
return locale
}
}
return 'en'
}
const i18n = createI18n({
// set locale
// options: en | zh
locale: getLanguage(),
// set locale messages
messages: messages,
fallbackLocale: 'en',
silentFallbackWarn: true,
silentTranslationWarn: true
})
export default i18n
+147
View File
@@ -0,0 +1,147 @@
export default {
ok: '确定',
cancel: '取消',
reset: '重置',
login: '登录',
register: '注册',
log: '日志',
edit: '编辑',
share: '分享',
terminal: '终端',
refresh: '刷新',
restart: '重启',
start: '启动',
stop: '暂停',
update: '更新',
add: '添加',
username: '用户名',
password: '密码',
loglevel: '日志等级',
all: '所有',
type: '类型',
line: '初始行数',
choice: '请选择',
latest: '最新日志',
delete: '删除',
inputPass: '输入密码',
inputPassAgain: '请再次输入密码',
passLessError: '密码不能小于5位',
passDifferentError: '两次输入不一致!',
setupPass: '设置admin密码',
inputNotNull: '用户名或密码不能为空!',
route: {
dashboard: '首页',
trojan: 'trojan管理',
user: '用户管理'
},
navbar: {
version: '系统版本',
setting: '系统设置',
password: '修改密码',
title: '修改标题',
clashRules: '修改规则',
importExport: '导入导出',
resetDay: '改重置日',
resetTitle: '修改流量重置日',
changeTitle: '修改登录页标题',
changeRules: '修改clash规则',
inputTitle: '输入登录页标题',
changeTitleSuccess: '修改登录页标题成功!',
changeRulesSuccess: '修改clash规则成功!',
resetRulesSuccess: '重置clash规则成功!',
changeDaySuccess: '修改流量重置日成功!',
closeResetSuccess: '关闭流量重置成功',
versionTitle: 'trojan管理程序版本',
passwordTitle: '变更管理员密码',
resetSuccess: '重置密码成功!',
meanClose: '0代表关闭流量重置',
exportTip: '导出数据库里所有记录到csv文件',
importTip: '导入csv文件数据到数据库',
importSuccess: '导入成功',
importCsv: '导入csv文件',
exportCsv: '导出csv文件'
},
dashboard: {
memory: '内存',
disk: '硬盘',
trojanVersion: 'trojan 版本',
trojanUser: 'trojan 用户数',
trojanUptime: 'trojan 已运行',
netSpeed: '网速',
netCount: '连接数',
load: '服务器负载',
upload: '上传',
download: '下载',
total: '总共',
day: '天',
hour: '时',
minute: '分',
second: '秒'
},
trojan: {
restartSuccess: '重启trojan成功!',
startSuccess: '启动trojan成功!',
stopSuccess: '停止trojan成功!',
updateSuccess: '更新trojan成功!',
loglevelSuccess: '设置日志等级成功!',
switchSuccess: '切换trojan类型成功!'
},
user: {
upload: '上传流量',
download: '下载流量',
total: '总流量',
quota: '流量限制',
expiryDate: '到期日期',
setExpire: '设置限期',
editExpire: '修改限期',
cancelExpire: '取消限期',
reset: '重置流量',
unlimit: '无',
search: '输入用户名搜索',
limitData: '限制流量',
modifyUser: '修改账密',
trojanShareLink: 'trojan链接',
clashShareLink: 'clash链接',
importClash: '导到clash',
meanUnlimit: '-1代表无流量限制',
inputUsername: '输入用户名',
inputPassword: '输入密码',
operate: '操作',
addUser: '新增trojan用户',
patchLimitUser: '限制以下用户流量',
patchDelUser: '确定批量删除以下用户?',
modifyUser2: '修改用户 ',
userpass: '的用户名和密码',
delUser: '确定删除用户 ',
patchReset: '确定重置以下用户流量?',
resetUser: '确定重置用户 ',
data: ' 的流量?',
limitUser: '确定限制用户 ',
limitUser2: '限制用户 ',
limitSuccess: '流量成功!',
delUser1: '删除用户',
success: '成功!',
resetUser1: '重置用户',
limitAdmin: '不能创建用户名为admin的用户!',
passLimit: '密码不支持中文!',
addUser2: '新增用户',
week: '单周',
month: '单月',
season: '单季',
halfYear: '半年',
year: '一年',
preset: '预设',
days: '天数',
setExpireSuccess: '设置用户限期成功!',
cancelExpireSuccess: '取消用户限期成功!',
remaining: '剩余天数'
},
request: {
requestError: '请求出错',
authFail: '授权失败,请重新登录',
accessDenied: '拒绝访问',
notFound: '请求错误,未找到该资源',
serverError: '服务端错误',
connectError: '连接服务器失败'
}
}
+42
View File
@@ -0,0 +1,42 @@
import { createApp } from 'vue'
import App from '@/App'
import ElementPlus from 'element-plus'
import '@/styles/index.scss' // global css
import store from '@/store/index'
import router from '@/router/index'
import i18n from './lang/index' // internationalization
import 'virtual:svg-icons-register'
import SvgIcon from '@/components/SvgIcon'// svg组件
import '@/icons'
const whiteList = ['/login', '/register'] // no redirect whitelist
const adminList = ['/trojan'] // need admin role
router.beforeEach(async (to, from, next) => {
if (store.state.UserToken) {
if (store.state.isAdmin === null) {
await store.dispatch('loginUser')
}
if (!store.state.isAdmin) {
router.options.routes.map((x) => {
if (adminList.indexOf(x.path) !== -1) {
x.hidden = true
}
})
}
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
} else {
next()
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next('/login')
}
}
})
createApp(App).use(ElementPlus).use(router).use(store).use(i18n).component('svg-icon', SvgIcon).mount('#app')

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