release v1.2.0

This commit is contained in:
dushixiang
2021-10-31 17:15:35 +08:00
parent 4665ab6f78
commit 6132a05786
173 changed files with 37928 additions and 9349 deletions

View File

@ -1,7 +1,6 @@
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)
### Go template ### Go template
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe
*.exe~ *.exe~
*.dll *.dll
*.so *.so
@ -17,4 +16,12 @@
# vendor/ # vendor/
.gitignore .gitignore
bin
data
docs
guacd
logs
playground
screenshot
web/node_modules/ web/node_modules/
.dockerignore

4
.gitignore vendored
View File

@ -27,3 +27,7 @@ playground/drive
playground/recording playground/recording
/log /log
# next terminal
/recording

View File

@ -12,32 +12,31 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositorie
RUN apk add gcc g++ RUN apk add gcc g++
RUN go env && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -ldflags '-linkmode external -extldflags "-static"' -o next-terminal main.go RUN go env && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -ldflags '-linkmode external -extldflags "-static"' -o next-terminal main.go
FROM guacamole/guacd:1.3.0 FROM alpine:latest
USER root
LABEL MAINTAINER="helloworld1024@foxmail.com" LABEL MAINTAINER="helloworld1024@foxmail.com"
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list ENV TZ Asia/Shanghai
RUN apt-get update && apt-get -y install supervisor
RUN mkdir -p /var/log/supervisor
COPY --from=builder /app/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
ENV DB sqlite ENV DB sqlite
ENV SQLITE_FILE 'next-terminal.db' ENV CONTAINER "true"
ENV SQLITE_FILE './data/sqlite/next-terminal.db'
ENV SERVER_PORT 8088 ENV SERVER_PORT 8088
ENV SERVER_ADDR 0.0.0.0:$SERVER_PORT ENV SERVER_ADDR 0.0.0.0:$SERVER_PORT
ENV SSHD_PORT 8089
ENV SSHD_ADDR 0.0.0.0:$SSHD_PORT
ENV TIME_ZONE=Asia/Shanghai ENV TIME_ZONE=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TIME_ZONE /etc/localtime && echo $TIME_ZONE > /etc/timezone RUN ln -snf /usr/share/zoneinfo/$TIME_ZONE /etc/localtime && echo $TIME_ZONE > /etc/timezone
WORKDIR /usr/local/next-terminal WORKDIR /usr/local/next-terminal
RUN touch config.yml
COPY --from=builder /app/next-terminal ./ COPY --from=builder /app/next-terminal ./
COPY --from=builder /app/web/build ./web/build COPY --from=builder /app/web/build ./web/build
COPY --from=builder /app/web/src/fonts/Menlo-Regular-1.ttf /usr/share/fonts/
RUN mkfontscale && mkfontdir && fc-cache EXPOSE $SERVER_PORT $SSHD_PORT
EXPOSE $SERVER_PORT RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add tzdata
RUN mkdir recording && mkdir drive RUN cp /usr/share/zoneinfo/${TZ} /etc/localtime
ENTRYPOINT /usr/bin/supervisord RUN echo ${TZ} > /etc/timezone
ENTRYPOINT ./next-terminal

View File

@ -1,50 +0,0 @@
FROM golang:alpine as builder
ENV GO111MODULE=on
ENV GUACD_VERSION=1.3.0
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /app
COPY . .
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add gcc g++
RUN go env && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -ldflags '-linkmode external -extldflags "-static"' -o next-terminal main.go
FROM arm64v8/debian:rc-buggy-20210208
ARG PREFIX_DIR=/usr/local/guacamole
WORKDIR $PREFIX_DIR
ARG BUILD_DEPENDENCIES=" \
libcairo2-dev \
libjpeg62-turbo-dev \
libpng-dev \
libtool-bin \
libossp-uuid-dev \
freerdp2-dev \
libpango1.0-dev \
libssh2-1-dev \
libtelnet-dev \
libvncserver-dev \
libwebsockets-dev \
libpulse-dev \
libssl-dev \
libvorbis-dev \
libwebp-dev\
make"
# Install dependencies
RUN apt-get update && \
apt-get install $BUILD_DEPENDENCIES && \
rm -rf /var/lib/apt/lists/*
# Install guacamole-server
RUN curl -SLO "http://apache.org/dyn/closer.cgi?action=download&filename=guacamole/${GUACD_VERSION}/source/guacamole-server-${GUACD_VERSION}.tar.gz" &&\
tar -xzf guacamole-server-${GUACD_VERSION}.tar.gz &&\
cd guacamole-server-${GUACD_VERSION} &&\
./configure --prefix="$PREFIX_DIR" --disable-guaclog &&\
make && make install && ldconfig\
rm -rf ${PREFIX_DIR}/guacamole-server-${GUACD_VERSION}.tar.gz ${PREFIX_DIR}/guacamole-server-${GUACD_VERSION} &&\

844
LICENSE
View File

@ -1,281 +1,620 @@
GNU GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 2, June 1991 Version 3, 19 November 2007
Copyright (C) 1989, 1991 Free Software Foundation, Inc., Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. of this license document, but changing it is not allowed.
Preamble Preamble
The licenses for most software are designed to take away your The GNU Affero General Public License is a free, copyleft license for
freedom to share and change it. By contrast, the GNU General Public software and other kinds of works, specifically designed to ensure
License is intended to guarantee your freedom to share and change free cooperation with the community in the case of network server software.
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software The licenses for most software and other practical works are designed
Foundation's software and to any other program whose authors commit to to take away your freedom to share and change the works. By contrast,
using it. (Some other Free Software Foundation software is covered by our General Public Licenses are intended to guarantee your freedom to
the GNU Lesser General Public License instead.) You can apply it to share and change all versions of a program--to make sure it remains free
your programs, too. software for all its users.
When we speak of free software, we are referring to freedom, not When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it them if you wish), that you receive source code or can get it if you
if you want it, that you can change the software or use pieces of it want it, that you can change the software or use pieces of it in new
in new free programs; and that you know you can do these things. free programs, and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid Developers that use our General Public Licenses protect your rights
anyone to deny you these rights or to ask you to surrender the rights. with two steps: (1) assert copyright on the software, and (2) offer
These restrictions translate to certain responsibilities for you if you you this License which gives you legal permission to copy, distribute
distribute copies of the software, or if you modify it. and/or modify the software.
For example, if you distribute copies of such a program, whether A secondary benefit of defending all users' freedom is that
gratis or for a fee, you must give the recipients all the rights that improvements made in alternate versions of the program, if they
you have. You must make sure that they, too, receive or can get the receive widespread use, become available for other developers to
source code. And you must show them these terms so they know their incorporate. Many developers of free software are heartened and
rights. encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
We protect your rights with two steps: (1) copyright the software, and The GNU Affero General Public License is designed specifically to
(2) offer you this license which gives you legal permission to copy, ensure that, in such cases, the modified source code becomes available
distribute and/or modify the software. to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
Also, for each author's protection and ours, we want to make certain An older license, called the Affero General Public License and
that everyone understands that there is no warranty for this free published by Affero, was designed to accomplish similar goals. This is
software. If the software is modified by someone else and passed on, we a different license, not a version of the Affero GPL, but Affero has
want its recipients to know that what they have is not the original, so released a new version of the Affero GPL which permits relicensing under
that any problems introduced by others will not reflect on the original this license.
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow.
GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains 0. Definitions.
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not "This License" refers to version 3 of the GNU Affero General Public License.
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's "Copyright" also means copyright-like laws that apply to other kinds of
source code as you receive it, in any medium, provided that you works, such as semiconductor masks.
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and "The Program" refers to any copyrightable work licensed under this
you may at your option offer warranty protection in exchange for a fee. License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
2. You may modify your copy or copies of the Program or any portion To "modify" a work means to copy from or adapt all or part of the work
of it, thus forming a work based on the Program, and copy and in a fashion requiring copyright permission, other than the making of an
distribute such modifications or work under the terms of Section 1 exact copy. The resulting work is called a "modified version" of the
above, provided that you also meet all of these conditions: earlier work or a work "based on" the earlier work.
a) You must cause the modified files to carry prominent notices A "covered work" means either the unmodified Program or a work based
stating that you changed the files and the date of any change. on the Program.
b) You must cause any work that you distribute or publish, that in To "propagate" a work means to do anything with it that, without
whole or in part contains or is derived from the Program or any permission, would make you directly or secondarily liable for
part thereof, to be licensed as a whole at no charge to all third infringement under applicable copyright law, except executing it on a
parties under the terms of this License. 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.
c) If the modified program normally reads commands interactively To "convey" a work means any kind of propagation that enables other
when run, you must cause it, when started running for such parties to make or receive copies. Mere interaction with a user through
interactive use in the most ordinary way, to print or display an a computer network, with no transfer of a copy, is not conveying.
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If An interactive user interface displays "Appropriate Legal Notices"
identifiable sections of that work are not derived from the Program, to the extent that it includes a convenient and prominently visible
and can be reasonably considered independent and separate works in feature that (1) displays an appropriate copyright notice, and (2)
themselves, then this License, and its terms, do not apply to those tells the user that there is no warranty for the work (except to the
sections when you distribute them as separate works. But when you extent that warranties are provided), that licensees may convey the
distribute the same sections as part of a whole which is a work based work under this License, and how to view a copy of this License. If
on the Program, the distribution of the whole must be on the terms of the interface presents a list of user commands or options, such as a
this License, whose permissions for other licensees extend to the menu, a prominent item in the list meets this criterion.
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest 1. Source Code.
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program The "source code" for a work means the preferred form of the work
with the Program (or with a work based on the Program) on a volume of for making modifications to it. "Object code" means any non-source
a storage or distribution medium does not bring the other work under form of a work.
the scope of this License.
3. You may copy and distribute the Program (or a work based on it, A "Standard Interface" means an interface that either is an official
under Section 2) in object code or executable form under the terms of standard defined by a recognized standards body, or, in the case of
Sections 1 and 2 above provided that you also do one of the following: interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
a) Accompany it with the complete corresponding machine-readable The "System Libraries" of an executable work include anything, other
source code, which must be distributed under the terms of Sections than the work as a whole, that (a) is included in the normal form of
1 and 2 above on a medium customarily used for software interchange; or, 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.
b) Accompany it with a written offer, valid for at least three The "Corresponding Source" for a work in object code form means all
years, to give any third party, for a charge no more than your the source code needed to generate, install, and (for an executable
cost of physically performing source distribution, a complete work) run the object code and to modify the work, including scripts to
machine-readable copy of the corresponding source code, to be control those activities. However, it does not include the work's
distributed under the terms of Sections 1 and 2 above on a medium System Libraries, or general-purpose tools or generally available free
customarily used for software interchange; or, 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.
c) Accompany it with the information you received as to the offer The Corresponding Source need not include anything that users
to distribute corresponding source code. (This alternative is can regenerate automatically from other parts of the Corresponding
allowed only for noncommercial distribution and only if you Source.
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for The Corresponding Source for a work in source code form is that
making modifications to it. For an executable work, complete source same work.
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering 2. Basic Permissions.
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program All rights granted under this License are granted for the term of
except as expressly provided under this License. Any attempt copyright on the Program, and are irrevocable provided the stated
otherwise to copy, modify, sublicense or distribute the Program is conditions are met. This License explicitly affirms your unlimited
void, and will automatically terminate your rights under this License. permission to run the unmodified Program. The output from running a
However, parties who have received copies, or rights, from you under covered work is covered by this License only if the output, given its
this License will not have their licenses terminated so long as such content, constitutes a covered work. This License acknowledges your
parties remain in full compliance. rights of fair use or other equivalent, as provided by copyright law.
5. You are not required to accept this License, since you have not You may make, run and propagate covered works that you do not
signed it. However, nothing else grants you permission to modify or convey, without conditions so long as your license otherwise remains
distribute the Program or its derivative works. These actions are in force. You may convey covered works to others for the sole purpose
prohibited by law if you do not accept this License. Therefore, by of having them make modifications exclusively for you, or provide you
modifying or distributing the Program (or any work based on the with facilities for running those works, provided that you comply with
Program), you indicate your acceptance of this License to do so, and the terms of this License in conveying all material for which you do
all its terms and conditions for copying, distributing or modifying not control copyright. Those thus making or running the covered works
the Program or works based on it. 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.
6. Each time you redistribute the Program (or any work based on the Conveying under any other circumstances is permitted solely under
Program), the recipient automatically receives a license from the the conditions stated below. Sublicensing is not allowed; section 10
original licensor to copy, distribute or modify the Program subject to makes it unnecessary.
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein. 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
You are not responsible for enforcing compliance by third parties to
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. this License.
7. If, as a consequence of a court judgment or allegation of patent Each contributor grants you a non-exclusive, worldwide, royalty-free
infringement or for any other reason (not limited to patent issues), patent license under the contributor's essential patent claims, to
conditions are imposed on you (whether by court order, agreement or 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 otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot excuse you from the conditions of this License. If you cannot convey a
distribute so as to satisfy simultaneously your obligations under this covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you License and any other pertinent obligations, then as a consequence you may
may not distribute the Program at all. For example, if a patent not convey it at all. For example, if you agree to terms that obligate you
license would not permit royalty-free redistribution of the Program by to collect a royalty for further conveying from those to whom you convey
all those who receive copies directly or indirectly through you, then the Program, the only way you could satisfy both those terms and this
the only way you could satisfy both it and this License would be to License would be to refrain entirely from conveying the Program.
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under 13. Remote Network Interaction; Use with the GNU General Public License.
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any Notwithstanding any other provision of this License, if you modify the
patents or other property right claims or to contest validity of any Program, your modified version must prominently offer all users
such claims; this section has the sole purpose of protecting the interacting with it remotely through a computer network (if your version
integrity of the free software distribution system, which is supports such interaction) an opportunity to receive the Corresponding
implemented by public license practices. Many people have made Source of your version by providing access to the Corresponding Source
generous contributions to the wide range of software distributed from a network server at no charge, through some standard or customary
through that system in reliance on consistent application of that means of facilitating copying of software. This Corresponding Source
system; it is up to the author/donor to decide if he or she is willing shall include the Corresponding Source for any work covered by version 3
to distribute software through any other system and a licensee cannot of the GNU General Public License that is incorporated pursuant to the
impose that choice. following paragraph.
This section is intended to make thoroughly clear what is believed to Notwithstanding any other provision of this License, you have
be a consequence of the rest of this License. permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
8. If the distribution and/or use of the Program is restricted in 14. Revised Versions of this License.
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions The Free Software Foundation may publish revised and/or new versions of
of the General Public License from time to time. Such new versions will the GNU Affero General Public License from time to time. Such new versions
be similar in spirit to the present version, but may differ in detail to will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns. address new problems or concerns.
Each version is given a distinguishing version number. If the Program Each version is given a distinguishing version number. If the
specifies a version number of this License which applies to it and "any Program specifies that a certain numbered version of the GNU Affero General
later version", you have the option of following the terms and conditions Public License "or any later version" applies to it, you have the
either of that version or of any later version published by the Free option of following the terms and conditions either of that numbered
Software Foundation. If the Program does not specify a version number of version or of any later version published by the Free Software
this License, you may choose any version ever published by the Free Software Foundation. If the Program does not specify a version number of the
Foundation. GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free If the Program specifies that a proxy can decide which future
programs whose distribution conditions are different, write to the author versions of the GNU Affero General Public License can be used, that proxy's
to ask for permission. For software which is copyrighted by the Free public statement of acceptance of a version permanently authorizes you
Software Foundation, write to the Free Software Foundation; we sometimes to choose that version for the Program.
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY 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.
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 15. Disclaimer of 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.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
POSSIBILITY OF SUCH DAMAGES.
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 END OF TERMS AND CONDITIONS
@ -287,53 +626,36 @@ 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 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 to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least 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. 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.> <one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author> Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published
the Free Software Foundation; either version 2 of the License, or by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License along You should have received a copy of the GNU Affero General Public License
with this program; if not, write to the Free Software Foundation, Inc., along with this program. If not, see <http://www.gnu.org/licenses/>.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this If your software can interact with users remotely through a computer
when it starts in an interactive mode: network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
Gnomovision version 69, Copyright (C) year name of author You should also get your employer (if you work as a programmer) or school,
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. if any, to sign a "copyright disclaimer" for the program, if necessary.
This is free software, and you are welcome to redistribute it For more information on this, and how to apply and follow the GNU AGPL, see
under certain conditions; type `show c' for details. <http://www.gnu.org/licenses/>.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This 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.

View File

@ -1,6 +1,6 @@
# Next Terminal # Next Terminal
**注意:目前最后一个开源版是 0.5.0 版本,后续版本暂未开源。新版本 Docker 容器不再是 All in one 方式,拆分为了 guacd 和 next-terminal 两个容器。** **说明最新版前后端代码已经全部开源1.2.x 及之后版本已去除授权限制。**
![Docker image](https://github.com/dushixiang/next-terminal/workflows/Docker%20image/badge.svg?branch=master) ![Docker image](https://github.com/dushixiang/next-terminal/workflows/Docker%20image/badge.svg?branch=master)
@ -23,6 +23,7 @@ Next Terminal基于 [Apache Guacamole](https://guacamole.apache.org/) 开发,
- 资产授权 - 资产授权
- 多用户&用户分组 - 多用户&用户分组
- 计划任务 - 计划任务
- ssh server
## 在线体验 ## 在线体验
@ -36,86 +37,14 @@ test/test
## 快速安装 ## 快速安装
- [安装文档](https://next-terminal.typesafe.cn/document/) - [安装文档](https://next-terminal.typesafe.cn)
默认账号密码为 admin/admin 默认账号密码为 admin/admin
## 相关截图 ## 问题反馈
### 资源占用 请提 Issues
未使用时资源占用非常小 ## License
![资源占用截图](./screenshot/docker_stats.png) Next Terminal 使用 [AGPL-3.0](./LICENSE) 开源协议,请自觉准守。
### 控制面板
更方便的概览系统信息
![dashboard](./screenshot/dashboard.png)
### 资产管理
支持多种RDP、SSH、Telnet、VNCKubernetes等多种协议的资产
![资产](./screenshot/assets.png)
#### rdp
![rdp](./screenshot/rdp.png)
#### vnc
![vnc](./screenshot/vnc.png)
#### ssh
![ssh](./screenshot/ssh.png)
### 授权凭证
极为方便的复用资产认证信息
![凭证](./screenshot/credential.png)
批量执行命令
![批量执行命令](./screenshot/command.png)
### 在线监控
实时监控用户的操作,并可以随时断开该会话
![在线监控](./screenshot/online_session.png)
### 离线回放
详细的数据回放,定位任何一个可疑操作
![离线回放](./screenshot/offline_session.png)
### 计划任务
自定义计划任务
![计划任务](./screenshot/cron.png)
### 访问安全
黑白名单访问控制支持ip、cidr及连续IP
![访问安全](./screenshot/access.png)
### 用户组授权
灵活的授权策略
![用户组授权](./screenshot/user_group.png)
## 联系方式
- QQ群 938145268
- Telegram
https://t.me/next_terminal

7
build.sh Normal file
View File

@ -0,0 +1,7 @@
#cd web
#npm run build
#rm -rf ../bin/web/build
#mkdir -p ../bin/web/build
#cp -r build ../bin/web/
#cd ..
CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o bin/next-terminal main.go

View File

@ -8,6 +8,18 @@ mysql:
password: next-terminal password: next-terminal
database: next-terminal database: next-terminal
sqlite: sqlite:
file: 'next-terminal.db' file: './data/sqlite/next-terminal.db'
server: server:
addr: 0.0.0.0:8088 addr: 0.0.0.0:8088
guacd:
hostname: 172.16.101.32
port: 4822
# 此路径需要为绝对路径并且next-terminal和guacd都能访问到
recording: 'data/recording'
# 此路径需要为绝对路径并且next-terminal和guacd都能访问到
drive: 'data/drive'
sshd:
enable: true
addr: 0.0.0.0:8089
key: ~/.ssh/id_rsa

10
go.mod
View File

@ -1,14 +1,17 @@
module next-terminal module next-terminal
go 1.13 go 1.16
require ( require (
github.com/antonfisher/nested-logrus-formatter v1.3.0 github.com/denisbrodbeck/machineid v1.0.1
github.com/gliderlabs/ssh v0.3.3
github.com/gofrs/uuid v3.3.0+incompatible github.com/gofrs/uuid v3.3.0+incompatible
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/labstack/echo/v4 v4.1.17 github.com/labstack/echo/v4 v4.1.17
github.com/labstack/gommon v0.3.0 github.com/labstack/gommon v0.3.0
github.com/manifoldco/promptui v0.8.0
github.com/mitchellh/go-homedir v1.0.0
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.12.0 github.com/pkg/sftp v1.12.0
@ -18,7 +21,8 @@ require (
github.com/spf13/pflag v1.0.3 github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.7.1 github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
golang.org/x/text v0.3.6
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gorm.io/driver/mysql v1.0.3 gorm.io/driver/mysql v1.0.3
gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlite v1.1.4

377
go.sum
View File

@ -1,377 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/antonfisher/nested-logrus-formatter v1.3.0 h1:8zixYquU1Odk+vzAaAQPAdRh1ZjmUXNQ1T+dUBvlhVo=
github.com/antonfisher/nested-logrus-formatter v1.3.0/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.1.17 h1:PQIBaRplyRy3OjwILGkPg89JRtH2x5bssi59G2EL3fo=
github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.12.0 h1:/f3b24xrDhkhddlaobPe2JgBqfdt+gC/NYl0QY9IOuI=
github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
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/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg=
gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI=
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.7 h1:rMS4CL3pNmYq1V5/X+nHHjh1Dx6dnf27+Cai5zabo+M=
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

54
main.go
View File

@ -1,20 +1,18 @@
package main package main
import ( import (
"crypto/md5" "encoding/json"
"fmt" "fmt"
"next-terminal/pkg/config"
"next-terminal/pkg/global"
"next-terminal/pkg/task"
"next-terminal/server/api" "next-terminal/server/api"
"next-terminal/server/config"
"next-terminal/server/constant"
"next-terminal/server/repository" "next-terminal/server/repository"
"next-terminal/server/task"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
) )
const Version = "v0.5.0"
func main() { func main() {
err := Run() err := Run()
if err != nil { if err != nil {
@ -24,47 +22,41 @@ func main() {
func Run() error { func Run() error {
fmt.Printf(` fmt.Printf(constant.Banner, constant.Version)
_______ __ ___________ .__ .__
\ \ ____ ___ ____/ |_ \__ ___/__________ _____ |__| ____ _____ | |
/ | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | |
/ | \ ___/ > < | | | |\ ___/| | \/ Y Y \ | | \/ __ \| |__
\____|__ /\___ >__/\_ \ |__| |____| \___ >__| |__|_| /__|___| (____ /____/
\/ \/ \/ \/ \/ \/ \/ ` + Version + "\n\n")
// 为了兼容之前调用global包的代码 后期预期会改为调用pgk/config if config.GlobalCfg.Debug {
global.Config = config.GlobalCfg jsonBytes, err := json.MarshalIndent(config.GlobalCfg, "", " ")
if err != nil {
if global.Config.EncryptionKey == "" { return err
global.Config.EncryptionKey = "next-terminal" }
fmt.Printf("当前配置为: %v\n", string(jsonBytes))
} }
md5Sum := fmt.Sprintf("%x", md5.Sum([]byte(global.Config.EncryptionKey)))
global.Config.EncryptionPassword = []byte(md5Sum)
global.Cache = api.SetupCache()
db := api.SetupDB() db := api.SetupDB()
e := api.SetupRoutes(db) e := api.SetupRoutes(db)
if global.Config.ResetPassword != "" { if config.GlobalCfg.ResetPassword != "" {
return api.ResetPassword(global.Config.ResetPassword) return api.ResetPassword(config.GlobalCfg.ResetPassword)
} }
if global.Config.ResetTotp != "" { if config.GlobalCfg.ResetTotp != "" {
return api.ResetTotp(global.Config.ResetTotp) return api.ResetTotp(config.GlobalCfg.ResetTotp)
} }
if global.Config.NewEncryptionKey != "" { if config.GlobalCfg.NewEncryptionKey != "" {
return api.ChangeEncryptionKey(global.Config.EncryptionKey, global.Config.NewEncryptionKey) return api.ChangeEncryptionKey(config.GlobalCfg.EncryptionKey, config.GlobalCfg.NewEncryptionKey)
} }
sessionRepo := repository.NewSessionRepository(db) sessionRepo := repository.NewSessionRepository(db)
propertyRepo := repository.NewPropertyRepository(db) propertyRepo := repository.NewPropertyRepository(db)
ticker := task.NewTicker(sessionRepo, propertyRepo) loginLogRepo := repository.NewLoginLogRepository(db)
jobLogRepo := repository.NewJobLogRepository(db)
ticker := task.NewTicker(sessionRepo, propertyRepo, loginLogRepo, jobLogRepo)
ticker.SetupTicker() ticker.SetupTicker()
if global.Config.Server.Cert != "" && global.Config.Server.Key != "" { if config.GlobalCfg.Server.Cert != "" && config.GlobalCfg.Server.Key != "" {
return e.StartTLS(global.Config.Server.Addr, global.Config.Server.Cert, global.Config.Server.Key) return e.StartTLS(config.GlobalCfg.Server.Addr, config.GlobalCfg.Server.Cert, config.GlobalCfg.Server.Key)
} else { } else {
return e.Start(global.Config.Server.Addr) return e.Start(config.GlobalCfg.Server.Addr)
} }
} }

View File

@ -1,104 +0,0 @@
package config
import (
"strings"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
var GlobalCfg *Config
type Config struct {
Debug bool
Demo bool
DB string
Server *Server
Mysql *Mysql
Sqlite *Sqlite
ResetPassword string
ResetTotp string
EncryptionKey string
EncryptionPassword []byte
NewEncryptionKey string
}
type Mysql struct {
Hostname string
Port int
Username string
Password string
Database string
}
type Sqlite struct {
File string
}
type Server struct {
Addr string
Cert string
Key string
}
func SetupConfig() *Config {
viper.SetConfigName("config")
viper.SetConfigType("yml")
viper.AddConfigPath("/etc/next-terminal/")
viper.AddConfigPath("$HOME/.next-terminal")
viper.AddConfigPath(".")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
pflag.String("db", "sqlite", "db mode")
pflag.String("sqlite.file", "next-terminal.db", "sqlite db file")
pflag.String("mysql.hostname", "127.0.0.1", "mysql hostname")
pflag.Int("mysql.port", 3306, "mysql port")
pflag.String("mysql.username", "mysql", "mysql username")
pflag.String("mysql.password", "mysql", "mysql password")
pflag.String("mysql.database", "next_terminal", "mysql database")
pflag.String("server.addr", "", "server listen addr")
pflag.String("server.cert", "", "tls cert file")
pflag.String("server.key", "", "tls key file")
pflag.String("reset-password", "", "")
pflag.String("encryption-key", "", "")
pflag.String("new-encryption-key", "", "")
pflag.Parse()
_ = viper.BindPFlags(pflag.CommandLine)
_ = viper.ReadInConfig()
var config = &Config{
DB: viper.GetString("db"),
Mysql: &Mysql{
Hostname: viper.GetString("mysql.hostname"),
Port: viper.GetInt("mysql.port"),
Username: viper.GetString("mysql.username"),
Password: viper.GetString("mysql.password"),
Database: viper.GetString("mysql.database"),
},
Sqlite: &Sqlite{
File: viper.GetString("sqlite.file"),
},
Server: &Server{
Addr: viper.GetString("server.addr"),
Cert: viper.GetString("server.cert"),
Key: viper.GetString("server.key"),
},
ResetPassword: viper.GetString("reset-password"),
ResetTotp: viper.GetString("reset-totp"),
Debug: viper.GetBool("debug"),
Demo: viper.GetBool("demo"),
EncryptionKey: viper.GetString("encryption-key"),
NewEncryptionKey: viper.GetString("new-encryption-key"),
}
GlobalCfg = config
return config
}
func init() {
GlobalCfg = SetupConfig()
}

View File

@ -1,28 +0,0 @@
package global
import (
"next-terminal/pkg/config"
"github.com/patrickmn/go-cache"
"github.com/robfig/cron/v3"
)
var Cache *cache.Cache
var Config *config.Config
var Store *TunStore
var Cron *cron.Cron
type Security struct {
Rule string
IP string
}
var Securities []*Security
func init() {
Cron = cron.New(cron.WithSeconds())
Cron.Start()
}

View File

@ -1,75 +0,0 @@
package global
import (
"strconv"
"sync"
"next-terminal/pkg/guacd"
"next-terminal/pkg/term"
"github.com/gorilla/websocket"
)
type Tun struct {
Protocol string
Mode string
WebSocket *websocket.Conn
Tunnel *guacd.Tunnel
NextTerminal *term.NextTerminal
}
func (r *Tun) Close(code int, reason string) {
if r.Tunnel != nil {
_ = r.Tunnel.Close()
}
if r.NextTerminal != nil {
_ = r.NextTerminal.Close()
}
ws := r.WebSocket
if ws != nil {
if r.Mode == "guacd" {
err := guacd.NewInstruction("error", "", strconv.Itoa(code))
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String()))
disconnect := guacd.NewInstruction("disconnect")
_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
} else {
msg := `{"type":"closed","content":"` + reason + `"}`
_ = ws.WriteMessage(websocket.TextMessage, []byte(msg))
}
}
}
type Observable struct {
Subject *Tun
Observers []Tun
}
type TunStore struct {
m sync.Map
}
func (s *TunStore) Set(k string, v *Observable) {
s.m.Store(k, v)
}
func (s *TunStore) Del(k string) {
s.m.Delete(k)
}
func (s *TunStore) Get(k string) (item *Observable, ok bool) {
value, ok := s.m.Load(k)
if ok {
return value.(*Observable), true
}
return item, false
}
func NewStore() *TunStore {
store := TunStore{sync.Map{}}
return &store
}
func init() {
Store = NewStore()
}

View File

@ -1,34 +0,0 @@
package service
import (
"next-terminal/pkg/global"
"next-terminal/server/repository"
)
type AssetService struct {
assetRepository *repository.AssetRepository
}
func NewAssetService(assetRepository *repository.AssetRepository) *AssetService {
return &AssetService{assetRepository: assetRepository}
}
func (r AssetService) Encrypt() error {
items, err := r.assetRepository.FindAll()
if err != nil {
return err
}
for i := range items {
item := items[i]
if item.Encrypted {
continue
}
if err := r.assetRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil {
return err
}
if err := r.assetRepository.UpdateById(&item, item.ID); err != nil {
return err
}
}
return nil
}

View File

@ -1,31 +0,0 @@
package service
import (
"strconv"
"next-terminal/server/model"
"next-terminal/server/repository"
)
type NumService struct {
numRepository *repository.NumRepository
}
func NewNumService(numRepository *repository.NumRepository) *NumService {
return &NumService{numRepository: numRepository}
}
func (r NumService) InitNums() error {
nums, err := r.numRepository.FindAll()
if err != nil {
return err
}
if len(nums) == 0 {
for i := 0; i <= 30; i++ {
if err := r.numRepository.Create(&model.Num{I: strconv.Itoa(i)}); err != nil {
return err
}
}
}
return nil
}

View File

@ -1,73 +0,0 @@
package task
import (
"strconv"
"time"
"next-terminal/pkg/constant"
"next-terminal/pkg/log"
"next-terminal/server/repository"
)
type Ticker struct {
sessionRepository *repository.SessionRepository
propertyRepository *repository.PropertyRepository
}
func NewTicker(sessionRepository *repository.SessionRepository, propertyRepository *repository.PropertyRepository) *Ticker {
return &Ticker{sessionRepository: sessionRepository, propertyRepository: propertyRepository}
}
func (t *Ticker) SetupTicker() {
// 每隔一小时删除一次未使用的会话信息
unUsedSessionTicker := time.NewTicker(time.Minute * 60)
go func() {
for range unUsedSessionTicker.C {
sessions, _ := t.sessionRepository.FindByStatusIn([]string{constant.NoConnect, constant.Connecting})
if len(sessions) > 0 {
now := time.Now()
for i := range sessions {
if now.Sub(sessions[i].ConnectedTime.Time) > time.Hour*1 {
_ = t.sessionRepository.DeleteById(sessions[i].ID)
s := sessions[i].Username + "@" + sessions[i].IP + ":" + strconv.Itoa(sessions[i].Port)
log.Infof("会话「%v」ID「%v」超过1小时未打开已删除。", s, sessions[i].ID)
}
}
}
}
}()
// 每日凌晨删除超过时长限制的会话
timeoutSessionTicker := time.NewTicker(time.Hour * 24)
go func() {
for range timeoutSessionTicker.C {
property, err := t.propertyRepository.FindByName("session-saved-limit")
if err != nil {
return
}
if property.Value == "" || property.Value == "-" {
return
}
limit, err := strconv.Atoi(property.Value)
if err != nil {
return
}
sessions, err := t.sessionRepository.FindOutTimeSessions(limit)
if err != nil {
return
}
if len(sessions) > 0 {
var sessionIds []string
for i := range sessions {
sessionIds = append(sessionIds, sessions[i].ID)
}
err := t.sessionRepository.DeleteByIds(sessionIds)
if err != nil {
log.Errorf("删除离线会话失败 %v", err)
}
}
}
}()
}

View File

@ -1,30 +0,0 @@
package term
import (
"bytes"
"sync"
)
type NextWriter struct {
b bytes.Buffer
mu sync.Mutex
}
func (w *NextWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.b.Write(p)
}
func (w *NextWriter) Read() ([]byte, int, error) {
w.mu.Lock()
defer w.mu.Unlock()
p := w.b.Bytes()
buf := make([]byte, len(p))
read, err := w.b.Read(buf)
w.b.Reset()
if err != nil {
return nil, 0, err
}
return buf, read, err
}

View File

@ -1 +0,0 @@
资产名称,ssh,127.0.0.1,22,username,password,privateKey,passphrase,description
1 资产名称 ssh 127.0.0.1 22 username password privateKey passphrase description

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 552 KiB

View File

@ -0,0 +1,119 @@
package api
import (
"strconv"
"strings"
"next-terminal/server/model"
"next-terminal/server/utils"
"github.com/labstack/echo/v4"
)
func AccessGatewayCreateEndpoint(c echo.Context) error {
var item model.AccessGateway
if err := c.Bind(&item); err != nil {
return err
}
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := accessGatewayRepository.Create(&item); err != nil {
return err
}
// 连接网关
accessGatewayService.ReConnect(&item)
return Success(c, "")
}
func AccessGatewayAllEndpoint(c echo.Context) error {
gateways, err := accessGatewayRepository.FindAll()
if err != nil {
return err
}
var simpleGateways = make([]model.AccessGatewayForPage, 0)
for i := 0; i < len(gateways); i++ {
simpleGateways = append(simpleGateways, model.AccessGatewayForPage{ID: gateways[i].ID, Name: gateways[i].Name})
}
return Success(c, simpleGateways)
}
func AccessGatewayPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
ip := c.QueryParam("ip")
name := c.QueryParam("name")
order := c.QueryParam("order")
field := c.QueryParam("field")
items, total, err := accessGatewayRepository.Find(pageIndex, pageSize, ip, name, order, field)
if err != nil {
return err
}
for i := 0; i < len(items); i++ {
g, err := accessGatewayService.GetGatewayById(items[i].ID)
if err != nil {
return err
}
items[i].Connected = g.Connected
items[i].Message = g.Message
}
return Success(c, H{
"total": total,
"items": items,
})
}
func AccessGatewayUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.AccessGateway
if err := c.Bind(&item); err != nil {
return err
}
if err := accessGatewayRepository.UpdateById(&item, id); err != nil {
return err
}
accessGatewayService.DisconnectById(id)
_, _ = accessGatewayService.GetGatewayAndReconnectById(id)
return Success(c, nil)
}
func AccessGatewayDeleteEndpoint(c echo.Context) error {
ids := c.Param("id")
split := strings.Split(ids, ",")
for i := range split {
id := split[i]
if err := accessGatewayRepository.DeleteById(id); err != nil {
return err
}
accessGatewayService.DisconnectById(id)
}
return Success(c, nil)
}
func AccessGatewayGetEndpoint(c echo.Context) error {
id := c.Param("id")
item, err := accessGatewayRepository.FindById(id)
if err != nil {
return err
}
return Success(c, item)
}
func AccessGatewayReconnectEndpoint(c echo.Context) error {
id := c.Param("id")
item, err := accessGatewayRepository.FindById(id)
if err != nil {
return err
}
accessGatewayService.ReConnect(&item)
return Success(c, "")
}

View File

@ -1,12 +1,15 @@
package api package api
import ( import (
"path"
"strconv"
"strings" "strings"
"time" "time"
"next-terminal/pkg/global" "next-terminal/server/config"
"next-terminal/pkg/totp" "next-terminal/server/global/cache"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/totp"
"next-terminal/server/utils" "next-terminal/server/utils"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -40,11 +43,6 @@ type Authorization struct {
User model.User User model.User
} }
//
//type UserServer struct {
// repository.UserRepository
//}
func LoginEndpoint(c echo.Context) error { func LoginEndpoint(c echo.Context) error {
var loginAccount LoginAccount var loginAccount LoginAccount
if err := c.Bind(&loginAccount); err != nil { if err := c.Bind(&loginAccount); err != nil {
@ -54,25 +52,33 @@ func LoginEndpoint(c echo.Context) error {
user, err := userRepository.FindByUsername(loginAccount.Username) user, err := userRepository.FindByUsername(loginAccount.Username)
// 存储登录失败次数信息 // 存储登录失败次数信息
loginFailCountKey := loginAccount.Username loginFailCountKey := c.RealIP() + loginAccount.Username
v, ok := global.Cache.Get(loginFailCountKey) v, ok := cache.GlobalCache.Get(loginFailCountKey)
if !ok { if !ok {
v = 1 v = 1
} }
count := v.(int) count := v.(int)
if count >= 5 { if count >= 5 {
return Fail(c, -1, "登录失败次数过多,请后再试") return Fail(c, -1, "登录失败次数过多,请等待5分钟后再试")
} }
if err != nil { if err != nil {
count++ count++
global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
// 保存登录日志
if err := SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
return err
}
return FailWithData(c, -1, "您输入的账号或密码不正确", count) return FailWithData(c, -1, "您输入的账号或密码不正确", count)
} }
if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil { if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil {
count++ count++
global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
// 保存登录日志
if err := SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
return err
}
return FailWithData(c, -1, "您输入的账号或密码不正确", count) return FailWithData(c, -1, "您输入的账号或密码不正确", count)
} }
@ -80,15 +86,42 @@ func LoginEndpoint(c echo.Context) error {
return Fail(c, 0, "") return Fail(c, 0, "")
} }
token, err := LoginSuccess(c, loginAccount, user) token, err := LoginSuccess(loginAccount, user)
if err != nil { if err != nil {
return err return err
} }
// 保存登录日志
if err := SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, true, loginAccount.Remember, token, ""); err != nil {
return err
}
return Success(c, token) return Success(c, token)
} }
func LoginSuccess(c echo.Context, loginAccount LoginAccount, user model.User) (token string, err error) { func SaveLoginLog(clientIP, clientUserAgent string, username string, success, remember bool, id, reason string) error {
loginLog := model.LoginLog{
Username: username,
ClientIP: clientIP,
ClientUserAgent: clientUserAgent,
LoginTime: utils.NowJsonTime(),
Reason: reason,
Remember: remember,
}
if success {
loginLog.State = "1"
loginLog.ID = id
} else {
loginLog.State = "0"
loginLog.ID = utils.UUID()
}
if err := loginLogRepository.Create(&loginLog); err != nil {
return err
}
return nil
}
func LoginSuccess(loginAccount LoginAccount, user model.User) (token string, err error) {
token = strings.Join([]string{utils.UUID(), utils.UUID(), utils.UUID(), utils.UUID()}, "") token = strings.Join([]string{utils.UUID(), utils.UUID(), utils.UUID(), utils.UUID()}, "")
authorization := Authorization{ authorization := Authorization{
@ -97,45 +130,20 @@ func LoginSuccess(c echo.Context, loginAccount LoginAccount, user model.User) (t
User: user, User: user,
} }
cacheKey := BuildCacheKeyByToken(token) cacheKey := userService.BuildCacheKeyByToken(token)
if authorization.Remember { if authorization.Remember {
// 记住登录有效期两周 // 记住登录有效期两周
global.Cache.Set(cacheKey, authorization, RememberEffectiveTime) cache.GlobalCache.Set(cacheKey, authorization, RememberEffectiveTime)
} else { } else {
global.Cache.Set(cacheKey, authorization, NotRememberEffectiveTime) cache.GlobalCache.Set(cacheKey, authorization, NotRememberEffectiveTime)
}
// 保存登录日志
loginLog := model.LoginLog{
ID: token,
UserId: user.ID,
ClientIP: c.RealIP(),
ClientUserAgent: c.Request().UserAgent(),
LoginTime: utils.NowJsonTime(),
Remember: authorization.Remember,
}
if loginLogRepository.Create(&loginLog) != nil {
return "", err
} }
// 修改登录状态 // 修改登录状态
err = userRepository.Update(&model.User{Online: true, ID: user.ID}) err = userRepository.Update(&model.User{Online: true, ID: user.ID})
return token, err return token, err
} }
func BuildCacheKeyByToken(token string) string {
cacheKey := strings.Join([]string{Token, token}, ":")
return cacheKey
}
func GetTokenFormCacheKey(cacheKey string) string {
token := strings.Split(cacheKey, ":")[1]
return token
}
func loginWithTotpEndpoint(c echo.Context) error { func loginWithTotpEndpoint(c echo.Context) error {
var loginAccount LoginAccount var loginAccount LoginAccount
if err := c.Bind(&loginAccount); err != nil { if err := c.Bind(&loginAccount); err != nil {
@ -143,47 +151,63 @@ func loginWithTotpEndpoint(c echo.Context) error {
} }
// 存储登录失败次数信息 // 存储登录失败次数信息
loginFailCountKey := loginAccount.Username loginFailCountKey := c.RealIP() + loginAccount.Username
v, ok := global.Cache.Get(loginFailCountKey) v, ok := cache.GlobalCache.Get(loginFailCountKey)
if !ok { if !ok {
v = 1 v = 1
} }
count := v.(int) count := v.(int)
if count >= 5 { if count >= 5 {
return Fail(c, -1, "登录失败次数过多,请后再试") return Fail(c, -1, "登录失败次数过多,请等待5分钟后再试")
} }
user, err := userRepository.FindByUsername(loginAccount.Username) user, err := userRepository.FindByUsername(loginAccount.Username)
if err != nil { if err != nil {
count++ count++
global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
// 保存登录日志
if err := SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
return err
}
return FailWithData(c, -1, "您输入的账号或密码不正确", count) return FailWithData(c, -1, "您输入的账号或密码不正确", count)
} }
if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil { if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil {
count++ count++
global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
// 保存登录日志
if err := SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "账号或密码不正确"); err != nil {
return err
}
return FailWithData(c, -1, "您输入的账号或密码不正确", count) return FailWithData(c, -1, "您输入的账号或密码不正确", count)
} }
if !totp.Validate(loginAccount.TOTP, user.TOTPSecret) { if !totp.Validate(loginAccount.TOTP, user.TOTPSecret) {
count++ count++
global.Cache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5))
// 保存登录日志
if err := SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, false, loginAccount.Remember, "", "双因素认证授权码不正确"); err != nil {
return err
}
return FailWithData(c, -1, "您输入双因素认证授权码不正确", count) return FailWithData(c, -1, "您输入双因素认证授权码不正确", count)
} }
token, err := LoginSuccess(c, loginAccount, user) token, err := LoginSuccess(loginAccount, user)
if err != nil { if err != nil {
return err return err
} }
// 保存登录日志
if err := SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, true, loginAccount.Remember, token, ""); err != nil {
return err
}
return Success(c, token) return Success(c, token)
} }
func LogoutEndpoint(c echo.Context) error { func LogoutEndpoint(c echo.Context) error {
token := GetToken(c) token := GetToken(c)
cacheKey := BuildCacheKeyByToken(token) cacheKey := userService.BuildCacheKeyByToken(token)
global.Cache.Delete(cacheKey) cache.GlobalCache.Delete(cacheKey)
err := userService.Logout(token) err := userService.Logout(token)
if err != nil { if err != nil {
return err return err
@ -192,7 +216,7 @@ func LogoutEndpoint(c echo.Context) error {
} }
func ConfirmTOTPEndpoint(c echo.Context) error { func ConfirmTOTPEndpoint(c echo.Context) error {
if global.Config.Demo { if config.GlobalCfg.Demo {
return Fail(c, 0, "演示模式禁止开启两步验证") return Fail(c, 0, "演示模式禁止开启两步验证")
} }
account, _ := GetCurrentAccount(c) account, _ := GetCurrentAccount(c)
@ -258,7 +282,7 @@ func ResetTOTPEndpoint(c echo.Context) error {
} }
func ChangePasswordEndpoint(c echo.Context) error { func ChangePasswordEndpoint(c echo.Context) error {
if global.Config.Demo { if config.GlobalCfg.Demo {
return Fail(c, 0, "演示模式禁止修改密码") return Fail(c, 0, "演示模式禁止修改密码")
} }
account, _ := GetCurrentAccount(c) account, _ := GetCurrentAccount(c)
@ -313,3 +337,48 @@ func InfoEndpoint(c echo.Context) error {
} }
return Success(c, info) return Success(c, info)
} }
func AccountAssetEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
protocol := c.QueryParam("protocol")
tags := c.QueryParam("tags")
owner := c.QueryParam("owner")
sharer := c.QueryParam("sharer")
userGroupId := c.QueryParam("userGroupId")
ip := c.QueryParam("ip")
order := c.QueryParam("order")
field := c.QueryParam("field")
account, _ := GetCurrentAccount(c)
items, total, err := assetRepository.Find(pageIndex, pageSize, name, protocol, tags, account, owner, sharer, userGroupId, ip, order, field)
if err != nil {
return err
}
return Success(c, H{
"total": total,
"items": items,
})
}
func AccountStorageEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c)
storageId := account.ID
storage, err := storageRepository.FindById(storageId)
if err != nil {
return err
}
structMap := utils.StructToMap(storage)
drivePath := storageService.GetBaseDrivePath()
dirSize, err := utils.DirSize(path.Join(drivePath, storageId))
if err != nil {
structMap["usedSize"] = -1
} else {
structMap["usedSize"] = dirSize
}
return Success(c, structMap)
}

View File

@ -1,8 +1,8 @@
package api package api
import ( import (
"next-terminal/pkg/constant" "next-terminal/server/constant"
"next-terminal/pkg/global" "next-terminal/server/global/cache"
"next-terminal/server/model" "next-terminal/server/model"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -41,17 +41,17 @@ func NotFound(c echo.Context, message string) error {
} }
func GetToken(c echo.Context) string { func GetToken(c echo.Context) string {
token := c.Request().Header.Get(Token) token := c.Request().Header.Get(constant.Token)
if len(token) > 0 { if len(token) > 0 {
return token return token
} }
return c.QueryParam(Token) return c.QueryParam(constant.Token)
} }
func GetCurrentAccount(c echo.Context) (model.User, bool) { func GetCurrentAccount(c echo.Context) (model.User, bool) {
token := GetToken(c) token := GetToken(c)
cacheKey := BuildCacheKeyByToken(token) cacheKey := userService.BuildCacheKeyByToken(token)
get, b := global.Cache.Get(cacheKey) get, b := cache.GlobalCache.Get(cacheKey)
if b { if b {
return get.(Authorization).User, true return get.(Authorization).User, true
} }

View File

@ -8,8 +8,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"next-terminal/pkg/constant" "next-terminal/server/config"
"next-terminal/pkg/global" "next-terminal/server/constant"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/utils" "next-terminal/server/utils"
@ -32,6 +32,7 @@ func AssetCreateEndpoint(c echo.Context) error {
item.Owner = account.ID item.Owner = account.ID
item.ID = utils.UUID() item.ID = utils.UUID()
item.Created = utils.NowJsonTime() item.Created = utils.NowJsonTime()
item.Active = true
if err := assetRepository.Create(&item); err != nil { if err := assetRepository.Create(&item); err != nil {
return err return err
@ -41,10 +42,12 @@ func AssetCreateEndpoint(c echo.Context) error {
return err return err
} }
// 创建后自动检测资产是否存活
go func() { go func() {
active := utils.Tcping(item.IP, item.Port) active, _ := assetService.CheckStatus(item.AccessGatewayId, item.IP, item.Port)
_ = assetRepository.UpdateActiveById(active, item.ID)
if item.Active != active {
_ = assetRepository.UpdateActiveById(active, item.ID)
}
}() }()
return Success(c, item) return Success(c, item)
@ -74,7 +77,6 @@ func AssetImportEndpoint(c echo.Context) error {
if total == 0 { if total == 0 {
return errors.New("csv数据为空") return errors.New("csv数据为空")
} }
var successCount = 0 var successCount = 0
var errorCount = 0 var errorCount = 0
m := echo.Map{} m := echo.Map{}
@ -97,6 +99,7 @@ func AssetImportEndpoint(c echo.Context) error {
Description: record[8], Description: record[8],
Created: utils.NowJsonTime(), Created: utils.NowJsonTime(),
Owner: account.ID, Owner: account.ID,
Active: true,
} }
if len(record) >= 10 { if len(record) >= 10 {
@ -110,11 +113,6 @@ func AssetImportEndpoint(c echo.Context) error {
m[strconv.Itoa(i)] = err.Error() m[strconv.Itoa(i)] = err.Error()
} else { } else {
successCount++ successCount++
// 创建后自动检测资产是否存活
go func() {
active := utils.Tcping(asset.IP, asset.Port)
_ = assetRepository.UpdateActiveById(active, asset.ID)
}()
} }
} }
} }
@ -141,6 +139,7 @@ func AssetPagingEndpoint(c echo.Context) error {
field := c.QueryParam("field") field := c.QueryParam("field")
account, _ := GetCurrentAccount(c) account, _ := GetCurrentAccount(c)
items, total, err := assetRepository.Find(pageIndex, pageSize, name, protocol, tags, account, owner, sharer, userGroupId, ip, order, field) items, total, err := assetRepository.Find(pageIndex, pageSize, name, protocol, tags, account, owner, sharer, userGroupId, ip, order, field)
if err != nil { if err != nil {
return err return err
@ -154,8 +153,7 @@ func AssetPagingEndpoint(c echo.Context) error {
func AssetAllEndpoint(c echo.Context) error { func AssetAllEndpoint(c echo.Context) error {
protocol := c.QueryParam("protocol") protocol := c.QueryParam("protocol")
account, _ := GetCurrentAccount(c) items, _ := assetRepository.FindByProtocol(protocol)
items, _ := assetRepository.FindByProtocolAndUser(protocol, account)
return Success(c, items) return Success(c, items)
} }
@ -205,7 +203,7 @@ func AssetUpdateEndpoint(c echo.Context) error {
item.Description = "-" item.Description = "-"
} }
if err := assetRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil { if err := assetRepository.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil {
return err return err
} }
if err := assetRepository.UpdateById(&item, id); err != nil { if err := assetRepository.UpdateById(&item, id); err != nil {
@ -267,7 +265,7 @@ func AssetTcpingEndpoint(c echo.Context) (err error) {
return err return err
} }
active := utils.Tcping(item.IP, item.Port) active, err := assetService.CheckStatus(item.AccessGatewayId, item.IP, item.Port)
if item.Active != active { if item.Active != active {
if err := assetRepository.UpdateActiveById(active, item.ID); err != nil { if err := assetRepository.UpdateActiveById(active, item.ID); err != nil {
@ -275,7 +273,15 @@ func AssetTcpingEndpoint(c echo.Context) (err error) {
} }
} }
return Success(c, active) var message = ""
if err != nil {
message = err.Error()
}
return Success(c, H{
"active": active,
"message": message,
})
} }
func AssetTagsEndpoint(c echo.Context) (err error) { func AssetTagsEndpoint(c echo.Context) (err error) {

View File

@ -29,6 +29,12 @@ func CommandCreateEndpoint(c echo.Context) error {
return Success(c, item) return Success(c, item)
} }
func CommandAllEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c)
items, _ := commandRepository.FindByUser(account)
return Success(c, items)
}
func CommandPagingEndpoint(c echo.Context) error { func CommandPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex")) pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize")) pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))

View File

@ -6,8 +6,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"next-terminal/pkg/constant" "next-terminal/server/config"
"next-terminal/pkg/global" "next-terminal/server/constant"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/utils" "next-terminal/server/utils"
@ -106,7 +106,7 @@ func CredentialUpdateEndpoint(c echo.Context) error {
item.Password = "-" item.Password = "-"
} }
if item.Password != "-" { if item.Password != "-" {
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), global.Config.EncryptionPassword) encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), config.GlobalCfg.EncryptionPassword)
if err != nil { if err != nil {
return err return err
} }
@ -121,7 +121,7 @@ func CredentialUpdateEndpoint(c echo.Context) error {
item.PrivateKey = "-" item.PrivateKey = "-"
} }
if item.PrivateKey != "-" { if item.PrivateKey != "-" {
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), global.Config.EncryptionPassword) encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), config.GlobalCfg.EncryptionPassword)
if err != nil { if err != nil {
return err return err
} }
@ -131,7 +131,7 @@ func CredentialUpdateEndpoint(c echo.Context) error {
item.Passphrase = "-" item.Passphrase = "-"
} }
if item.Passphrase != "-" { if item.Passphrase != "-" {
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), global.Config.EncryptionPassword) encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), config.GlobalCfg.EncryptionPassword)
if err != nil { if err != nil {
return err return err
} }

View File

@ -53,7 +53,7 @@ func JobUpdateEndpoint(c echo.Context) error {
return err return err
} }
item.ID = id item.ID = id
if err := jobRepository.UpdateById(&item); err != nil { if err := jobService.UpdateById(&item); err != nil {
return err return err
} }
@ -83,7 +83,7 @@ func JobDeleteEndpoint(c echo.Context) error {
split := strings.Split(ids, ",") split := strings.Split(ids, ",")
for i := range split { for i := range split {
jobId := split[i] jobId := split[i]
if err := jobRepository.DeleteJobById(jobId); err != nil { if err := jobService.DeleteJobById(jobId); err != nil {
return err return err
} }
} }

View File

@ -4,8 +4,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"next-terminal/pkg/global" "next-terminal/server/global/cache"
"next-terminal/pkg/log" "next-terminal/server/log"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@ -13,10 +13,11 @@ import (
func LoginLogPagingEndpoint(c echo.Context) error { func LoginLogPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex")) pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize")) pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
userId := c.QueryParam("userId") username := c.QueryParam("username")
clientIp := c.QueryParam("clientIp") clientIp := c.QueryParam("clientIp")
state := c.QueryParam("state")
items, total, err := loginLogRepository.Find(pageIndex, pageSize, userId, clientIp) items, total, err := loginLogRepository.Find(pageIndex, pageSize, username, clientIp, state)
if err != nil { if err != nil {
return err return err
@ -33,7 +34,7 @@ func LoginLogDeleteEndpoint(c echo.Context) error {
split := strings.Split(ids, ",") split := strings.Split(ids, ",")
for i := range split { for i := range split {
token := split[i] token := split[i]
global.Cache.Delete(token) cache.GlobalCache.Delete(token)
if err := userService.Logout(token); err != nil { if err := userService.Logout(token); err != nil {
log.WithError(err).Error("Cache Delete Failed") log.WithError(err).Error("Cache Delete Failed")
} }
@ -44,3 +45,10 @@ func LoginLogDeleteEndpoint(c echo.Context) error {
return Success(c, nil) return Success(c, nil)
} }
//func LoginLogClearEndpoint(c echo.Context) error {
// loginLogs, err := loginLogRepository.FindAliveLoginLogs()
// if err != nil {
// return err
// }
//}

View File

@ -3,12 +3,12 @@ package api
import ( import (
"fmt" "fmt"
"net" "net"
"regexp"
"strings" "strings"
"time" "time"
"next-terminal/pkg/constant" "next-terminal/server/constant"
"next-terminal/pkg/global" "next-terminal/server/global/cache"
"next-terminal/server/global/security"
"next-terminal/server/utils" "next-terminal/server/utils"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -33,27 +33,26 @@ func ErrorHandler(next echo.HandlerFunc) echo.HandlerFunc {
func TcpWall(next echo.HandlerFunc) echo.HandlerFunc { func TcpWall(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
securities := security.GlobalSecurityManager.Values()
if global.Securities == nil { if len(securities) == 0 {
return next(c) return next(c)
} }
ip := c.RealIP() ip := c.RealIP()
for i := 0; i < len(global.Securities); i++ {
security := global.Securities[i]
if strings.Contains(security.IP, "/") { for _, s := range securities {
if strings.Contains(s.IP, "/") {
// CIDR // CIDR
_, ipNet, err := net.ParseCIDR(security.IP) _, ipNet, err := net.ParseCIDR(s.IP)
if err != nil { if err != nil {
continue continue
} }
if !ipNet.Contains(net.ParseIP(ip)) { if !ipNet.Contains(net.ParseIP(ip)) {
continue continue
} }
} else if strings.Contains(security.IP, "-") { } else if strings.Contains(s.IP, "-") {
// 范围段 // 范围段
split := strings.Split(security.IP, "-") split := strings.Split(s.IP, "-")
if len(split) < 2 { if len(split) < 2 {
continue continue
} }
@ -65,16 +64,16 @@ func TcpWall(next echo.HandlerFunc) echo.HandlerFunc {
} }
} else { } else {
// IP // IP
if security.IP != ip { if s.IP != ip {
continue continue
} }
} }
if security.Rule == constant.AccessRuleAllow { if s.Rule == constant.AccessRuleAllow {
return next(c) return next(c)
} }
if security.Rule == constant.AccessRuleReject { if s.Rule == constant.AccessRuleReject {
if c.Request().Header.Get("X-Requested-With") != "" || c.Request().Header.Get(Token) != "" { if c.Request().Header.Get("X-Requested-With") != "" || c.Request().Header.Get(constant.Token) != "" {
return Fail(c, 0, "您的访问请求被拒绝 :(") return Fail(c, 0, "您的访问请求被拒绝 :(")
} else { } else {
return c.HTML(666, "您的访问请求被拒绝 :(") return c.HTML(666, "您的访问请求被拒绝 :(")
@ -88,10 +87,7 @@ func TcpWall(next echo.HandlerFunc) echo.HandlerFunc {
func Auth(next echo.HandlerFunc) echo.HandlerFunc { func Auth(next echo.HandlerFunc) echo.HandlerFunc {
startWithUrls := []string{"/login", "/static", "/favicon.ico", "/logo.svg", "/asciinema"} anonymousUrls := []string{"/login", "/static", "/favicon.ico", "/logo.svg", "/asciinema"}
download := regexp.MustCompile(`^/sessions/\w{8}(-\w{4}){3}-\w{12}/download`)
recording := regexp.MustCompile(`^/sessions/\w{8}(-\w{4}){3}-\w{12}/recording`)
return func(c echo.Context) error { return func(c echo.Context) error {
@ -100,32 +96,27 @@ func Auth(next echo.HandlerFunc) echo.HandlerFunc {
return next(c) return next(c)
} }
// 路由拦截 - 登录身份、资源权限判断等 // 路由拦截 - 登录身份、资源权限判断等
for i := range startWithUrls { for i := range anonymousUrls {
if strings.HasPrefix(uri, startWithUrls[i]) { if strings.HasPrefix(uri, anonymousUrls[i]) {
return next(c) return next(c)
} }
} }
if download.FindString(uri) != "" {
return next(c)
}
if recording.FindString(uri) != "" {
return next(c)
}
token := GetToken(c) token := GetToken(c)
cacheKey := BuildCacheKeyByToken(token) if token == "" {
authorization, found := global.Cache.Get(cacheKey) return Fail(c, 401, "您的登录信息已失效,请重新登录后再试。")
}
cacheKey := userService.BuildCacheKeyByToken(token)
authorization, found := cache.GlobalCache.Get(cacheKey)
if !found { if !found {
return Fail(c, 401, "您的登录信息已失效,请重新登录后再试。") return Fail(c, 401, "您的登录信息已失效,请重新登录后再试。")
} }
if authorization.(Authorization).Remember { if authorization.(Authorization).Remember {
// 记住登录有效期两周 // 记住登录有效期两周
global.Cache.Set(cacheKey, authorization, time.Hour*time.Duration(24*14)) cache.GlobalCache.Set(cacheKey, authorization, time.Hour*time.Duration(24*14))
} else { } else {
global.Cache.Set(cacheKey, authorization, time.Hour*time.Duration(2)) cache.GlobalCache.Set(cacheKey, authorization, time.Hour*time.Duration(2))
} }
return next(c) return next(c)

View File

@ -1,17 +0,0 @@
package api
import (
"github.com/labstack/echo/v4"
)
// todo 监控
func MonitorEndpoint(c echo.Context) (err error) {
//ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
//if err != nil {
// log.Errorf("升级为WebSocket协议失败%v", err.Error())
// return err
//}
return
}

View File

@ -1,8 +1,7 @@
package api package api
import ( import (
"next-terminal/pkg/constant" "next-terminal/server/constant"
"next-terminal/server/repository"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@ -44,16 +43,43 @@ func OverviewCounterEndPoint(c echo.Context) error {
return Success(c, counter) return Success(c, counter)
} }
func OverviewSessionPoint(c echo.Context) (err error) { func OverviewAssetEndPoint(c echo.Context) error {
d := c.QueryParam("d") account, _ := GetCurrentAccount(c)
var results []repository.D var (
if d == "m" { ssh int64
results, err = sessionRepository.CountSessionByDay(30) rdp int64
vnc int64
telnet int64
kubernetes int64
)
if constant.TypeUser == account.Type {
ssh, _ = assetRepository.CountByUserIdAndProtocol(account.ID, constant.SSH)
rdp, _ = assetRepository.CountByUserIdAndProtocol(account.ID, constant.RDP)
vnc, _ = assetRepository.CountByUserIdAndProtocol(account.ID, constant.VNC)
telnet, _ = assetRepository.CountByUserIdAndProtocol(account.ID, constant.Telnet)
kubernetes, _ = assetRepository.CountByUserIdAndProtocol(account.ID, constant.K8s)
} else { } else {
results, err = sessionRepository.CountSessionByDay(7) ssh, _ = assetRepository.CountByProtocol(constant.SSH)
rdp, _ = assetRepository.CountByProtocol(constant.RDP)
vnc, _ = assetRepository.CountByProtocol(constant.VNC)
telnet, _ = assetRepository.CountByProtocol(constant.Telnet)
kubernetes, _ = assetRepository.CountByProtocol(constant.K8s)
} }
m := echo.Map{
"ssh": ssh,
"rdp": rdp,
"vnc": vnc,
"telnet": telnet,
"kubernetes": kubernetes,
}
return Success(c, m)
}
func OverviewAccessEndPoint(c echo.Context) error {
account, _ := GetCurrentAccount(c)
access, err := sessionRepository.OverviewAccess(account)
if err != nil { if err != nil {
return err return err
} }
return Success(c, results) return Success(c, access)
} }

View File

@ -7,6 +7,7 @@ import (
type RU struct { type RU struct {
UserGroupId string `json:"userGroupId"` UserGroupId string `json:"userGroupId"`
UserId string `json:"userId"` UserId string `json:"userId"`
StrategyId string `json:"strategyId"`
ResourceType string `json:"resourceType"` ResourceType string `json:"resourceType"`
ResourceIds []string `json:"resourceIds"` ResourceIds []string `json:"resourceIds"`
} }
@ -19,26 +20,16 @@ type UR struct {
func RSGetSharersEndPoint(c echo.Context) error { func RSGetSharersEndPoint(c echo.Context) error {
resourceId := c.QueryParam("resourceId") resourceId := c.QueryParam("resourceId")
userIds, err := resourceSharerRepository.FindUserIdsByResourceId(resourceId) resourceType := c.QueryParam("resourceType")
userId := c.QueryParam("userId")
userGroupId := c.QueryParam("userGroupId")
userIds, err := resourceSharerRepository.Find(resourceId, resourceType, userId, userGroupId)
if err != nil { if err != nil {
return err return err
} }
return Success(c, userIds) return Success(c, userIds)
} }
func RSOverwriteSharersEndPoint(c echo.Context) error {
var ur UR
if err := c.Bind(&ur); err != nil {
return err
}
if err := resourceSharerRepository.OverwriteUserIdsByResourceId(ur.ResourceId, ur.ResourceType, ur.UserIds); err != nil {
return err
}
return Success(c, "")
}
func ResourceRemoveByUserIdAssignEndPoint(c echo.Context) error { func ResourceRemoveByUserIdAssignEndPoint(c echo.Context) error {
var ru RU var ru RU
if err := c.Bind(&ru); err != nil { if err := c.Bind(&ru); err != nil {
@ -58,7 +49,7 @@ func ResourceAddByUserIdAssignEndPoint(c echo.Context) error {
return err return err
} }
if err := resourceSharerRepository.AddSharerResources(ru.UserGroupId, ru.UserId, ru.ResourceType, ru.ResourceIds); err != nil { if err := resourceSharerRepository.AddSharerResources(ru.UserGroupId, ru.UserId, ru.StrategyId, ru.ResourceType, ru.ResourceIds); err != nil {
return err return err
} }

View File

@ -5,27 +5,23 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strings"
"time"
"next-terminal/pkg/global" "next-terminal/server/config"
"next-terminal/pkg/log" "next-terminal/server/global/cache"
"next-terminal/pkg/service" "next-terminal/server/log"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/repository" "next-terminal/server/repository"
"next-terminal/server/service"
"next-terminal/server/utils" "next-terminal/server/utils"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
"github.com/patrickmn/go-cache"
"gorm.io/driver/mysql" "gorm.io/driver/mysql"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
) )
const Token = "X-Auth-Token"
var ( var (
userRepository *repository.UserRepository userRepository *repository.UserRepository
userGroupRepository *repository.UserGroupRepository userGroupRepository *repository.UserGroupRepository
@ -35,20 +31,23 @@ var (
propertyRepository *repository.PropertyRepository propertyRepository *repository.PropertyRepository
commandRepository *repository.CommandRepository commandRepository *repository.CommandRepository
sessionRepository *repository.SessionRepository sessionRepository *repository.SessionRepository
numRepository *repository.NumRepository
accessSecurityRepository *repository.AccessSecurityRepository accessSecurityRepository *repository.AccessSecurityRepository
accessGatewayRepository *repository.AccessGatewayRepository
jobRepository *repository.JobRepository jobRepository *repository.JobRepository
jobLogRepository *repository.JobLogRepository jobLogRepository *repository.JobLogRepository
loginLogRepository *repository.LoginLogRepository loginLogRepository *repository.LoginLogRepository
storageRepository *repository.StorageRepository
strategyRepository *repository.StrategyRepository
jobService *service.JobService jobService *service.JobService
propertyService *service.PropertyService propertyService *service.PropertyService
userService *service.UserService userService *service.UserService
sessionService *service.SessionService sessionService *service.SessionService
mailService *service.MailService mailService *service.MailService
numService *service.NumService assetService *service.AssetService
assetService *service.AssetService credentialService *service.CredentialService
credentialService *service.CredentialService storageService *service.StorageService
accessGatewayService *service.AccessGatewayService
) )
func SetupRoutes(db *gorm.DB) *echo.Echo { func SetupRoutes(db *gorm.DB) *echo.Echo {
@ -56,8 +55,10 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
InitRepository(db) InitRepository(db)
InitService() InitService()
cache.GlobalCache.OnEvicted(userService.OnEvicted)
if err := InitDBData(); err != nil { if err := InitDBData(); err != nil {
log.WithError(err).Error("初始化数据异常") log.Errorf("初始化数据异常: %v", err.Error())
os.Exit(0) os.Exit(0)
} }
@ -68,13 +69,10 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
e := echo.New() e := echo.New()
e.HideBanner = true e.HideBanner = true
//e.Logger = log.GetEchoLogger() //e.Logger = log.GetEchoLogger()
e.Use(log.Hook()) //e.Use(log.Hook())
e.File("/", "web/build/index.html") e.File("/", "web/build/index.html")
e.File("/asciinema.html", "web/build/asciinema.html") e.File("/asciinema.html", "web/build/asciinema.html")
e.File("/asciinema-player.js", "web/build/asciinema-player.js")
e.File("/asciinema-player.css", "web/build/asciinema-player.css")
e.File("/", "web/build/index.html") e.File("/", "web/build/index.html")
e.File("/logo.svg", "web/build/logo.svg")
e.File("/favicon.ico", "web/build/favicon.ico") e.File("/favicon.ico", "web/build/favicon.ico")
e.Static("/static", "web/build/static") e.Static("/static", "web/build/static")
@ -93,7 +91,7 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
e.GET("/tunnel", TunEndpoint) e.GET("/tunnel", TunEndpoint)
e.GET("/ssh", SSHEndpoint) e.GET("/ssh", SSHEndpoint)
e.GET("/ssh-monitor", SshMonitor)
e.POST("/logout", LogoutEndpoint) e.POST("/logout", LogoutEndpoint)
e.POST("/change-password", ChangePasswordEndpoint) e.POST("/change-password", ChangePasswordEndpoint)
e.GET("/reload-totp", ReloadTOTPEndpoint) e.GET("/reload-totp", ReloadTOTPEndpoint)
@ -101,15 +99,21 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
e.POST("/confirm-totp", ConfirmTOTPEndpoint) e.POST("/confirm-totp", ConfirmTOTPEndpoint)
e.GET("/info", InfoEndpoint) e.GET("/info", InfoEndpoint)
users := e.Group("/users") account := e.Group("/account")
{ {
users.POST("", Admin(UserCreateEndpoint)) account.GET("/assets", AccountAssetEndpoint)
account.GET("/storage", AccountStorageEndpoint)
}
users := e.Group("/users", Admin)
{
users.POST("", UserCreateEndpoint)
users.GET("/paging", UserPagingEndpoint) users.GET("/paging", UserPagingEndpoint)
users.PUT("/:id", Admin(UserUpdateEndpoint)) users.PUT("/:id", UserUpdateEndpoint)
users.DELETE("/:id", Admin(UserDeleteEndpoint)) users.DELETE("/:id", UserDeleteEndpoint)
users.GET("/:id", Admin(UserGetEndpoint)) users.GET("/:id", UserGetEndpoint)
users.POST("/:id/change-password", Admin(UserChangePasswordEndpoint)) users.POST("/:id/change-password", UserChangePasswordEndpoint)
users.POST("/:id/reset-totp", Admin(UserResetTotpEndpoint)) users.POST("/:id/reset-totp", UserResetTotpEndpoint)
} }
userGroups := e.Group("/user-groups", Admin) userGroups := e.Group("/user-groups", Admin)
@ -119,36 +123,35 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
userGroups.PUT("/:id", UserGroupUpdateEndpoint) userGroups.PUT("/:id", UserGroupUpdateEndpoint)
userGroups.DELETE("/:id", UserGroupDeleteEndpoint) userGroups.DELETE("/:id", UserGroupDeleteEndpoint)
userGroups.GET("/:id", UserGroupGetEndpoint) userGroups.GET("/:id", UserGroupGetEndpoint)
//userGroups.POST("/:id/members", UserGroupAddMembersEndpoint)
//userGroups.DELETE("/:id/members/:memberId", UserGroupDelMembersEndpoint)
} }
assets := e.Group("/assets") assets := e.Group("/assets", Admin)
{ {
assets.GET("", AssetAllEndpoint) assets.GET("", AssetAllEndpoint)
assets.POST("", AssetCreateEndpoint) assets.POST("", AssetCreateEndpoint)
assets.POST("/import", Admin(AssetImportEndpoint)) assets.POST("/import", AssetImportEndpoint)
assets.GET("/paging", AssetPagingEndpoint) assets.GET("/paging", AssetPagingEndpoint)
assets.POST("/:id/tcping", AssetTcpingEndpoint) assets.POST("/:id/tcping", AssetTcpingEndpoint)
assets.PUT("/:id", AssetUpdateEndpoint) assets.PUT("/:id", AssetUpdateEndpoint)
assets.DELETE("/:id", AssetDeleteEndpoint)
assets.GET("/:id", AssetGetEndpoint) assets.GET("/:id", AssetGetEndpoint)
assets.POST("/:id/change-owner", Admin(AssetChangeOwnerEndpoint)) assets.DELETE("/:id", AssetDeleteEndpoint)
assets.POST("/:id/change-owner", AssetChangeOwnerEndpoint)
} }
e.GET("/tags", AssetTagsEndpoint) e.GET("/tags", AssetTagsEndpoint)
commands := e.Group("/commands") commands := e.Group("/commands")
{ {
commands.GET("", CommandAllEndpoint)
commands.GET("/paging", CommandPagingEndpoint) commands.GET("/paging", CommandPagingEndpoint)
commands.POST("", CommandCreateEndpoint) commands.POST("", CommandCreateEndpoint)
commands.PUT("/:id", CommandUpdateEndpoint) commands.PUT("/:id", CommandUpdateEndpoint)
commands.DELETE("/:id", CommandDeleteEndpoint) commands.DELETE("/:id", CommandDeleteEndpoint)
commands.GET("/:id", CommandGetEndpoint) commands.GET("/:id", CommandGetEndpoint)
commands.POST("/:id/change-owner", Admin(CommandChangeOwnerEndpoint)) commands.POST("/:id/change-owner", CommandChangeOwnerEndpoint, Admin)
} }
credentials := e.Group("/credentials") credentials := e.Group("/credentials", Admin)
{ {
credentials.GET("", CredentialAllEndpoint) credentials.GET("", CredentialAllEndpoint)
credentials.GET("/paging", CredentialPagingEndpoint) credentials.GET("/paging", CredentialPagingEndpoint)
@ -156,45 +159,54 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
credentials.PUT("/:id", CredentialUpdateEndpoint) credentials.PUT("/:id", CredentialUpdateEndpoint)
credentials.DELETE("/:id", CredentialDeleteEndpoint) credentials.DELETE("/:id", CredentialDeleteEndpoint)
credentials.GET("/:id", CredentialGetEndpoint) credentials.GET("/:id", CredentialGetEndpoint)
credentials.POST("/:id/change-owner", Admin(CredentialChangeOwnerEndpoint)) credentials.POST("/:id/change-owner", CredentialChangeOwnerEndpoint)
} }
sessions := e.Group("/sessions") sessions := e.Group("/sessions")
{ {
sessions.POST("", SessionCreateEndpoint)
sessions.GET("/paging", Admin(SessionPagingEndpoint)) sessions.GET("/paging", Admin(SessionPagingEndpoint))
sessions.POST("/:id/connect", SessionConnectEndpoint)
sessions.POST("/:id/disconnect", Admin(SessionDisconnectEndpoint)) sessions.POST("/:id/disconnect", Admin(SessionDisconnectEndpoint))
sessions.DELETE("/:id", Admin(SessionDeleteEndpoint))
sessions.GET("/:id/recording", Admin(SessionRecordingEndpoint))
sessions.GET("/:id", Admin(SessionGetEndpoint))
sessions.POST("", SessionCreateEndpoint)
sessions.POST("/:id/connect", SessionConnectEndpoint)
sessions.POST("/:id/resize", SessionResizeEndpoint) sessions.POST("/:id/resize", SessionResizeEndpoint)
sessions.GET("/:id/ls", SessionLsEndpoint) sessions.GET("/:id/stats", SessionStatsEndpoint)
sessions.POST("/:id/ls", SessionLsEndpoint)
sessions.GET("/:id/download", SessionDownloadEndpoint) sessions.GET("/:id/download", SessionDownloadEndpoint)
sessions.POST("/:id/upload", SessionUploadEndpoint) sessions.POST("/:id/upload", SessionUploadEndpoint)
sessions.POST("/:id/edit", SessionEditEndpoint)
sessions.POST("/:id/mkdir", SessionMkDirEndpoint) sessions.POST("/:id/mkdir", SessionMkDirEndpoint)
sessions.POST("/:id/rm", SessionRmEndpoint) sessions.POST("/:id/rm", SessionRmEndpoint)
sessions.POST("/:id/rename", SessionRenameEndpoint) sessions.POST("/:id/rename", SessionRenameEndpoint)
sessions.DELETE("/:id", Admin(SessionDeleteEndpoint))
sessions.GET("/:id/recording", SessionRecordingEndpoint)
} }
resourceSharers := e.Group("/resource-sharers") resourceSharers := e.Group("/resource-sharers", Admin)
{ {
resourceSharers.GET("/sharers", RSGetSharersEndPoint) resourceSharers.GET("", RSGetSharersEndPoint)
resourceSharers.POST("/overwrite-sharers", RSOverwriteSharersEndPoint) resourceSharers.POST("/remove-resources", ResourceRemoveByUserIdAssignEndPoint)
resourceSharers.POST("/remove-resources", Admin(ResourceRemoveByUserIdAssignEndPoint)) resourceSharers.POST("/add-resources", ResourceAddByUserIdAssignEndPoint)
resourceSharers.POST("/add-resources", Admin(ResourceAddByUserIdAssignEndPoint))
} }
loginLogs := e.Group("login-logs", Admin) loginLogs := e.Group("login-logs", Admin)
{ {
loginLogs.GET("/paging", LoginLogPagingEndpoint) loginLogs.GET("/paging", LoginLogPagingEndpoint)
loginLogs.DELETE("/:id", LoginLogDeleteEndpoint) loginLogs.DELETE("/:id", LoginLogDeleteEndpoint)
//loginLogs.DELETE("/clear", LoginLogClearEndpoint)
} }
e.GET("/properties", Admin(PropertyGetEndpoint)) e.GET("/properties", Admin(PropertyGetEndpoint))
e.PUT("/properties", Admin(PropertyUpdateEndpoint)) e.PUT("/properties", Admin(PropertyUpdateEndpoint))
e.GET("/overview/counter", OverviewCounterEndPoint) overview := e.Group("overview", Admin)
e.GET("/overview/sessions", OverviewSessionPoint) {
overview.GET("/counter", OverviewCounterEndPoint)
overview.GET("/asset", OverviewAssetEndPoint)
overview.GET("/access", OverviewAccessEndPoint)
}
jobs := e.Group("/jobs", Admin) jobs := e.Group("/jobs", Admin)
{ {
@ -218,6 +230,44 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
securities.GET("/:id", SecurityGetEndpoint) securities.GET("/:id", SecurityGetEndpoint)
} }
storages := e.Group("/storages")
{
storages.GET("/paging", StoragePagingEndpoint, Admin)
storages.POST("", StorageCreateEndpoint, Admin)
storages.DELETE("/:id", StorageDeleteEndpoint, Admin)
storages.PUT("/:id", StorageUpdateEndpoint, Admin)
storages.GET("/shares", StorageSharesEndpoint, Admin)
storages.GET("/:id", StorageGetEndpoint, Admin)
storages.POST("/:storageId/ls", StorageLsEndpoint)
storages.GET("/:storageId/download", StorageDownloadEndpoint)
storages.POST("/:storageId/upload", StorageUploadEndpoint)
storages.POST("/:storageId/mkdir", StorageMkDirEndpoint)
storages.POST("/:storageId/rm", StorageRmEndpoint)
storages.POST("/:storageId/rename", StorageRenameEndpoint)
storages.POST("/:storageId/edit", StorageEditEndpoint)
}
strategies := e.Group("/strategies", Admin)
{
strategies.GET("", StrategyAllEndpoint)
strategies.GET("/paging", StrategyPagingEndpoint)
strategies.POST("", StrategyCreateEndpoint)
strategies.DELETE("/:id", StrategyDeleteEndpoint)
strategies.PUT("/:id", StrategyUpdateEndpoint)
}
accessGateways := e.Group("/access-gateways", Admin)
{
accessGateways.GET("", AccessGatewayAllEndpoint)
accessGateways.POST("", AccessGatewayCreateEndpoint)
accessGateways.GET("/paging", AccessGatewayPagingEndpoint)
accessGateways.PUT("/:id", AccessGatewayUpdateEndpoint)
accessGateways.DELETE("/:id", AccessGatewayDeleteEndpoint)
accessGateways.GET("/:id", AccessGatewayGetEndpoint)
accessGateways.POST("/:id/reconnect", AccessGatewayReconnectEndpoint)
}
return e return e
} }
@ -241,29 +291,32 @@ func InitRepository(db *gorm.DB) {
propertyRepository = repository.NewPropertyRepository(db) propertyRepository = repository.NewPropertyRepository(db)
commandRepository = repository.NewCommandRepository(db) commandRepository = repository.NewCommandRepository(db)
sessionRepository = repository.NewSessionRepository(db) sessionRepository = repository.NewSessionRepository(db)
numRepository = repository.NewNumRepository(db)
accessSecurityRepository = repository.NewAccessSecurityRepository(db) accessSecurityRepository = repository.NewAccessSecurityRepository(db)
accessGatewayRepository = repository.NewAccessGatewayRepository(db)
jobRepository = repository.NewJobRepository(db) jobRepository = repository.NewJobRepository(db)
jobLogRepository = repository.NewJobLogRepository(db) jobLogRepository = repository.NewJobLogRepository(db)
loginLogRepository = repository.NewLoginLogRepository(db) loginLogRepository = repository.NewLoginLogRepository(db)
storageRepository = repository.NewStorageRepository(db)
strategyRepository = repository.NewStrategyRepository(db)
} }
func InitService() { func InitService() {
jobService = service.NewJobService(jobRepository, jobLogRepository, assetRepository, credentialRepository)
propertyService = service.NewPropertyService(propertyRepository) propertyService = service.NewPropertyService(propertyRepository)
userService = service.NewUserService(userRepository, loginLogRepository) userService = service.NewUserService(userRepository, loginLogRepository)
sessionService = service.NewSessionService(sessionRepository) sessionService = service.NewSessionService(sessionRepository)
mailService = service.NewMailService(propertyRepository) mailService = service.NewMailService(propertyRepository)
numService = service.NewNumService(numRepository)
assetService = service.NewAssetService(assetRepository) assetService = service.NewAssetService(assetRepository)
jobService = service.NewJobService(jobRepository, jobLogRepository, assetRepository, credentialRepository, assetService)
credentialService = service.NewCredentialService(credentialRepository) credentialService = service.NewCredentialService(credentialRepository)
storageService = service.NewStorageService(storageRepository, userRepository, propertyRepository)
accessGatewayService = service.NewAccessGatewayService(accessGatewayRepository)
} }
func InitDBData() (err error) { func InitDBData() (err error) {
if err := propertyService.InitProperties(); err != nil { if err := propertyService.DeleteDeprecatedProperty(); err != nil {
return err return err
} }
if err := numService.InitNums(); err != nil { if err := propertyService.InitProperties(); err != nil {
return err return err
} }
if err := userService.InitUser(); err != nil { if err := userService.InitUser(); err != nil {
@ -287,6 +340,12 @@ func InitDBData() (err error) {
if err := assetService.Encrypt(); err != nil { if err := assetService.Encrypt(); err != nil {
return err return err
} }
if err := storageService.InitStorages(); err != nil {
return err
}
if err := accessGatewayService.ReConnectAll(); err != nil {
return err
}
return nil return nil
} }
@ -368,59 +427,46 @@ func ChangeEncryptionKey(oldEncryptionKey, newEncryptionKey string) error {
return nil return nil
} }
func SetupCache() *cache.Cache {
// 配置缓存器
mCache := cache.New(5*time.Minute, 10*time.Minute)
mCache.OnEvicted(func(key string, value interface{}) {
if strings.HasPrefix(key, Token) {
token := GetTokenFormCacheKey(key)
log.Debugf("用户Token「%v」过期", token)
err := userService.Logout(token)
if err != nil {
log.Errorf("退出登录失败 %v", err)
}
}
})
return mCache
}
func SetupDB() *gorm.DB { func SetupDB() *gorm.DB {
var logMode logger.Interface var logMode logger.Interface
if global.Config.Debug { if config.GlobalCfg.Debug {
logMode = logger.Default.LogMode(logger.Info) logMode = logger.Default.LogMode(logger.Info)
} else { } else {
logMode = logger.Default.LogMode(logger.Silent) logMode = logger.Default.LogMode(logger.Silent)
} }
fmt.Printf("当前数据库模式为:%v\n", global.Config.DB) fmt.Printf("当前数据库模式为:%v\n", config.GlobalCfg.DB)
var err error var err error
var db *gorm.DB var db *gorm.DB
if global.Config.DB == "mysql" { if config.GlobalCfg.DB == "mysql" {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=60s",
global.Config.Mysql.Username, config.GlobalCfg.Mysql.Username,
global.Config.Mysql.Password, config.GlobalCfg.Mysql.Password,
global.Config.Mysql.Hostname, config.GlobalCfg.Mysql.Hostname,
global.Config.Mysql.Port, config.GlobalCfg.Mysql.Port,
global.Config.Mysql.Database, config.GlobalCfg.Mysql.Database,
) )
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logMode, Logger: logMode,
}) })
} else { } else {
db, err = gorm.Open(sqlite.Open(global.Config.Sqlite.File), &gorm.Config{ db, err = gorm.Open(sqlite.Open(config.GlobalCfg.Sqlite.File), &gorm.Config{
Logger: logMode, Logger: logMode,
}) })
} }
if err != nil { if err != nil {
log.WithError(err).Panic("连接数据库异常") log.Errorf("连接数据库异常: %v", err.Error())
os.Exit(0)
} }
if err := db.AutoMigrate(&model.User{}, &model.Asset{}, &model.AssetAttribute{}, &model.Session{}, &model.Command{}, if err := db.AutoMigrate(&model.User{}, &model.Asset{}, &model.AssetAttribute{}, &model.Session{}, &model.Command{},
&model.Credential{}, &model.Property{}, &model.ResourceSharer{}, &model.UserGroup{}, &model.UserGroupMember{}, &model.Credential{}, &model.Property{}, &model.ResourceSharer{}, &model.UserGroup{}, &model.UserGroupMember{},
&model.LoginLog{}, &model.Num{}, &model.Job{}, &model.JobLog{}, &model.AccessSecurity{}); err != nil { &model.LoginLog{}, &model.Job{}, &model.JobLog{}, &model.AccessSecurity{}, &model.AccessGateway{},
log.WithError(err).Panic("初始化数据库表结构异常") &model.Storage{}, &model.Strategy{}); err != nil {
log.Errorf("初始化数据库表结构异常: %v", err.Error())
os.Exit(0)
} }
return db return db
} }

View File

@ -4,7 +4,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"next-terminal/pkg/global" "next-terminal/server/global/security"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/utils" "next-terminal/server/utils"
@ -24,9 +24,14 @@ func SecurityCreateEndpoint(c echo.Context) error {
return err return err
} }
// 更新内存中的安全规则 // 更新内存中的安全规则
if err := ReloadAccessSecurity(); err != nil { rule := &security.Security{
return err ID: item.ID,
IP: item.IP,
Rule: item.Rule,
Priority: item.Priority,
} }
security.GlobalSecurityManager.Add <- rule
return Success(c, "") return Success(c, "")
} }
@ -36,15 +41,18 @@ func ReloadAccessSecurity() error {
return err return err
} }
if len(rules) > 0 { if len(rules) > 0 {
var securities []*global.Security // 先清空
security.GlobalSecurityManager.Clear()
// 再添加到全局的安全管理器中
for i := 0; i < len(rules); i++ { for i := 0; i < len(rules); i++ {
rule := global.Security{ rule := &security.Security{
IP: rules[i].IP, ID: rules[i].ID,
Rule: rules[i].Rule, IP: rules[i].IP,
Rule: rules[i].Rule,
Priority: rules[i].Priority,
} }
securities = append(securities, &rule) security.GlobalSecurityManager.Add <- rule
} }
global.Securities = securities
} }
return nil return nil
} }
@ -81,9 +89,15 @@ func SecurityUpdateEndpoint(c echo.Context) error {
return err return err
} }
// 更新内存中的安全规则 // 更新内存中的安全规则
if err := ReloadAccessSecurity(); err != nil { security.GlobalSecurityManager.Del <- id
return err rule := &security.Security{
ID: item.ID,
IP: item.IP,
Rule: item.Rule,
Priority: item.Priority,
} }
security.GlobalSecurityManager.Add <- rule
return Success(c, nil) return Success(c, nil)
} }
@ -92,15 +106,14 @@ func SecurityDeleteEndpoint(c echo.Context) error {
split := strings.Split(ids, ",") split := strings.Split(ids, ",")
for i := range split { for i := range split {
jobId := split[i] id := split[i]
if err := accessSecurityRepository.DeleteById(jobId); err != nil { if err := accessSecurityRepository.DeleteById(id); err != nil {
return err return err
} }
// 更新内存中的安全规则
security.GlobalSecurityManager.Del <- id
} }
// 更新内存中的安全规则
if err := ReloadAccessSecurity(); err != nil {
return err
}
return Success(c, nil) return Success(c, nil)
} }

View File

@ -1,11 +1,11 @@
package api package api
import ( import (
"bufio"
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -13,14 +13,18 @@ import (
"strings" "strings"
"sync" "sync"
"next-terminal/pkg/constant" "next-terminal/server/constant"
"next-terminal/pkg/global" "next-terminal/server/global/session"
"next-terminal/pkg/log" "next-terminal/server/guacd"
"next-terminal/server/log"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/service"
"next-terminal/server/utils" "next-terminal/server/utils"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/pkg/sftp" "github.com/pkg/sftp"
"gorm.io/gorm"
) )
func SessionPagingEndpoint(c echo.Context) error { func SessionPagingEndpoint(c echo.Context) error {
@ -42,7 +46,7 @@ func SessionPagingEndpoint(c echo.Context) error {
if status == constant.Disconnected && len(items[i].Recording) > 0 { if status == constant.Disconnected && len(items[i].Recording) > 0 {
var recording string var recording string
if items[i].Mode == constant.Naive { if items[i].Mode == constant.Naive || items[i].Mode == constant.Terminal {
recording = items[i].Recording recording = items[i].Recording
} else { } else {
recording = items[i].Recording + "/recording" recording = items[i].Recording + "/recording"
@ -78,14 +82,28 @@ func SessionDeleteEndpoint(c echo.Context) error {
func SessionConnectEndpoint(c echo.Context) error { func SessionConnectEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
session := model.Session{} s := model.Session{}
session.ID = sessionId s.ID = sessionId
session.Status = constant.Connected s.Status = constant.Connected
session.ConnectedTime = utils.NowJsonTime() s.ConnectedTime = utils.NowJsonTime()
if err := sessionRepository.UpdateById(&session, sessionId); err != nil { if err := sessionRepository.UpdateById(&s, sessionId); err != nil {
return err return err
} }
o, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
asset, err := assetRepository.FindById(o.AssetId)
if err != nil {
return err
}
if !asset.Active {
asset.Active = true
_ = assetRepository.UpdateById(&asset, asset.ID)
}
return Success(c, nil) return Success(c, nil)
} }
@ -104,18 +122,48 @@ var mutex sync.Mutex
func CloseSessionById(sessionId string, code int, reason string) { func CloseSessionById(sessionId string, code int, reason string) {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
observable, _ := global.Store.Get(sessionId) nextSession := session.GlobalSessionManager.GetById(sessionId)
if observable != nil { if nextSession != nil {
log.Debugf("会话%v创建者退出,原因:%v", sessionId, reason) log.Debugf("[%v] 会话关闭,原因:%v", sessionId, reason)
observable.Subject.Close(code, reason) WriteCloseMessage(nextSession.WebSocket, nextSession.Mode, code, reason)
for i := 0; i < len(observable.Observers); i++ { if nextSession.Observer != nil {
observable.Observers[i].Close(code, reason) obs := nextSession.Observer.All()
log.Debugf("强制踢出会话%v的观察者", sessionId) for _, ob := range obs {
WriteCloseMessage(ob.WebSocket, ob.Mode, code, reason)
log.Debugf("[%v] 强制踢出会话的观察者: %v", sessionId, ob.ID)
}
} }
} }
global.Store.Del(sessionId) session.GlobalSessionManager.Del <- sessionId
DisDBSess(sessionId, code, reason)
}
func WriteCloseMessage(ws *websocket.Conn, mode string, code int, reason string) {
switch mode {
case constant.Guacd:
if ws != nil {
err := guacd.NewInstruction("error", "", strconv.Itoa(code))
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String()))
disconnect := guacd.NewInstruction("disconnect")
_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
}
case constant.Naive:
if ws != nil {
msg := `0` + reason
_ = ws.WriteMessage(websocket.TextMessage, []byte(msg))
}
case constant.Terminal:
// 这里是关闭观察者的ssh会话
if ws != nil {
msg := `0` + reason
_ = ws.WriteMessage(websocket.TextMessage, []byte(msg))
}
}
}
func DisDBSess(sessionId string, code int, reason string) {
s, err := sessionRepository.FindById(sessionId) s, err := sessionRepository.FindById(sessionId)
if err != nil { if err != nil {
return return
@ -131,17 +179,17 @@ func CloseSessionById(sessionId string, code int, reason string) {
return return
} }
session := model.Session{} ss := model.Session{}
session.ID = sessionId ss.ID = sessionId
session.Status = constant.Disconnected ss.Status = constant.Disconnected
session.DisconnectedTime = utils.NowJsonTime() ss.DisconnectedTime = utils.NowJsonTime()
session.Code = code ss.Code = code
session.Message = reason ss.Message = reason
session.Password = "-" ss.Password = "-"
session.PrivateKey = "-" ss.PrivateKey = "-"
session.Passphrase = "-" ss.Passphrase = "-"
_ = sessionRepository.UpdateById(&session, sessionId) _ = sessionRepository.UpdateById(&ss, sessionId)
} }
func SessionResizeEndpoint(c echo.Context) error { func SessionResizeEndpoint(c echo.Context) error {
@ -150,11 +198,10 @@ func SessionResizeEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
if len(width) == 0 || len(height) == 0 { if len(width) == 0 || len(height) == 0 {
panic("参数异常") return errors.New("参数异常")
} }
intWidth, _ := strconv.Atoi(width) intWidth, _ := strconv.Atoi(width)
intHeight, _ := strconv.Atoi(height) intHeight, _ := strconv.Atoi(height)
if err := sessionRepository.UpdateWindowSizeById(intWidth, intHeight, sessionId); err != nil { if err := sessionRepository.UpdateWindowSizeById(intWidth, intHeight, sessionId); err != nil {
@ -175,37 +222,83 @@ func SessionCreateEndpoint(c echo.Context) error {
user, _ := GetCurrentAccount(c) user, _ := GetCurrentAccount(c)
if constant.TypeUser == user.Type {
// 检测是否有访问权限
assetIds, err := resourceSharerRepository.FindAssetIdsByUserId(user.ID)
if err != nil {
return err
}
if !utils.Contains(assetIds, assetId) {
return errors.New("您没有权限访问此资产")
}
}
asset, err := assetRepository.FindById(assetId) asset, err := assetRepository.FindById(assetId)
if err != nil { if err != nil {
return err return err
} }
session := &model.Session{ var (
ID: utils.UUID(), upload = "1"
AssetId: asset.ID, download = "1"
Username: asset.Username, _delete = "1"
Password: asset.Password, rename = "1"
PrivateKey: asset.PrivateKey, edit = "1"
Passphrase: asset.Passphrase, fileSystem = "1"
Protocol: asset.Protocol, )
IP: asset.IP, if asset.Owner != user.ID && constant.TypeUser == user.Type {
Port: asset.Port, // 普通用户访问非自己创建的资产需要校验权限
Status: constant.NoConnect, resourceSharers, err := resourceSharerRepository.FindByResourceIdAndUserId(assetId, user.ID)
Creator: user.ID, if err != nil {
ClientIP: c.RealIP(), return err
Mode: mode, }
if len(resourceSharers) == 0 {
return errors.New("您没有权限访问此资产")
}
strategyId := resourceSharers[0].StrategyId
if strategyId != "" {
strategy, err := strategyRepository.FindById(strategyId)
if err != nil {
if !errors.Is(gorm.ErrRecordNotFound, err) {
return err
}
} else {
upload = strategy.Upload
download = strategy.Download
_delete = strategy.Delete
rename = strategy.Rename
edit = strategy.Edit
}
}
}
var storageId = ""
if constant.RDP == asset.Protocol {
attr, err := assetRepository.FindAssetAttrMapByAssetId(assetId)
if err != nil {
return err
}
if "true" == attr[guacd.EnableDrive] {
fileSystem = "1"
storageId = attr[guacd.DrivePath]
if storageId == "" {
storageId = user.ID
}
} else {
fileSystem = "0"
}
}
s := &model.Session{
ID: utils.UUID(),
AssetId: asset.ID,
Username: asset.Username,
Password: asset.Password,
PrivateKey: asset.PrivateKey,
Passphrase: asset.Passphrase,
Protocol: asset.Protocol,
IP: asset.IP,
Port: asset.Port,
Status: constant.NoConnect,
Creator: user.ID,
ClientIP: c.RealIP(),
Mode: mode,
Upload: upload,
Download: download,
Delete: _delete,
Rename: rename,
Edit: edit,
StorageId: storageId,
AccessGatewayId: asset.AccessGatewayId,
} }
if asset.AccountType == "credential" { if asset.AccountType == "credential" {
@ -215,28 +308,41 @@ func SessionCreateEndpoint(c echo.Context) error {
} }
if credential.Type == constant.Custom { if credential.Type == constant.Custom {
session.Username = credential.Username s.Username = credential.Username
session.Password = credential.Password s.Password = credential.Password
} else { } else {
session.Username = credential.Username s.Username = credential.Username
session.PrivateKey = credential.PrivateKey s.PrivateKey = credential.PrivateKey
session.Passphrase = credential.Passphrase s.Passphrase = credential.Passphrase
} }
} }
if err := sessionRepository.Create(session); err != nil { if err := sessionRepository.Create(s); err != nil {
return err return err
} }
return Success(c, echo.Map{"id": session.ID}) return Success(c, echo.Map{
"id": s.ID,
"upload": s.Upload,
"download": s.Download,
"delete": s.Delete,
"rename": s.Rename,
"edit": s.Edit,
"storageId": s.StorageId,
"fileSystem": fileSystem,
})
} }
func SessionUploadEndpoint(c echo.Context) error { func SessionUploadEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId) s, err := sessionRepository.FindById(sessionId)
if err != nil { if err != nil {
return err return err
} }
if s.Upload != "1" {
return errors.New("禁止操作")
}
file, err := c.FormFile("file") file, err := c.FormFile("file")
if err != nil { if err != nil {
return err return err
@ -251,77 +357,94 @@ func SessionUploadEndpoint(c echo.Context) error {
remoteDir := c.QueryParam("dir") remoteDir := c.QueryParam("dir")
remoteFile := path.Join(remoteDir, filename) remoteFile := path.Join(remoteDir, filename)
if "ssh" == session.Protocol { if "ssh" == s.Protocol {
tun, ok := global.Store.Get(sessionId) nextSession := session.GlobalSessionManager.GetById(sessionId)
if !ok { if nextSession == nil {
return errors.New("获取sftp客户端失败") return errors.New("获取会话失败")
} }
dstFile, err := tun.Subject.NextTerminal.SftpClient.Create(remoteFile) sftpClient := nextSession.NextTerminal.SftpClient
// 文件夹不存在时自动创建文件夹
if _, err := sftpClient.Stat(remoteDir); os.IsNotExist(err) {
if err := sftpClient.MkdirAll(remoteDir); err != nil {
return err
}
}
dstFile, err := sftpClient.Create(remoteFile)
if err != nil { if err != nil {
return err return err
} }
defer dstFile.Close() defer dstFile.Close()
buf := make([]byte, 1024) if _, err = io.Copy(dstFile, src); err != nil {
for {
n, err := src.Read(buf)
if err != nil {
if err != io.EOF {
log.Warnf("文件上传错误 %v", err)
} else {
break
}
}
_, _ = dstFile.Write(buf[:n])
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
if strings.Contains(remoteFile, "../") {
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
// Destination
dst, err := os.Create(path.Join(drivePath, remoteFile))
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
return err return err
} }
return Success(c, nil) return Success(c, nil)
} else if "rdp" == s.Protocol {
return StorageUpload(c, file, s.StorageId)
} }
return err return err
} }
func SessionDownloadEndpoint(c echo.Context) error { func SessionEditEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId) s, err := sessionRepository.FindById(sessionId)
if err != nil { if err != nil {
return err return err
} }
//remoteDir := c.Query("dir") if s.Edit != "1" {
return errors.New("禁止操作")
}
file := c.FormValue("file")
fileContent := c.FormValue("fileContent")
if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
sftpClient := nextSession.NextTerminal.SftpClient
dstFile, err := sftpClient.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
if err != nil {
return err
}
defer dstFile.Close()
write := bufio.NewWriter(dstFile)
if _, err := write.WriteString(fileContent); err != nil {
return err
}
if err := write.Flush(); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == s.Protocol {
return StorageEdit(c, file, fileContent, s.StorageId)
}
return err
}
func SessionDownloadEndpoint(c echo.Context) error {
sessionId := c.Param("id")
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
if s.Download != "1" {
return errors.New("禁止操作")
}
remoteFile := c.QueryParam("file") remoteFile := c.QueryParam("file")
// 获取带后缀的文件名称 // 获取带后缀的文件名称
filenameWithSuffix := path.Base(remoteFile) filenameWithSuffix := path.Base(remoteFile)
if "ssh" == session.Protocol { if "ssh" == s.Protocol {
tun, ok := global.Store.Get(sessionId) nextSession := session.GlobalSessionManager.GetById(sessionId)
if !ok { if nextSession == nil {
return errors.New("获取sftp客户端失败") return errors.New("获取会话失败")
} }
dstFile, err := tun.Subject.NextTerminal.SftpClient.Open(remoteFile) dstFile, err := nextSession.NextTerminal.SftpClient.Open(remoteFile)
if err != nil { if err != nil {
return err return err
} }
@ -335,105 +458,51 @@ func SessionDownloadEndpoint(c echo.Context) error {
} }
return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(buff.Bytes())) return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(buff.Bytes()))
} else if "rdp" == session.Protocol { } else if "rdp" == s.Protocol {
if strings.Contains(remoteFile, "../") { storageId := s.StorageId
SafetyRuleTrigger(c) return StorageDownload(c, remoteFile, storageId)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
return c.Attachment(path.Join(drivePath, remoteFile), filenameWithSuffix)
} }
return err return err
} }
type File struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
Mode string `json:"mode"`
IsLink bool `json:"isLink"`
ModTime utils.JsonTime `json:"modTime"`
Size int64 `json:"size"`
}
func SessionLsEndpoint(c echo.Context) error { func SessionLsEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
session, err := sessionRepository.FindByIdAndDecrypt(sessionId) s, err := sessionRepository.FindByIdAndDecrypt(sessionId)
if err != nil { if err != nil {
return err return err
} }
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol { remoteDir := c.FormValue("dir")
tun, ok := global.Store.Get(sessionId) if "ssh" == s.Protocol {
if !ok { nextSession := session.GlobalSessionManager.GetById(sessionId)
return errors.New("获取sftp客户端失败") if nextSession == nil {
return errors.New("获取会话失败")
} }
if tun.Subject.NextTerminal == nil { if nextSession.NextTerminal.SftpClient == nil {
nextTerminal, err := CreateNextTerminalBySession(session) sftpClient, err := sftp.NewClient(nextSession.NextTerminal.SshClient)
if err != nil {
return err
}
tun.Subject.NextTerminal = nextTerminal
}
if tun.Subject.NextTerminal.SftpClient == nil {
sftpClient, err := sftp.NewClient(tun.Subject.NextTerminal.SshClient)
if err != nil { if err != nil {
log.Errorf("创建sftp客户端失败%v", err.Error()) log.Errorf("创建sftp客户端失败%v", err.Error())
return err return err
} }
tun.Subject.NextTerminal.SftpClient = sftpClient nextSession.NextTerminal.SftpClient = sftpClient
} }
fileInfos, err := tun.Subject.NextTerminal.SftpClient.ReadDir(remoteDir) fileInfos, err := nextSession.NextTerminal.SftpClient.ReadDir(remoteDir)
if err != nil { if err != nil {
return err return err
} }
var files = make([]File, 0) var files = make([]service.File, 0)
for i := range fileInfos { for i := range fileInfos {
// 忽略因此文件 // 忽略隐藏文件
if strings.HasPrefix(fileInfos[i].Name(), ".") { if strings.HasPrefix(fileInfos[i].Name(), ".") {
continue continue
} }
file := File{ file := service.File{
Name: fileInfos[i].Name(),
Path: path.Join(remoteDir, fileInfos[i].Name()),
IsDir: fileInfos[i].IsDir(),
Mode: fileInfos[i].Mode().String(),
IsLink: fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink,
ModTime: utils.NewJsonTime(fileInfos[i].ModTime()),
Size: fileInfos[i].Size(),
}
files = append(files, file)
}
return Success(c, files)
} else if "rdp" == session.Protocol {
if strings.Contains(remoteDir, "../") {
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
fileInfos, err := ioutil.ReadDir(path.Join(drivePath, remoteDir))
if err != nil {
return err
}
var files = make([]File, 0)
for i := range fileInfos {
file := File{
Name: fileInfos[i].Name(), Name: fileInfos[i].Name(),
Path: path.Join(remoteDir, fileInfos[i].Name()), Path: path.Join(remoteDir, fileInfos[i].Name()),
IsDir: fileInfos[i].IsDir(), IsDir: fileInfos[i].IsDir(),
@ -447,115 +516,87 @@ func SessionLsEndpoint(c echo.Context) error {
} }
return Success(c, files) return Success(c, files)
} else if "rdp" == s.Protocol {
storageId := s.StorageId
return StorageLs(c, remoteDir, storageId)
} }
return errors.New("当前协议不支持此操作") return errors.New("当前协议不支持此操作")
} }
func SafetyRuleTrigger(c echo.Context) {
log.Warnf("IP %v 尝试进行攻击请ban掉此IP", c.RealIP())
security := model.AccessSecurity{
ID: utils.UUID(),
Source: "安全规则触发",
IP: c.RealIP(),
Rule: constant.AccessRuleReject,
}
_ = accessSecurityRepository.Create(&security)
}
func SessionMkDirEndpoint(c echo.Context) error { func SessionMkDirEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId) s, err := sessionRepository.FindById(sessionId)
if err != nil { if err != nil {
return err return err
} }
remoteDir := c.QueryParam("dir") if s.Upload != "1" {
if "ssh" == session.Protocol { return errors.New("禁止操作")
tun, ok := global.Store.Get(sessionId) }
if !ok { remoteDir := c.QueryParam("dir")
return errors.New("获取sftp客户端失败") if "ssh" == s.Protocol {
} nextSession := session.GlobalSessionManager.GetById(sessionId)
if err := tun.Subject.NextTerminal.SftpClient.Mkdir(remoteDir); err != nil { if nextSession == nil {
return err return errors.New("获取会话失败")
} }
return Success(c, nil) if err := nextSession.NextTerminal.SftpClient.Mkdir(remoteDir); err != nil {
} else if "rdp" == session.Protocol { return err
if strings.Contains(remoteDir, "../") { }
SafetyRuleTrigger(c) return Success(c, nil)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。") } else if "rdp" == s.Protocol {
} return StorageMkDir(c, remoteDir, s.StorageId)
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
if err := os.MkdirAll(path.Join(drivePath, remoteDir), os.ModePerm); err != nil {
return err
}
return Success(c, nil)
} }
return errors.New("当前协议不支持此操作") return errors.New("当前协议不支持此操作")
} }
func SessionRmEndpoint(c echo.Context) error { func SessionRmEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId) s, err := sessionRepository.FindById(sessionId)
if err != nil { if err != nil {
return err return err
} }
key := c.QueryParam("key") if s.Delete != "1" {
if "ssh" == session.Protocol { return errors.New("禁止操作")
tun, ok := global.Store.Get(sessionId) }
if !ok { // 文件夹或者文件
return errors.New("获取sftp客户端失败") file := c.FormValue("file")
if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
} }
sftpClient := tun.Subject.NextTerminal.SftpClient sftpClient := nextSession.NextTerminal.SftpClient
stat, err := sftpClient.Stat(key) stat, err := sftpClient.Stat(file)
if err != nil { if err != nil {
return err return err
} }
if stat.IsDir() { if stat.IsDir() {
fileInfos, err := sftpClient.ReadDir(key) fileInfos, err := sftpClient.ReadDir(file)
if err != nil { if err != nil {
return err return err
} }
for i := range fileInfos { for i := range fileInfos {
if err := sftpClient.Remove(path.Join(key, fileInfos[i].Name())); err != nil { if err := sftpClient.Remove(path.Join(file, fileInfos[i].Name())); err != nil {
return err return err
} }
} }
if err := sftpClient.RemoveDirectory(key); err != nil { if err := sftpClient.RemoveDirectory(file); err != nil {
return err return err
} }
} else { } else {
if err := sftpClient.Remove(key); err != nil { if err := sftpClient.Remove(file); err != nil {
return err return err
} }
} }
return Success(c, nil) return Success(c, nil)
} else if "rdp" == session.Protocol { } else if "rdp" == s.Protocol {
if strings.Contains(key, "../") { return StorageRm(c, file, s.StorageId)
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
if err := os.RemoveAll(path.Join(drivePath, key)); err != nil {
return err
}
return Success(c, nil)
} }
return errors.New("当前协议不支持此操作") return errors.New("当前协议不支持此操作")
@ -563,58 +604,80 @@ func SessionRmEndpoint(c echo.Context) error {
func SessionRenameEndpoint(c echo.Context) error { func SessionRenameEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId) s, err := sessionRepository.FindById(sessionId)
if err != nil { if err != nil {
return err return err
} }
if s.Rename != "1" {
return errors.New("禁止操作")
}
oldName := c.QueryParam("oldName") oldName := c.QueryParam("oldName")
newName := c.QueryParam("newName") newName := c.QueryParam("newName")
if "ssh" == session.Protocol { if "ssh" == s.Protocol {
tun, ok := global.Store.Get(sessionId) nextSession := session.GlobalSessionManager.GetById(sessionId)
if !ok { if nextSession == nil {
return errors.New("获取sftp客户端失败") return errors.New("获取会话失败")
} }
sftpClient := tun.Subject.NextTerminal.SftpClient sftpClient := nextSession.NextTerminal.SftpClient
if err := sftpClient.Rename(oldName, newName); err != nil { if err := sftpClient.Rename(oldName, newName); err != nil {
return err return err
} }
return Success(c, nil) return Success(c, nil)
} else if "rdp" == session.Protocol { } else if "rdp" == s.Protocol {
if strings.Contains(oldName, "../") { return StorageRename(c, oldName, newName, s.StorageId)
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
if err := os.Rename(path.Join(drivePath, oldName), path.Join(drivePath, newName)); err != nil {
return err
}
return Success(c, nil)
} }
return errors.New("当前协议不支持此操作") return errors.New("当前协议不支持此操作")
} }
func SessionRecordingEndpoint(c echo.Context) error { func SessionRecordingEndpoint(c echo.Context) error {
sessionId := c.Param("id") sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId) s, err := sessionRepository.FindById(sessionId)
if err != nil { if err != nil {
return err return err
} }
var recording string var recording string
if session.Mode == constant.Naive { if s.Mode == constant.Naive || s.Mode == constant.Terminal {
recording = session.Recording recording = s.Recording
} else { } else {
recording = session.Recording + "/recording" recording = s.Recording + "/recording"
} }
log.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording)) log.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording))
return c.File(recording) return c.File(recording)
} }
func SessionGetEndpoint(c echo.Context) error {
sessionId := c.Param("id")
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
return Success(c, s)
}
func SessionStatsEndpoint(c echo.Context) error {
sessionId := c.Param("id")
s, err := sessionRepository.FindByIdAndDecrypt(sessionId)
if err != nil {
return err
}
if "ssh" != s.Protocol {
return Fail(c, -1, "不支持当前协议")
}
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
sshClient := nextSession.NextTerminal.SshClient
stats, err := GetAllStats(sshClient)
if err != nil {
return err
}
return Success(c, stats)
}

View File

@ -1,18 +1,23 @@
package api package api
import ( import (
"context"
"encoding/base64"
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"path" "path"
"strconv" "strconv"
"time" "time"
"unicode/utf8"
"next-terminal/pkg/constant" "next-terminal/server/config"
"next-terminal/pkg/global" "next-terminal/server/constant"
"next-terminal/pkg/guacd" "next-terminal/server/global/session"
"next-terminal/pkg/log" "next-terminal/server/guacd"
"next-terminal/pkg/term" "next-terminal/server/log"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/term"
"next-terminal/server/utils" "next-terminal/server/utils"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@ -27,17 +32,44 @@ var UpGrader = websocket.Upgrader{
} }
const ( const (
Connected = "connected" Closed = 0
Data = "data" Connected = 1
Resize = "resize" Data = 2
Closed = "closed" Resize = 3
Ping = 4
) )
type Message struct { type Message struct {
Type string `json:"type"` Type int `json:"type"`
Content string `json:"content"` Content string `json:"content"`
} }
func (r Message) ToString() string {
if r.Content != "" {
return strconv.Itoa(r.Type) + r.Content
} else {
return strconv.Itoa(r.Type)
}
}
func NewMessage(_type int, content string) Message {
return Message{Content: content, Type: _type}
}
func ParseMessage(value string) (message Message, err error) {
if value == "" {
return
}
_type, err := strconv.Atoi(value[:1])
if err != nil {
return
}
var content = value[1:]
message = NewMessage(_type, content)
return
}
type WindowSize struct { type WindowSize struct {
Cols int `json:"cols"` Cols int `json:"cols"`
Rows int `json:"rows"` Rows int `json:"rows"`
@ -50,92 +82,73 @@ func SSHEndpoint(c echo.Context) (err error) {
return err return err
} }
defer ws.Close()
sessionId := c.QueryParam("sessionId") sessionId := c.QueryParam("sessionId")
cols, _ := strconv.Atoi(c.QueryParam("cols")) cols, _ := strconv.Atoi(c.QueryParam("cols"))
rows, _ := strconv.Atoi(c.QueryParam("rows")) rows, _ := strconv.Atoi(c.QueryParam("rows"))
session, err := sessionRepository.FindByIdAndDecrypt(sessionId) s, err := sessionRepository.FindByIdAndDecrypt(sessionId)
if err != nil { if err != nil {
msg := Message{ return WriteMessage(ws, NewMessage(Closed, "获取会话失败"))
Type: Closed,
Content: "get sshSession error." + err.Error(),
}
_ = WriteMessage(ws, msg)
return err
} }
user, _ := GetCurrentAccount(c) if err := permissionCheck(c, s.AssetId); err != nil {
if constant.TypeUser == user.Type { return WriteMessage(ws, NewMessage(Closed, err.Error()))
// 检测是否有访问权限
assetIds, err := resourceSharerRepository.FindAssetIdsByUserId(user.ID)
if err != nil {
return err
}
if !utils.Contains(assetIds, session.AssetId) {
msg := Message{
Type: Closed,
Content: "您没有权限访问此资产",
}
return WriteMessage(ws, msg)
}
} }
var ( var (
username = session.Username username = s.Username
password = session.Password password = s.Password
privateKey = session.PrivateKey privateKey = s.PrivateKey
passphrase = session.Passphrase passphrase = s.Passphrase
ip = session.IP ip = s.IP
port = session.Port port = s.Port
) )
recording := "" if s.AccessGatewayId != "" && s.AccessGatewayId != "-" {
propertyMap := propertyRepository.FindAllMap() g, err := accessGatewayService.GetGatewayAndReconnectById(s.AccessGatewayId)
if propertyMap[guacd.EnableRecording] == "true" { if err != nil {
recording = path.Join(propertyMap[guacd.RecordingPath], sessionId, "recording.cast") return WriteMessage(ws, NewMessage(Closed, "获取接入网关失败:"+err.Error()))
}
tun := global.Tun{
Protocol: session.Protocol,
Mode: session.Mode,
WebSocket: ws,
}
if session.ConnectionId != "" {
// 监控会话
observable, ok := global.Store.Get(sessionId)
if ok {
observers := append(observable.Observers, tun)
observable.Observers = observers
global.Store.Set(sessionId, observable)
log.Debugf("加入会话%v,当前观察者数量为:%v", session.ConnectionId, len(observers))
} }
if !g.Connected {
return err return WriteMessage(ws, NewMessage(Closed, "接入网关不可用:"+g.Message))
}
exposedIP, exposedPort, err := g.OpenSshTunnel(s.ID, ip, port)
if err != nil {
return WriteMessage(ws, NewMessage(Closed, "创建隧道失败:"+err.Error()))
}
defer g.CloseSshTunnel(s.ID)
ip = exposedIP
port = exposedPort
} }
nextTerminal, err := term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, rows, cols, recording) recording := ""
var isRecording = false
property, err := propertyRepository.FindByName(guacd.EnableRecording)
if err == nil && property.Value == "true" {
isRecording = true
}
if isRecording {
recording = path.Join(config.GlobalCfg.Guacd.Recording, sessionId, "recording.cast")
}
var xterm = "xterm-256color"
nextTerminal, err := term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, rows, cols, recording, xterm, true)
if err != nil { if err != nil {
log.Errorf("创建SSH客户端失败%v", err.Error()) return WriteMessage(ws, NewMessage(Closed, "创建SSH客户端失败"+err.Error()))
msg := Message{ }
Type: Closed,
Content: err.Error(), if err := nextTerminal.RequestPty(xterm, rows, cols); err != nil {
}
err := WriteMessage(ws, msg)
return err return err
} }
tun.NextTerminal = nextTerminal
var observers []global.Tun if err := nextTerminal.Shell(); err != nil {
observable := global.Observable{ return err
Subject: &tun,
Observers: observers,
} }
global.Store.Set(sessionId, &observable)
sess := model.Session{ sess := model.Session{
ConnectionId: sessionId, ConnectionId: sessionId,
Width: cols, Width: cols,
@ -149,106 +162,209 @@ func SSHEndpoint(c echo.Context) (err error) {
return err return err
} }
msg := Message{ if err := WriteMessage(ws, NewMessage(Connected, "")); err != nil {
Type: Connected, return err
Content: "",
} }
_ = WriteMessage(ws, msg)
quitChan := make(chan bool) nextSession := &session.Session{
ID: s.ID,
Protocol: s.Protocol,
Mode: s.Mode,
WebSocket: ws,
GuacdTunnel: nil,
NextTerminal: nextTerminal,
Observer: session.NewObserver(s.ID),
}
go nextSession.Observer.Run()
session.GlobalSessionManager.Add <- nextSession
go ReadMessage(nextTerminal, quitChan, ws) ctx, cancel := context.WithCancel(context.Background())
tick := time.NewTicker(time.Millisecond * time.Duration(60))
defer tick.Stop()
var buf []byte
dataChan := make(chan rune)
go func() {
SshLoop:
for {
select {
case <-ctx.Done():
log.Debugf("WebSocket已关闭即将关闭SSH连接...")
break SshLoop
default:
r, size, err := nextTerminal.StdoutReader.ReadRune()
if err != nil {
log.Debugf("SSH 读取失败,即将退出循环...")
_ = WriteMessage(ws, NewMessage(Closed, ""))
break SshLoop
}
if size > 0 {
dataChan <- r
}
}
}
log.Debugf("SSH 连接已关闭,退出循环。")
}()
go func() {
tickLoop:
for {
select {
case <-ctx.Done():
break tickLoop
case <-tick.C:
if len(buf) > 0 {
s := string(buf)
// 录屏
if isRecording {
_ = nextTerminal.Recorder.WriteData(s)
}
// 监控
if len(nextSession.Observer.All()) > 0 {
obs := nextSession.Observer.All()
for _, ob := range obs {
_ = WriteMessage(ob.WebSocket, NewMessage(Data, s))
}
}
if err := WriteMessage(ws, NewMessage(Data, s)); err != nil {
log.Debugf("WebSocket写入失败即将退出循环...")
cancel()
}
buf = []byte{}
}
case data := <-dataChan:
if data != utf8.RuneError {
p := make([]byte, utf8.RuneLen(data))
utf8.EncodeRune(p, data)
buf = append(buf, p...)
} else {
buf = append(buf, []byte("@")...)
}
}
}
log.Debugf("SSH 连接已关闭,退出定时器循环。")
}()
//var enterKeys []rune
//enterIndex := 0
for { for {
_, message, err := ws.ReadMessage() _, message, err := ws.ReadMessage()
if err != nil { if err != nil {
// web socket会话关闭后主动关闭ssh会话 // web socket会话关闭后主动关闭ssh会话
CloseSessionById(sessionId, Normal, "正常退出") log.Debugf("WebSocket已关闭")
quitChan <- true CloseSessionById(sessionId, Normal, "用户正常退出")
quitChan <- true cancel()
break break
} }
var msg Message msg, err := ParseMessage(string(message))
err = json.Unmarshal(message, &msg)
if err != nil { if err != nil {
log.Warnf("解析Json失败: %v, 原始字符串:%v", err, string(message)) log.Warnf("消息解码失败: %v, 原始字符串:%v", err, string(message))
continue continue
} }
switch msg.Type { switch msg.Type {
case Resize: case Resize:
var winSize WindowSize decodeString, err := base64.StdEncoding.DecodeString(msg.Content)
err = json.Unmarshal([]byte(msg.Content), &winSize)
if err != nil { if err != nil {
log.Warnf("解析SSH会话窗口大小失败: %v", err) log.Warnf("Base64解码失败: %v原始字符串%v", err, msg.Content)
continue
}
var winSize WindowSize
err = json.Unmarshal(decodeString, &winSize)
if err != nil {
log.Warnf("解析SSH会话窗口大小失败: %v原始字符串%v", err, msg.Content)
continue continue
} }
if err := nextTerminal.WindowChange(winSize.Rows, winSize.Cols); err != nil { if err := nextTerminal.WindowChange(winSize.Rows, winSize.Cols); err != nil {
log.Warnf("更改SSH会话窗口大小失败: %v", err) log.Warnf("更改SSH会话窗口大小失败: %v", err)
continue
} }
_ = sessionRepository.UpdateWindowSizeById(winSize.Rows, winSize.Cols, sessionId)
case Data: case Data:
_, err = nextTerminal.Write([]byte(msg.Content)) input := []byte(msg.Content)
//hexInput := hex.EncodeToString(input)
//switch hexInput {
//case "0d": // 回车
// DealCommand(enterKeys)
// // 清空输入的字符
// enterKeys = enterKeys[:0]
// enterIndex = 0
//case "7f": // backspace
// enterIndex--
// if enterIndex < 0 {
// enterIndex = 0
// }
// temp := enterKeys[:enterIndex]
// if len(enterKeys) > enterIndex {
// enterKeys = append(temp, enterKeys[enterIndex+1:]...)
// } else {
// enterKeys = temp
// }
//case "1b5b337e": // del
// temp := enterKeys[:enterIndex]
// if len(enterKeys) > enterIndex {
// enterKeys = append(temp, enterKeys[enterIndex+1:]...)
// } else {
// enterKeys = temp
// }
// enterIndex--
// if enterIndex < 0 {
// enterIndex = 0
// }
//case "1b5b41":
//case "1b5b42":
// break
//case "1b5b43": // ->
// enterIndex++
// if enterIndex > len(enterKeys) {
// enterIndex = len(enterKeys)
// }
//case "1b5b44": // <-
// enterIndex--
// if enterIndex < 0 {
// enterIndex = 0
// }
//default:
// enterKeys = utils.InsertSlice(enterIndex, []rune(msg.Content), enterKeys)
// enterIndex++
//}
_, err := nextTerminal.Write(input)
if err != nil { if err != nil {
log.Debugf("SSH会话写入失败: %v", err) CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
msg := Message{ }
Type: Closed, case Ping:
Content: "the remote connection is closed.", _, _, err := nextTerminal.SshClient.Conn.SendRequest("helloworld1024@foxmail.com", true, nil)
} if err != nil {
_ = WriteMessage(ws, msg) CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
} else {
_ = WriteMessage(ws, NewMessage(Ping, ""))
} }
}
}
} }
return err return err
} }
func ReadMessage(nextTerminal *term.NextTerminal, quitChan chan bool, ws *websocket.Conn) { func permissionCheck(c echo.Context, assetId string) error {
user, _ := GetCurrentAccount(c)
if constant.TypeUser == user.Type {
// 检测是否有访问权限
assetIds, err := resourceSharerRepository.FindAssetIdsByUserId(user.ID)
if err != nil {
return err
}
var quit bool if !utils.Contains(assetIds, assetId) {
for { return errors.New("您没有权限访问此资产")
select {
case quit = <-quitChan:
if quit {
return
}
default:
p, n, err := nextTerminal.Read()
if err != nil {
msg := Message{
Type: Closed,
Content: err.Error(),
}
_ = WriteMessage(ws, msg)
}
if n > 0 {
s := string(p)
msg := Message{
Type: Data,
Content: s,
}
_ = WriteMessage(ws, msg)
}
time.Sleep(time.Duration(10) * time.Millisecond)
} }
} }
return nil
} }
func WriteMessage(ws *websocket.Conn, msg Message) error { func WriteMessage(ws *websocket.Conn, msg Message) error {
message, err := json.Marshal(msg) message := []byte(msg.ToString())
if err != nil { return ws.WriteMessage(websocket.TextMessage, message)
return err
}
WriteByteMessage(ws, message)
return err
}
func WriteByteMessage(ws *websocket.Conn, p []byte) {
err := ws.WriteMessage(websocket.TextMessage, p)
if err != nil {
log.Debugf("write: %v", err)
}
} }
func CreateNextTerminalBySession(session model.Session) (*term.NextTerminal, error) { func CreateNextTerminalBySession(session model.Session) (*term.NextTerminal, error) {
@ -260,5 +376,46 @@ func CreateNextTerminalBySession(session model.Session) (*term.NextTerminal, err
ip = session.IP ip = session.IP
port = session.Port port = session.Port
) )
return term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, 10, 10, "") return term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, 10, 10, "", "", false)
}
func SshMonitor(c echo.Context) error {
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
if err != nil {
log.Errorf("升级为WebSocket协议失败%v", err.Error())
return err
}
defer ws.Close()
sessionId := c.QueryParam("sessionId")
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return WriteMessage(ws, NewMessage(Closed, "获取会话失败"))
}
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return WriteMessage(ws, NewMessage(Closed, "会话已离线"))
}
obId := utils.UUID()
obSession := &session.Session{
ID: obId,
Protocol: s.Protocol,
Mode: s.Mode,
WebSocket: ws,
}
nextSession.Observer.Add <- obSession
log.Debugf("会话 %v 观察者 %v 进入", sessionId, obId)
for {
_, _, err := ws.ReadMessage()
if err != nil {
log.Debugf("会话 %v 观察者 %v 退出", sessionId, obId)
nextSession.Observer.Del <- obId
break
}
}
return nil
} }

View File

@ -0,0 +1,6 @@
package api
func DealCommand(enterKeys []rune) {
println(string(enterKeys))
}

468
server/api/sshd.go Normal file
View File

@ -0,0 +1,468 @@
package api
import (
"encoding/hex"
"errors"
"fmt"
"io"
"path"
"strings"
"time"
"next-terminal/server/config"
"next-terminal/server/constant"
"next-terminal/server/global/cache"
"next-terminal/server/global/session"
"next-terminal/server/guacd"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/term"
"next-terminal/server/totp"
"next-terminal/server/utils"
"github.com/gliderlabs/ssh"
"github.com/manifoldco/promptui"
"gorm.io/gorm"
)
func sessionHandler(sess *ssh.Session) {
defer func() {
(*sess).Close()
}()
username := (*sess).User()
remoteAddr := strings.Split((*sess).RemoteAddr().String(), ":")[0]
user, err := userRepository.FindByUsername(username)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
_, _ = io.WriteString(*sess, "您输入的账户或密码不正确.\n")
} else {
_, _ = io.WriteString(*sess, err.Error())
}
return
}
// 判断是否需要进行双因素认证
if user.TOTPSecret != "" && user.TOTPSecret != "-" {
totpUI(sess, user, remoteAddr, username)
} else {
// 保存登录日志
_ = SaveLoginLog(remoteAddr, "terminal", username, true, false, utils.UUID(), "")
mainUI(sess, user)
}
}
func totpUI(sess *ssh.Session, user model.User, remoteAddr string, username string) {
validate := func(input string) error {
if len(input) < 6 {
return errors.New("双因素认证授权码必须为6个数字")
}
return nil
}
prompt := promptui.Prompt{
Label: "请输入双因素认证授权码",
Validate: validate,
Mask: '*',
Stdin: *sess,
Stdout: *sess,
}
var success = false
for i := 0; i < 5; i++ {
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
loginFailCountKey := remoteAddr + username
v, ok := cache.GlobalCache.Get(loginFailCountKey)
if !ok {
v = 1
}
count := v.(int)
if count >= 5 {
_, _ = io.WriteString(*sess, "登录失败次数过多请等待30秒后再试\r\n")
continue
}
if !totp.Validate(result, user.TOTPSecret) {
count++
println(count)
cache.GlobalCache.Set(loginFailCountKey, count, time.Second*time.Duration(30))
// 保存登录日志
_ = SaveLoginLog(remoteAddr, "terminal", username, false, false, "", "双因素认证授权码不正确")
_, _ = io.WriteString(*sess, "您输入的双因素认证授权码不匹配\r\n")
continue
}
success = true
break
}
if success {
// 保存登录日志
_ = SaveLoginLog(remoteAddr, "terminal", username, true, false, utils.UUID(), "")
mainUI(sess, user)
}
}
func mainUI(sess *ssh.Session, user model.User) {
prompt := promptui.Select{
Label: "欢迎使用 Next Terminal请选择您要使用的功能",
Items: []string{"我的资产", "退出系统"},
Stdin: *sess,
Stdout: *sess,
}
MainLoop:
for {
_, result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
switch result {
case "我的资产":
AssetUI(sess, user)
case "退出系统":
break MainLoop
}
}
}
func AssetUI(sess *ssh.Session, user model.User) {
assets, err := assetRepository.FindByProtocolAndUser(constant.SSH, user)
if err != nil {
return
}
quitItem := model.Asset{ID: "quit", Name: "返回上级菜单", Description: "这里是返回上级菜单的选项"}
assets = append([]model.Asset{quitItem}, assets...)
templates := &promptui.SelectTemplates{
Label: "{{ . }}?",
Active: "\U0001F336 {{ .Name | cyan }} ({{ .IP | red }}:{{ .Port | red }})",
Inactive: " {{ .Name | cyan }} ({{ .IP | red }}:{{ .Port | red }})",
Selected: "\U0001F336 {{ .Name | red | cyan }}",
Details: `
--------- 详细信息 ----------
{{ "名称:" | faint }} {{ .Name }}
{{ "主机:" | faint }} {{ .IP }}
{{ "端口:" | faint }} {{ .Port }}
{{ "标签:" | faint }} {{ .Tags }}
{{ "备注:" | faint }} {{ .Description }}
`,
}
searcher := func(input string, index int) bool {
asset := assets[index]
name := strings.Replace(strings.ToLower(asset.Name), " ", "", -1)
input = strings.Replace(strings.ToLower(input), " ", "", -1)
return strings.Contains(name, input)
}
prompt := promptui.Select{
Label: "请选择您要访问的资产",
Items: assets,
Templates: templates,
Size: 4,
Searcher: searcher,
Stdin: *sess,
Stdout: *sess,
}
AssetUILoop:
for {
i, _, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
chooseAssetId := assets[i].ID
switch chooseAssetId {
case "quit":
break AssetUILoop
default:
if err := createSession(sess, assets[i].ID, user.ID); err != nil {
_, _ = io.WriteString(*sess, err.Error()+"\r\n")
return
}
}
}
}
func createSession(sess *ssh.Session, assetId, creator string) (err error) {
asset, err := assetRepository.FindById(assetId)
if err != nil {
return err
}
ClientIP := strings.Split((*sess).RemoteAddr().String(), ":")[0]
s := &model.Session{
ID: utils.UUID(),
AssetId: asset.ID,
Username: asset.Username,
Password: asset.Password,
PrivateKey: asset.PrivateKey,
Passphrase: asset.Passphrase,
Protocol: asset.Protocol,
IP: asset.IP,
Port: asset.Port,
Status: constant.NoConnect,
Creator: creator,
ClientIP: ClientIP,
Mode: constant.Terminal,
Upload: "0",
Download: "0",
Delete: "0",
Rename: "0",
StorageId: "",
AccessGatewayId: asset.AccessGatewayId,
}
if asset.AccountType == "credential" {
credential, err := credentialRepository.FindById(asset.CredentialId)
if err != nil {
return nil
}
if credential.Type == constant.Custom {
s.Username = credential.Username
s.Password = credential.Password
} else {
s.Username = credential.Username
s.PrivateKey = credential.PrivateKey
s.Passphrase = credential.Passphrase
}
}
if err := sessionRepository.Create(s); err != nil {
return err
}
return handleAccessAsset(sess, s.ID)
}
func handleAccessAsset(sess *ssh.Session, sessionId string) (err error) {
s, err := sessionRepository.FindByIdAndDecrypt(sessionId)
if err != nil {
return err
}
var (
username = s.Username
password = s.Password
privateKey = s.PrivateKey
passphrase = s.Passphrase
ip = s.IP
port = s.Port
)
if s.AccessGatewayId != "" && s.AccessGatewayId != "-" {
g, err := accessGatewayService.GetGatewayAndReconnectById(s.AccessGatewayId)
if err != nil {
return errors.New("获取接入网关失败:" + err.Error())
}
if !g.Connected {
return errors.New("接入网关不可用:" + g.Message)
}
exposedIP, exposedPort, err := g.OpenSshTunnel(s.ID, ip, port)
if err != nil {
return errors.New("开启SSH隧道失败" + err.Error())
}
defer g.CloseSshTunnel(s.ID)
ip = exposedIP
port = exposedPort
}
pty, winCh, isPty := (*sess).Pty()
if !isPty {
return errors.New("No PTY requested.\n")
}
recording := ""
property, err := propertyRepository.FindByName(guacd.EnableRecording)
if err == nil && property.Value == "true" {
recording = path.Join(config.GlobalCfg.Guacd.Recording, sessionId, "recording.cast")
}
nextTerminal, err := term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, pty.Window.Height, pty.Window.Width, recording, pty.Term, false)
if err != nil {
return err
}
sshSession := nextTerminal.SshSession
writer := NewWriter(sessionId, sess, nextTerminal.Recorder)
sshSession.Stdout = writer
sshSession.Stdin = *sess
sshSession.Stderr = *sess
if err := nextTerminal.RequestPty(pty.Term, pty.Window.Height, pty.Window.Width); err != nil {
return err
}
if err := nextTerminal.Shell(); err != nil {
return err
}
go func() {
log.Debugf("开启窗口大小监控...")
for win := range winCh {
_ = sshSession.WindowChange(win.Height, win.Width)
}
log.Debugf("退出窗口大小监控")
// ==== 修改数据库中的会话状态为已断开,修复用户直接关闭窗口时会话状态不正确的问题 ====
CloseSessionById(sessionId, Normal, "用户正常退出")
// ==== 修改数据库中的会话状态为已断开,修复用户直接关闭窗口时会话状态不正确的问题 ====
}()
// ==== 修改数据库中的会话状态为已连接 ====
sessionForUpdate := model.Session{}
sessionForUpdate.ID = sessionId
sessionForUpdate.Status = constant.Connected
sessionForUpdate.Recording = recording
sessionForUpdate.ConnectedTime = utils.NowJsonTime()
if err := sessionRepository.UpdateById(&sessionForUpdate, sessionId); err != nil {
return err
}
// ==== 修改数据库中的会话状态为已连接 ====
nextSession := &session.Session{
ID: s.ID,
Protocol: s.Protocol,
Mode: s.Mode,
NextTerminal: nextTerminal,
Observer: session.NewObserver(s.ID),
}
go nextSession.Observer.Run()
session.GlobalSessionManager.Add <- nextSession
if err := sshSession.Wait(); err != nil {
return err
}
// ==== 修改数据库中的会话状态为已断开 ====
CloseSessionById(sessionId, Normal, "用户正常退出")
// ==== 修改数据库中的会话状态为已断开 ====
return nil
}
func passwordAuth(ctx ssh.Context, pass string) bool {
username := ctx.User()
remoteAddr := strings.Split(ctx.RemoteAddr().String(), ":")[0]
user, err := userRepository.FindByUsername(username)
if err != nil {
// 保存登录日志
_ = SaveLoginLog(remoteAddr, "terminal", username, false, false, "", "账号或密码不正确")
return false
}
if err := utils.Encoder.Match([]byte(user.Password), []byte(pass)); err != nil {
// 保存登录日志
_ = SaveLoginLog(remoteAddr, "terminal", username, false, false, "", "账号或密码不正确")
return false
}
return true
}
func Setup() {
ssh.Handle(func(s ssh.Session) {
_, _ = io.WriteString(s, fmt.Sprintf(constant.Banner, constant.Version))
defer func() {
if e, ok := recover().(error); ok {
log.Fatal(e)
}
}()
sessionHandler(&s)
})
fmt.Printf("⇨ sshd server started on %v\n", config.GlobalCfg.Sshd.Addr)
err := ssh.ListenAndServe(
config.GlobalCfg.Sshd.Addr,
nil,
ssh.PasswordAuth(passwordAuth),
ssh.HostKeyFile(config.GlobalCfg.Sshd.Key),
)
log.Fatal(fmt.Sprintf("启动sshd服务失败: %v", err.Error()))
}
func init() {
if config.GlobalCfg.Sshd.Enable {
go Setup()
}
}
type Writer struct {
sessionId string
sess *ssh.Session
recorder *term.Recorder
rz bool
sz bool
}
func NewWriter(sessionId string, sess *ssh.Session, recorder *term.Recorder) *Writer {
return &Writer{sessionId: sessionId, sess: sess, recorder: recorder}
}
func (w *Writer) Write(p []byte) (n int, err error) {
if w.recorder != nil {
s := string(p)
if !w.sz && !w.rz {
// rz的开头字符
hexData := hex.EncodeToString(p)
if strings.Contains(hexData, "727a0d2a2a184230303030303030303030303030300d8a11") {
w.sz = true
} else if strings.Contains(hexData, "727a2077616974696e6720746f20726563656976652e2a2a184230313030303030303233626535300d8a11") {
w.rz = true
}
}
if w.sz {
// sz 会以 OO 结尾
if "OO" == s {
w.sz = false
}
} else if w.rz {
// rz 最后会显示 Received /home/xxx
if strings.Contains(s, "Received") {
w.rz = false
// 把上传的文件名称也显示一下
err := w.recorder.WriteData(s)
if err != nil {
return 0, err
}
sendObData(w.sessionId, s)
}
} else {
err := w.recorder.WriteData(s)
if err != nil {
return 0, err
}
sendObData(w.sessionId, s)
}
}
return (*w.sess).Write(p)
}
func sendObData(sessionId, s string) {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession != nil {
if nextSession.Observer != nil {
obs := nextSession.Observer.All()
for _, ob := range obs {
_ = WriteMessage(ob.WebSocket, NewMessage(Data, s))
}
}
}
}

384
server/api/stats.go Normal file
View File

@ -0,0 +1,384 @@
package api
import (
"bufio"
"fmt"
"strconv"
"strings"
"time"
"next-terminal/server/utils"
"golang.org/x/crypto/ssh"
)
type FileSystem struct {
MountPoint string `json:"mountPoint"`
Used uint64 `json:"used"`
Free uint64 `json:"free"`
}
type Network struct {
IPv4 string `json:"ipv4"`
IPv6 string `json:"ipv6"`
Rx uint64 `json:"rx"`
Tx uint64 `json:"tx"`
}
type cpuRaw struct {
User uint64 // time spent in user mode
Nice uint64 // time spent in user mode with low priority (nice)
System uint64 // time spent in system mode
Idle uint64 // time spent in the idle task
Iowait uint64 // time spent waiting for I/O to complete (since Linux 2.5.41)
Irq uint64 // time spent servicing interrupts (since 2.6.0-test4)
SoftIrq uint64 // time spent servicing softirqs (since 2.6.0-test4)
Steal uint64 // time spent in other OSes when running in a virtualized environment
Guest uint64 // time spent running a virtual CPU for guest operating systems under the control of the Linux kernel.
Total uint64 // total of all time fields
}
type CPU struct {
User float32 `json:"user"`
Nice float32 `json:"nice"`
System float32 `json:"system"`
Idle float32 `json:"idle"`
IOWait float32 `json:"ioWait"`
Irq float32 `json:"irq"`
SoftIrq float32 `json:"softIrq"`
Steal float32 `json:"steal"`
Guest float32 `json:"guest"`
}
type Stat struct {
Uptime int64 `json:"uptime"`
Hostname string `json:"hostname"`
Load1 string `json:"load1"`
Load5 string `json:"load5"`
Load10 string `json:"load10"`
RunningProcess string `json:"runningProcess"`
TotalProcess string `json:"totalProcess"`
MemTotal uint64 `json:"memTotal"`
MemAvailable uint64 `json:"memAvailable"`
MemFree uint64 `json:"memFree"`
MemBuffers uint64 `json:"memBuffers"`
MemCached uint64 `json:"memCached"`
SwapTotal uint64 `json:"swapTotal"`
SwapFree uint64 `json:"swapFree"`
FileSystems []FileSystem `json:"fileSystems"`
Network map[string]Network `json:"network"`
CPU CPU `json:"cpu"`
}
func GetAllStats(client *ssh.Client) (*Stat, error) {
start := time.Now()
stats := &Stat{}
if err := getUptime(client, stats); err != nil {
return nil, err
}
if err := getHostname(client, stats); err != nil {
return nil, err
}
if err := getLoad(client, stats); err != nil {
return nil, err
}
if err := getMem(client, stats); err != nil {
return nil, err
}
if err := getFileSystems(client, stats); err != nil {
return nil, err
}
if err := getInterfaces(client, stats); err != nil {
return nil, err
}
if err := getInterfaceInfo(client, stats); err != nil {
return nil, err
}
if err := getCPU(client, stats); err != nil {
return nil, err
}
cost := time.Since(start)
fmt.Printf("%s: %v\n", "GetAllStats", cost)
return stats, nil
}
func getHostname(client *ssh.Client, stat *Stat) (err error) {
//defer utils.TimeWatcher("getHostname")
hostname, err := utils.RunCommand(client, "/bin/hostname -f")
if err != nil {
return
}
stat.Hostname = strings.TrimSpace(hostname)
return
}
func getUptime(client *ssh.Client, stat *Stat) (err error) {
//defer utils.TimeWatcher("getUptime")
uptime, err := utils.RunCommand(client, "/bin/cat /proc/uptime")
if err != nil {
return
}
parts := strings.Fields(uptime)
if len(parts) == 2 {
var upSeconds float64
upSeconds, err = strconv.ParseFloat(parts[0], 64)
if err != nil {
return
}
stat.Uptime = int64(upSeconds * 1000)
}
return
}
func getLoad(client *ssh.Client, stat *Stat) (err error) {
//defer utils.TimeWatcher("getLoad")
line, err := utils.RunCommand(client, "/bin/cat /proc/loadavg")
if err != nil {
return
}
parts := strings.Fields(line)
if len(parts) == 5 {
stat.Load1 = parts[0]
stat.Load5 = parts[1]
stat.Load10 = parts[2]
if i := strings.Index(parts[3], "/"); i != -1 {
stat.RunningProcess = parts[3][0:i]
if i+1 < len(parts[3]) {
stat.TotalProcess = parts[3][i+1:]
}
}
}
return
}
func getMem(client *ssh.Client, stat *Stat) (err error) {
//defer utils.TimeWatcher("getMem")
lines, err := utils.RunCommand(client, "/bin/cat /proc/meminfo")
if err != nil {
return
}
scanner := bufio.NewScanner(strings.NewReader(lines))
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) == 3 {
val, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
continue
}
val *= 1024
switch parts[0] {
case "MemTotal:":
stat.MemTotal = val
case "MemFree:":
stat.MemFree = val
case "MemAvailable:":
stat.MemAvailable = val
case "Buffers:":
stat.MemBuffers = val
case "Cached:":
stat.MemCached = val
case "SwapTotal:":
stat.SwapTotal = val
case "SwapFree:":
stat.SwapFree = val
}
}
}
return
}
func getFileSystems(client *ssh.Client, stat *Stat) (err error) {
//defer utils.TimeWatcher("getFileSystems")
lines, err := utils.RunCommand(client, "/bin/df -B1")
if err != nil {
return
}
scanner := bufio.NewScanner(strings.NewReader(lines))
flag := 0
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
n := len(parts)
dev := n > 0 && strings.Index(parts[0], "/dev/") == 0
if n == 1 && dev {
flag = 1
} else if (n == 5 && flag == 1) || (n == 6 && dev) {
i := flag
flag = 0
used, err := strconv.ParseUint(parts[2-i], 10, 64)
if err != nil {
continue
}
free, err := strconv.ParseUint(parts[3-i], 10, 64)
if err != nil {
continue
}
stat.FileSystems = append(stat.FileSystems, FileSystem{
parts[5-i], used, free,
})
}
}
return
}
func getInterfaces(client *ssh.Client, stats *Stat) (err error) {
//defer utils.TimeWatcher("getInterfaces")
var lines string
lines, err = utils.RunCommand(client, "/bin/ip -o addr")
if err != nil {
// try /sbin/ip
lines, err = utils.RunCommand(client, "/sbin/ip -o addr")
if err != nil {
return
}
}
if stats.Network == nil {
stats.Network = make(map[string]Network)
}
scanner := bufio.NewScanner(strings.NewReader(lines))
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) >= 4 && (parts[2] == "inet" || parts[2] == "inet6") {
ipv4 := parts[2] == "inet"
intfname := parts[1]
if info, ok := stats.Network[intfname]; ok {
if ipv4 {
info.IPv4 = parts[3]
} else {
info.IPv6 = parts[3]
}
stats.Network[intfname] = info
} else {
info := Network{}
if ipv4 {
info.IPv4 = parts[3]
} else {
info.IPv6 = parts[3]
}
stats.Network[intfname] = info
}
}
}
return
}
func getInterfaceInfo(client *ssh.Client, stats *Stat) (err error) {
//defer utils.TimeWatcher("getInterfaceInfo")
lines, err := utils.RunCommand(client, "/bin/cat /proc/net/dev")
if err != nil {
return
}
if stats.Network == nil {
return
} // should have been here already
scanner := bufio.NewScanner(strings.NewReader(lines))
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) == 17 {
intf := strings.TrimSpace(parts[0])
intf = strings.TrimSuffix(intf, ":")
if info, ok := stats.Network[intf]; ok {
rx, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
continue
}
tx, err := strconv.ParseUint(parts[9], 10, 64)
if err != nil {
continue
}
info.Rx = rx
info.Tx = tx
stats.Network[intf] = info
}
}
}
return
}
func parseCPUFields(fields []string, stat *cpuRaw) {
numFields := len(fields)
for i := 1; i < numFields; i++ {
val, err := strconv.ParseUint(fields[i], 10, 64)
if err != nil {
continue
}
stat.Total += val
switch i {
case 1:
stat.User = val
case 2:
stat.Nice = val
case 3:
stat.System = val
case 4:
stat.Idle = val
case 5:
stat.Iowait = val
case 6:
stat.Irq = val
case 7:
stat.SoftIrq = val
case 8:
stat.Steal = val
case 9:
stat.Guest = val
}
}
}
// the CPU stats that were fetched last time round
var preCPU cpuRaw
func getCPU(client *ssh.Client, stats *Stat) (err error) {
//defer utils.TimeWatcher("getCPU")
lines, err := utils.RunCommand(client, "/bin/cat /proc/stat")
if err != nil {
return
}
var (
nowCPU cpuRaw
total float32
)
scanner := bufio.NewScanner(strings.NewReader(lines))
for scanner.Scan() {
line := scanner.Text()
fields := strings.Fields(line)
if len(fields) > 0 && fields[0] == "cpu" { // changing here if want to get every cpu-core's stats
parseCPUFields(fields, &nowCPU)
break
}
}
if preCPU.Total == 0 { // having no pre raw cpu data
goto END
}
total = float32(nowCPU.Total - preCPU.Total)
stats.CPU.User = float32(nowCPU.User-preCPU.User) / total * 100
stats.CPU.Nice = float32(nowCPU.Nice-preCPU.Nice) / total * 100
stats.CPU.System = float32(nowCPU.System-preCPU.System) / total * 100
stats.CPU.Idle = float32(nowCPU.Idle-preCPU.Idle) / total * 100
stats.CPU.IOWait = float32(nowCPU.Iowait-preCPU.Iowait) / total * 100
stats.CPU.Irq = float32(nowCPU.Irq-preCPU.Irq) / total * 100
stats.CPU.SoftIrq = float32(nowCPU.SoftIrq-preCPU.SoftIrq) / total * 100
stats.CPU.Guest = float32(nowCPU.Guest-preCPU.Guest) / total * 100
END:
preCPU = nowCPU
return
}

353
server/api/storage.go Normal file
View File

@ -0,0 +1,353 @@
package api
import (
"bufio"
"errors"
"io"
"mime/multipart"
"os"
"path"
"strconv"
"strings"
"next-terminal/server/constant"
"next-terminal/server/model"
"next-terminal/server/utils"
"github.com/labstack/echo/v4"
)
func StoragePagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
order := c.QueryParam("order")
field := c.QueryParam("field")
items, total, err := storageRepository.Find(pageIndex, pageSize, name, order, field)
if err != nil {
return err
}
drivePath := storageService.GetBaseDrivePath()
for i := range items {
item := items[i]
dirSize, err := utils.DirSize(path.Join(drivePath, item.ID))
if err != nil {
items[i].UsedSize = -1
} else {
items[i].UsedSize = dirSize
}
}
return Success(c, H{
"total": total,
"items": items,
})
}
func StorageCreateEndpoint(c echo.Context) error {
var item model.Storage
if err := c.Bind(&item); err != nil {
return err
}
account, _ := GetCurrentAccount(c)
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
item.Owner = account.ID
// 创建对应的目录文件夹
drivePath := storageService.GetBaseDrivePath()
if err := os.MkdirAll(path.Join(drivePath, item.ID), os.ModePerm); err != nil {
return err
}
if err := storageRepository.Create(&item); err != nil {
return err
}
return Success(c, "")
}
func StorageUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.Storage
if err := c.Bind(&item); err != nil {
return err
}
drivePath := storageService.GetBaseDrivePath()
dirSize, err := utils.DirSize(path.Join(drivePath, item.ID))
if err != nil {
return err
}
if item.LimitSize > 0 && item.LimitSize < dirSize {
// 不能小于已使用的大小
return errors.New("空间大小不能小于已使用大小")
}
storage, err := storageRepository.FindById(id)
if err != nil {
return err
}
storage.Name = item.Name
storage.LimitSize = item.LimitSize
storage.IsShare = item.IsShare
if err := storageRepository.UpdateById(&storage, id); err != nil {
return err
}
return Success(c, "")
}
func StorageGetEndpoint(c echo.Context) error {
storageId := c.Param("id")
storage, err := storageRepository.FindById(storageId)
if err != nil {
return err
}
structMap := utils.StructToMap(storage)
drivePath := storageService.GetBaseDrivePath()
dirSize, err := utils.DirSize(path.Join(drivePath, storageId))
if err != nil {
structMap["usedSize"] = -1
} else {
structMap["usedSize"] = dirSize
}
return Success(c, structMap)
}
func StorageSharesEndpoint(c echo.Context) error {
storages, err := storageRepository.FindShares()
if err != nil {
return err
}
return Success(c, storages)
}
func StorageDeleteEndpoint(c echo.Context) error {
ids := c.Param("id")
split := strings.Split(ids, ",")
for i := range split {
id := split[i]
if err := storageService.DeleteStorageById(id, false); err != nil {
return err
}
}
return Success(c, nil)
}
func PermissionCheck(c echo.Context, id string) error {
storage, err := storageRepository.FindById(id)
if err != nil {
return err
}
account, _ := GetCurrentAccount(c)
if account.Type != constant.TypeAdmin {
if storage.Owner != account.ID {
return errors.New("您没有权限访问此地址 :(")
}
}
return nil
}
func StorageLsEndpoint(c echo.Context) error {
storageId := c.Param("storageId")
if err := PermissionCheck(c, storageId); err != nil {
return err
}
remoteDir := c.FormValue("dir")
return StorageLs(c, remoteDir, storageId)
}
func StorageLs(c echo.Context, remoteDir, storageId string) error {
drivePath := storageService.GetBaseDrivePath()
if strings.Contains(remoteDir, "../") {
return Fail(c, -1, "非法请求 :(")
}
files, err := storageService.Ls(path.Join(drivePath, storageId), remoteDir)
if err != nil {
return err
}
return Success(c, files)
}
func StorageDownloadEndpoint(c echo.Context) error {
storageId := c.Param("storageId")
if err := PermissionCheck(c, storageId); err != nil {
return err
}
remoteFile := c.QueryParam("file")
return StorageDownload(c, remoteFile, storageId)
}
func StorageDownload(c echo.Context, remoteFile, storageId string) error {
drivePath := storageService.GetBaseDrivePath()
if strings.Contains(remoteFile, "../") {
return Fail(c, -1, "非法请求 :(")
}
// 获取带后缀的文件名称
filenameWithSuffix := path.Base(remoteFile)
return c.Attachment(path.Join(path.Join(drivePath, storageId), remoteFile), filenameWithSuffix)
}
func StorageUploadEndpoint(c echo.Context) error {
storageId := c.Param("storageId")
if err := PermissionCheck(c, storageId); err != nil {
return err
}
file, err := c.FormFile("file")
if err != nil {
return err
}
return StorageUpload(c, file, storageId)
}
func StorageUpload(c echo.Context, file *multipart.FileHeader, storageId string) error {
drivePath := storageService.GetBaseDrivePath()
storage, _ := storageRepository.FindById(storageId)
if storage.LimitSize > 0 {
dirSize, err := utils.DirSize(path.Join(drivePath, storageId))
if err != nil {
return err
}
if dirSize+file.Size > storage.LimitSize {
return errors.New("可用空间不足")
}
}
filename := file.Filename
src, err := file.Open()
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
remoteFile := path.Join(remoteDir, filename)
if strings.Contains(remoteDir, "../") {
return Fail(c, -1, "非法请求 :(")
}
if strings.Contains(remoteFile, "../") {
return Fail(c, -1, "非法请求 :(")
}
// 判断文件夹不存在时自动创建
dir := path.Join(path.Join(drivePath, storageId), remoteDir)
if !utils.FileExists(dir) {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
}
// Destination
dst, err := os.Create(path.Join(path.Join(drivePath, storageId), remoteFile))
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
return err
}
return Success(c, nil)
}
func StorageMkDirEndpoint(c echo.Context) error {
storageId := c.Param("storageId")
if err := PermissionCheck(c, storageId); err != nil {
return err
}
remoteDir := c.QueryParam("dir")
return StorageMkDir(c, remoteDir, storageId)
}
func StorageMkDir(c echo.Context, remoteDir, storageId string) error {
drivePath := storageService.GetBaseDrivePath()
if strings.Contains(remoteDir, "../") {
return Fail(c, -1, ":) 非法请求")
}
if err := os.MkdirAll(path.Join(path.Join(drivePath, storageId), remoteDir), os.ModePerm); err != nil {
return err
}
return Success(c, nil)
}
func StorageRmEndpoint(c echo.Context) error {
storageId := c.Param("storageId")
if err := PermissionCheck(c, storageId); err != nil {
return err
}
// 文件夹或者文件
file := c.FormValue("file")
return StorageRm(c, file, storageId)
}
func StorageRm(c echo.Context, file, storageId string) error {
drivePath := storageService.GetBaseDrivePath()
if strings.Contains(file, "../") {
return Fail(c, -1, ":) 非法请求")
}
if err := os.RemoveAll(path.Join(path.Join(drivePath, storageId), file)); err != nil {
return err
}
return Success(c, nil)
}
func StorageRenameEndpoint(c echo.Context) error {
storageId := c.Param("storageId")
if err := PermissionCheck(c, storageId); err != nil {
return err
}
oldName := c.QueryParam("oldName")
newName := c.QueryParam("newName")
return StorageRename(c, oldName, newName, storageId)
}
func StorageRename(c echo.Context, oldName, newName, storageId string) error {
drivePath := storageService.GetBaseDrivePath()
if strings.Contains(oldName, "../") {
return Fail(c, -1, ":) 非法请求")
}
if strings.Contains(newName, "../") {
return Fail(c, -1, ":) 非法请求")
}
if err := os.Rename(path.Join(path.Join(drivePath, storageId), oldName), path.Join(path.Join(drivePath, storageId), newName)); err != nil {
return err
}
return Success(c, nil)
}
func StorageEditEndpoint(c echo.Context) error {
storageId := c.Param("storageId")
if err := PermissionCheck(c, storageId); err != nil {
return err
}
file := c.Param("file")
fileContent := c.Param("fileContent")
return StorageEdit(c, file, fileContent, storageId)
}
func StorageEdit(c echo.Context, file string, fileContent string, storageId string) error {
drivePath := storageService.GetBaseDrivePath()
if strings.Contains(file, "../") {
return Fail(c, -1, ":) 非法请求")
}
realFilePath := path.Join(path.Join(drivePath, storageId), file)
dstFile, err := os.OpenFile(realFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return err
}
defer dstFile.Close()
write := bufio.NewWriter(dstFile)
if _, err := write.WriteString(fileContent); err != nil {
return err
}
if err := write.Flush(); err != nil {
return err
}
return Success(c, nil)
}

77
server/api/strategy.go Normal file
View File

@ -0,0 +1,77 @@
package api
import (
"strconv"
"strings"
"next-terminal/server/model"
"next-terminal/server/utils"
"github.com/labstack/echo/v4"
)
func StrategyAllEndpoint(c echo.Context) error {
items, err := strategyRepository.FindAll()
if err != nil {
return err
}
return Success(c, items)
}
func StrategyPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
order := c.QueryParam("order")
field := c.QueryParam("field")
items, total, err := strategyRepository.Find(pageIndex, pageSize, name, order, field)
if err != nil {
return err
}
return Success(c, H{
"total": total,
"items": items,
})
}
func StrategyCreateEndpoint(c echo.Context) error {
var item model.Strategy
if err := c.Bind(&item); err != nil {
return err
}
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := strategyRepository.Create(&item); err != nil {
return err
}
return Success(c, "")
}
func StrategyDeleteEndpoint(c echo.Context) error {
ids := c.Param("id")
split := strings.Split(ids, ",")
for i := range split {
id := split[i]
if err := strategyRepository.DeleteById(id); err != nil {
return err
}
}
return Success(c, nil)
}
func StrategyUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.Strategy
if err := c.Bind(&item); err != nil {
return err
}
if err := strategyRepository.UpdateById(&item, id); err != nil {
return err
}
return Success(c, "")
}

66
server/api/test/test.go Normal file
View File

@ -0,0 +1,66 @@
package main
import (
"fmt"
"strings"
"github.com/manifoldco/promptui"
)
type pepper struct {
Name string
HeatUnit int
Peppers int
}
func main() {
peppers := []pepper{
{Name: "Bell Pepper", HeatUnit: 0, Peppers: 0},
{Name: "Banana Pepper", HeatUnit: 100, Peppers: 1},
{Name: "Poblano", HeatUnit: 1000, Peppers: 2},
{Name: "Jalapeño", HeatUnit: 3500, Peppers: 3},
{Name: "Aleppo", HeatUnit: 10000, Peppers: 4},
{Name: "Tabasco", HeatUnit: 30000, Peppers: 5},
{Name: "Malagueta", HeatUnit: 50000, Peppers: 6},
{Name: "Habanero", HeatUnit: 100000, Peppers: 7},
{Name: "Red Savina Habanero", HeatUnit: 350000, Peppers: 8},
{Name: "Dragons Breath", HeatUnit: 855000, Peppers: 9},
}
templates := &promptui.SelectTemplates{
Label: "{{ . }}?",
Active: "\U0001F336 {{ .Name | cyan }} ({{ .HeatUnit | red }})",
Inactive: " {{ .Name | cyan }} ({{ .HeatUnit | red }})",
Selected: "\U0001F336 {{ .Name | red | cyan }}",
Details: `
--------- Pepper ----------
{{ "Name:" | faint }} {{ .Name }}/
{{ "Heat Unit:" | faint }} {{ .HeatUnit }}
{{ "Peppers:" | faint }} {{ .Peppers }}`,
}
searcher := func(input string, index int) bool {
pepper := peppers[index]
name := strings.Replace(strings.ToLower(pepper.Name), " ", "", -1)
input = strings.Replace(strings.ToLower(input), " ", "", -1)
return strings.Contains(name, input)
}
prompt := promptui.Select{
Label: "Spicy Level",
Items: peppers,
Templates: templates,
Size: 4,
Searcher: searcher,
}
i, _, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose number %d: %s\n", i+1, peppers[i].Name)
}

View File

@ -1,26 +1,34 @@
package api package api
import ( import (
"context"
"encoding/base64"
"errors" "errors"
"path" "path"
"strconv" "strconv"
"time"
"next-terminal/pkg/constant" "next-terminal/server/config"
"next-terminal/pkg/global" "next-terminal/server/constant"
"next-terminal/pkg/guacd" "next-terminal/server/global/session"
"next-terminal/pkg/log" "next-terminal/server/guacd"
"next-terminal/server/log"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/utils"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
const ( const (
TunnelClosed int = -1 TunnelClosed int = -1
Normal int = 0 Normal int = 0
NotFoundSession int = 800 NotFoundSession int = 800
NewTunnelError int = 801 NewTunnelError int = 801
ForcedDisconnect int = 802 ForcedDisconnect int = 802
AccessGatewayUnAvailable int = 803
AccessGatewayCreateError int = 804
AccessGatewayConnectError int = 804
) )
func TunEndpoint(c echo.Context) error { func TunEndpoint(c echo.Context) error {
@ -44,111 +52,63 @@ func TunEndpoint(c echo.Context) error {
propertyMap := propertyRepository.FindAllMap() propertyMap := propertyRepository.FindAllMap()
var session model.Session var s model.Session
if len(connectionId) > 0 { if len(connectionId) > 0 {
session, err = sessionRepository.FindByConnectionId(connectionId) s, err = sessionRepository.FindByConnectionId(connectionId)
if err != nil { if err != nil {
log.Warnf("会话不存在")
return err return err
} }
if session.Status != constant.Connected { if s.Status != constant.Connected {
log.Warnf("会话未在线")
return errors.New("会话未在线") return errors.New("会话未在线")
} }
configuration.ConnectionID = connectionId configuration.ConnectionID = connectionId
sessionId = session.ID sessionId = s.ID
configuration.SetParameter("width", strconv.Itoa(session.Width)) configuration.SetParameter("width", strconv.Itoa(s.Width))
configuration.SetParameter("height", strconv.Itoa(session.Height)) configuration.SetParameter("height", strconv.Itoa(s.Height))
configuration.SetParameter("dpi", "96") configuration.SetParameter("dpi", "96")
} else { } else {
configuration.SetParameter("width", width) configuration.SetParameter("width", width)
configuration.SetParameter("height", height) configuration.SetParameter("height", height)
configuration.SetParameter("dpi", dpi) configuration.SetParameter("dpi", dpi)
session, err = sessionRepository.FindByIdAndDecrypt(sessionId) s, err = sessionRepository.FindByIdAndDecrypt(sessionId)
if err != nil { if err != nil {
CloseSessionById(sessionId, NotFoundSession, "会话不存在")
return err return err
} }
setConfig(propertyMap, s, configuration)
if propertyMap[guacd.EnableRecording] == "true" { var (
configuration.SetParameter(guacd.RecordingPath, path.Join(propertyMap[guacd.RecordingPath], sessionId)) ip = s.IP
configuration.SetParameter(guacd.CreateRecordingPath, propertyMap[guacd.CreateRecordingPath]) port = s.Port
} else { )
configuration.SetParameter(guacd.RecordingPath, "") if s.AccessGatewayId != "" && s.AccessGatewayId != "-" {
} g, err := accessGatewayService.GetGatewayAndReconnectById(s.AccessGatewayId)
if err != nil {
configuration.Protocol = session.Protocol disconnect(ws, AccessGatewayUnAvailable, "获取接入网关失败:"+err.Error())
switch configuration.Protocol { return nil
case "rdp":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
configuration.SetParameter("security", "any")
configuration.SetParameter("ignore-cert", "true")
configuration.SetParameter("create-drive-path", "true")
configuration.SetParameter("resize-method", "reconnect")
configuration.SetParameter(guacd.EnableDrive, propertyMap[guacd.EnableDrive])
configuration.SetParameter(guacd.DriveName, propertyMap[guacd.DriveName])
configuration.SetParameter(guacd.DrivePath, propertyMap[guacd.DrivePath])
configuration.SetParameter(guacd.EnableWallpaper, propertyMap[guacd.EnableWallpaper])
configuration.SetParameter(guacd.EnableTheming, propertyMap[guacd.EnableTheming])
configuration.SetParameter(guacd.EnableFontSmoothing, propertyMap[guacd.EnableFontSmoothing])
configuration.SetParameter(guacd.EnableFullWindowDrag, propertyMap[guacd.EnableFullWindowDrag])
configuration.SetParameter(guacd.EnableDesktopComposition, propertyMap[guacd.EnableDesktopComposition])
configuration.SetParameter(guacd.EnableMenuAnimations, propertyMap[guacd.EnableMenuAnimations])
configuration.SetParameter(guacd.DisableBitmapCaching, propertyMap[guacd.DisableBitmapCaching])
configuration.SetParameter(guacd.DisableOffscreenCaching, propertyMap[guacd.DisableOffscreenCaching])
configuration.SetParameter(guacd.DisableGlyphCaching, propertyMap[guacd.DisableGlyphCaching])
case "ssh":
if len(session.PrivateKey) > 0 && session.PrivateKey != "-" {
configuration.SetParameter("username", session.Username)
configuration.SetParameter("private-key", session.PrivateKey)
configuration.SetParameter("passphrase", session.Passphrase)
} else {
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
} }
if !g.Connected {
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize]) disconnect(ws, AccessGatewayUnAvailable, "接入网关不可用:"+g.Message)
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName]) return nil
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme]) }
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace]) exposedIP, exposedPort, err := g.OpenSshTunnel(s.ID, ip, port)
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType]) if err != nil {
case "vnc": disconnect(ws, AccessGatewayCreateError, "创建SSH隧道失败"+err.Error())
configuration.SetParameter("username", session.Username) return nil
configuration.SetParameter("password", session.Password) }
case "telnet": defer g.CloseSshTunnel(s.ID)
configuration.SetParameter("username", session.Username) ip = exposedIP
configuration.SetParameter("password", session.Password) port = exposedPort
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
case "kubernetes":
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
default:
log.WithField("configuration.Protocol", configuration.Protocol).Error("UnSupport Protocol")
return Fail(c, 400, "不支持的协议")
} }
configuration.SetParameter("hostname", ip)
configuration.SetParameter("hostname", session.IP) configuration.SetParameter("port", strconv.Itoa(port))
configuration.SetParameter("port", strconv.Itoa(session.Port))
// 加载资产配置的属性,优先级比全局配置的高,因此最后加载,覆盖掉全局配置 // 加载资产配置的属性,优先级比全局配置的高,因此最后加载,覆盖掉全局配置
attributes, _ := assetRepository.FindAttrById(session.AssetId) attributes, err := assetRepository.FindAssetAttrMapByAssetId(s.AssetId)
if err != nil {
return err
}
if len(attributes) > 0 { if len(attributes) > 0 {
for i := range attributes { setAssetConfig(attributes, s, configuration)
attribute := attributes[i]
configuration.SetParameter(attribute.Name, attribute.Value)
}
} }
} }
for name := range configuration.Parameters { for name := range configuration.Parameters {
@ -158,94 +118,235 @@ func TunEndpoint(c echo.Context) error {
} }
} }
addr := propertyMap[guacd.Host] + ":" + propertyMap[guacd.Port] addr := config.GlobalCfg.Guacd.Hostname + ":" + strconv.Itoa(config.GlobalCfg.Guacd.Port)
log.Debugf("[%v:%v] 创建guacd隧道[%v]", sessionId, connectionId, addr)
tunnel, err := guacd.NewTunnel(addr, configuration) guacdTunnel, err := guacd.NewTunnel(addr, configuration)
if err != nil { if err != nil {
if connectionId == "" { if connectionId == "" {
CloseSessionById(sessionId, NewTunnelError, err.Error()) disconnect(ws, NewTunnelError, err.Error())
} }
log.Printf("建立连接失败: %v", err.Error()) log.Printf("[%v:%v] 建立连接失败: %v", sessionId, connectionId, err.Error())
return err return err
} }
tun := global.Tun{ nextSession := &session.Session{
Protocol: session.Protocol, ID: sessionId,
Mode: session.Mode, Protocol: s.Protocol,
WebSocket: ws, Mode: s.Mode,
Tunnel: tunnel, WebSocket: ws,
GuacdTunnel: guacdTunnel,
} }
if len(session.ConnectionId) == 0 { if len(s.ConnectionId) == 0 {
if configuration.Protocol == constant.SSH {
var observers []global.Tun nextTerminal, err := CreateNextTerminalBySession(s)
observable := global.Observable{ if err != nil {
Subject: &tun, return err
Observers: observers, }
nextSession.NextTerminal = nextTerminal
} }
global.Store.Set(sessionId, &observable) nextSession.Observer = session.NewObserver(sessionId)
session.GlobalSessionManager.Add <- nextSession
go nextSession.Observer.Run()
sess := model.Session{ sess := model.Session{
ConnectionId: tunnel.UUID, ConnectionId: guacdTunnel.UUID,
Width: intWidth, Width: intWidth,
Height: intHeight, Height: intHeight,
Status: constant.Connecting, Status: constant.Connecting,
Recording: configuration.GetParameter(guacd.RecordingPath), Recording: configuration.GetParameter(guacd.RecordingPath),
} }
// 创建新会话 // 创建新会话
log.Debugf("创建新会话 %v", sess.ConnectionId) log.Debugf("[%v:%v] 创建新会话: %v", sessionId, connectionId, sess.ConnectionId)
if err := sessionRepository.UpdateById(&sess, sessionId); err != nil { if err := sessionRepository.UpdateById(&sess, sessionId); err != nil {
return err return err
} }
} else { } else {
// 监控会话 // 监控会话
observable, ok := global.Store.Get(sessionId) forObsSession := session.GlobalSessionManager.GetById(sessionId)
if ok { if forObsSession == nil {
observers := append(observable.Observers, tun) disconnect(ws, NotFoundSession, "获取会话失败")
observable.Observers = observers return nil
global.Store.Set(sessionId, observable)
log.Debugf("加入会话%v,当前观察者数量为:%v", session.ConnectionId, len(observers))
} }
nextSession.ID = utils.UUID()
forObsSession.Observer.Add <- nextSession
log.Debugf("[%v:%v] 观察者[%v]加入会话[%v]", sessionId, connectionId, nextSession.ID, s.ConnectionId)
} }
ctx, cancel := context.WithCancel(context.Background())
tick := time.NewTicker(time.Millisecond * time.Duration(60))
defer tick.Stop()
var buf []byte
dataChan := make(chan []byte)
go func() { go func() {
GuacdLoop:
for { for {
instruction, err := tunnel.Read() select {
if err != nil { case <-ctx.Done():
if connectionId == "" { log.Debugf("[%v:%v] WebSocket 已关闭,即将关闭 Guacd 连接...", sessionId, connectionId)
CloseSessionById(sessionId, TunnelClosed, "远程连接关闭") break GuacdLoop
default:
instruction, err := guacdTunnel.Read()
if err != nil {
log.Debugf("[%v:%v] Guacd 读取失败,即将退出循环...", sessionId, connectionId)
disconnect(ws, TunnelClosed, "远程连接已关闭")
break GuacdLoop
} }
break if len(instruction) == 0 {
} continue
if len(instruction) == 0 {
continue
}
err = ws.WriteMessage(websocket.TextMessage, instruction)
if err != nil {
if connectionId == "" {
CloseSessionById(sessionId, Normal, "正常退出")
} }
break dataChan <- instruction
} }
} }
log.Debugf("[%v:%v] Guacd 连接已关闭,退出 Guacd 循环。", sessionId, connectionId)
}()
go func() {
tickLoop:
for {
select {
case <-ctx.Done():
break tickLoop
case <-tick.C:
if len(buf) > 0 {
err = ws.WriteMessage(websocket.TextMessage, buf)
if err != nil {
log.Debugf("[%v:%v] WebSocket写入失败即将关闭Guacd连接...", sessionId, connectionId)
break tickLoop
}
buf = []byte{}
}
case data := <-dataChan:
buf = append(buf, data...)
}
}
log.Debugf("[%v:%v] Guacd连接已关闭退出定时器循环。", sessionId, connectionId)
}() }()
for { for {
_, message, err := ws.ReadMessage() _, message, err := ws.ReadMessage()
if err != nil { if err != nil {
if connectionId == "" { log.Debugf("[%v:%v] WebSocket已关闭", sessionId, connectionId)
CloseSessionById(sessionId, Normal, "正常退出") // guacdTunnel.Read() 会阻塞所以要先把guacdTunnel客户端关闭才能退出Guacd循环
_ = guacdTunnel.Close()
if connectionId != "" {
observerId := nextSession.ID
forObsSession := session.GlobalSessionManager.GetById(sessionId)
if forObsSession != nil {
// 移除会话中保存的观察者信息
forObsSession.Observer.Del <- observerId
log.Debugf("[%v:%v] 观察者[%v]退出会话", sessionId, connectionId, observerId)
}
} else {
CloseSessionById(sessionId, Normal, "用户正常退出")
} }
cancel()
break break
} }
_, err = tunnel.WriteAndFlush(message) _, err = guacdTunnel.WriteAndFlush(message)
if err != nil { if err != nil {
if connectionId == "" { CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
CloseSessionById(sessionId, TunnelClosed, "远程连接关闭")
}
break
} }
} }
return err return nil
}
func setAssetConfig(attributes map[string]string, s model.Session, configuration *guacd.Configuration) {
for key, value := range attributes {
if guacd.DrivePath == key {
// 忽略该参数
continue
}
if guacd.EnableDrive == key && value == "true" {
storageId := attributes[guacd.DrivePath]
if storageId == "" || storageId == "-" {
// 默认空间ID和用户ID相同
storageId = s.Creator
}
realPath := path.Join(storageService.GetBaseDrivePath(), storageId)
configuration.SetParameter(guacd.EnableDrive, "true")
configuration.SetParameter(guacd.DriveName, "Next Terminal Filesystem")
configuration.SetParameter(guacd.DrivePath, realPath)
log.Debugf("[%v] 会话 %v:%v 映射目录地址为 %v", s.ID, s.IP, s.Port, realPath)
} else {
configuration.SetParameter(key, value)
}
}
}
func setConfig(propertyMap map[string]string, s model.Session, configuration *guacd.Configuration) {
if propertyMap[guacd.EnableRecording] == "true" {
configuration.SetParameter(guacd.RecordingPath, path.Join(config.GlobalCfg.Guacd.Recording, s.ID))
configuration.SetParameter(guacd.CreateRecordingPath, propertyMap[guacd.CreateRecordingPath])
} else {
configuration.SetParameter(guacd.RecordingPath, "")
}
configuration.Protocol = s.Protocol
switch configuration.Protocol {
case "rdp":
configuration.SetParameter("username", s.Username)
configuration.SetParameter("password", s.Password)
configuration.SetParameter("security", "any")
configuration.SetParameter("ignore-cert", "true")
configuration.SetParameter("create-drive-path", "true")
configuration.SetParameter("resize-method", "reconnect")
configuration.SetParameter(guacd.EnableWallpaper, propertyMap[guacd.EnableWallpaper])
configuration.SetParameter(guacd.EnableTheming, propertyMap[guacd.EnableTheming])
configuration.SetParameter(guacd.EnableFontSmoothing, propertyMap[guacd.EnableFontSmoothing])
configuration.SetParameter(guacd.EnableFullWindowDrag, propertyMap[guacd.EnableFullWindowDrag])
configuration.SetParameter(guacd.EnableDesktopComposition, propertyMap[guacd.EnableDesktopComposition])
configuration.SetParameter(guacd.EnableMenuAnimations, propertyMap[guacd.EnableMenuAnimations])
configuration.SetParameter(guacd.DisableBitmapCaching, propertyMap[guacd.DisableBitmapCaching])
configuration.SetParameter(guacd.DisableOffscreenCaching, propertyMap[guacd.DisableOffscreenCaching])
configuration.SetParameter(guacd.DisableGlyphCaching, propertyMap[guacd.DisableGlyphCaching])
case "ssh":
if len(s.PrivateKey) > 0 && s.PrivateKey != "-" {
configuration.SetParameter("username", s.Username)
configuration.SetParameter("private-key", s.PrivateKey)
configuration.SetParameter("passphrase", s.Passphrase)
} else {
configuration.SetParameter("username", s.Username)
configuration.SetParameter("password", s.Password)
}
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
case "vnc":
configuration.SetParameter("username", s.Username)
configuration.SetParameter("password", s.Password)
case "telnet":
configuration.SetParameter("username", s.Username)
configuration.SetParameter("password", s.Password)
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
case "kubernetes":
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
default:
}
}
func disconnect(ws *websocket.Conn, code int, reason string) {
// guacd 无法处理中文字符所以进行了base64编码。
encodeReason := base64.StdEncoding.EncodeToString([]byte(reason))
err := guacd.NewInstruction("error", encodeReason, strconv.Itoa(code))
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String()))
disconnect := guacd.NewInstruction("disconnect")
_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
} }

View File

@ -1,18 +1,20 @@
package api package api
import ( import (
"errors"
"strconv" "strconv"
"strings" "strings"
"next-terminal/pkg/global" "next-terminal/server/global/cache"
"next-terminal/pkg/log" "next-terminal/server/log"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/utils" "next-terminal/server/utils"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"gorm.io/gorm"
) )
func UserCreateEndpoint(c echo.Context) error { func UserCreateEndpoint(c echo.Context) (err error) {
var item model.User var item model.User
if err := c.Bind(&item); err != nil { if err := c.Bind(&item); err != nil {
return err return err
@ -20,7 +22,6 @@ func UserCreateEndpoint(c echo.Context) error {
password := item.Password password := item.Password
var pass []byte var pass []byte
var err error
if pass, err = utils.Encoder.Encode([]byte(password)); err != nil { if pass, err = utils.Encoder.Encode([]byte(password)); err != nil {
return err return err
} }
@ -32,6 +33,10 @@ func UserCreateEndpoint(c echo.Context) error {
if err := userRepository.Create(&item); err != nil { if err := userRepository.Create(&item); err != nil {
return err return err
} }
err = storageService.CreateStorageByUser(&item)
if err != nil {
return err
}
if item.Mail != "" { if item.Mail != "" {
go mailService.SendMail(item.Mail, "[Next Terminal] 注册通知", "你好,"+item.Nickname+"。管理员为你注册了账号:"+item.Username+" 密码:"+password) go mailService.SendMail(item.Mail, "[Next Terminal] 注册通知", "你好,"+item.Nickname+"。管理员为你注册了账号:"+item.Username+" 密码:"+password)
@ -89,16 +94,22 @@ func UserDeleteEndpoint(c echo.Context) error {
if account.ID == userId { if account.ID == userId {
return Fail(c, -1, "不允许删除自身账户") return Fail(c, -1, "不允许删除自身账户")
} }
user, err := userRepository.FindById(userId)
if err != nil {
return err
}
// 将用户强制下线 // 将用户强制下线
loginLogs, err := loginLogRepository.FindAliveLoginLogsByUserId(userId) loginLogs, err := loginLogRepository.FindAliveLoginLogsByUsername(user.Username)
if err != nil { if err != nil {
return err return err
} }
for j := range loginLogs { for j := range loginLogs {
global.Cache.Delete(loginLogs[j].ID) token := loginLogs[j].ID
if err := userService.Logout(loginLogs[j].ID); err != nil { cacheKey := userService.BuildCacheKeyByToken(token)
log.WithError(err).WithField("id:", loginLogs[j].ID).Error("Cache Deleted Error") cache.GlobalCache.Delete(cacheKey)
if err := userService.Logout(token); err != nil {
log.WithError(err).WithField("id:", token).Error("Cache Deleted Error")
return Fail(c, 500, "强制下线错误") return Fail(c, 500, "强制下线错误")
} }
} }
@ -107,6 +118,10 @@ func UserDeleteEndpoint(c echo.Context) error {
if err := userRepository.DeleteById(userId); err != nil { if err := userRepository.DeleteById(userId); err != nil {
return err return err
} }
// 删除用户的默认磁盘空间
if err := storageService.DeleteStorageById(userId, true); err != nil {
return err
}
} }
return Success(c, nil) return Success(c, nil)
@ -125,7 +140,10 @@ func UserGetEndpoint(c echo.Context) error {
func UserChangePasswordEndpoint(c echo.Context) error { func UserChangePasswordEndpoint(c echo.Context) error {
id := c.Param("id") id := c.Param("id")
password := c.QueryParam("password") password := c.FormValue("password")
if password == "" {
return Fail(c, -1, "请输入密码")
}
user, err := userRepository.FindById(id) user, err := userRepository.FindById(id)
if err != nil { if err != nil {
@ -172,9 +190,11 @@ func ReloadToken() error {
for i := range loginLogs { for i := range loginLogs {
loginLog := loginLogs[i] loginLog := loginLogs[i]
token := loginLog.ID token := loginLog.ID
user, err := userRepository.FindById(loginLog.UserId) user, err := userRepository.FindByUsername(loginLog.Username)
if err != nil { if err != nil {
log.Debugf("用户「%v」获取失败忽略", loginLog.UserId) if errors.Is(gorm.ErrRecordNotFound, err) {
_ = loginLogRepository.DeleteById(token)
}
continue continue
} }
@ -184,13 +204,13 @@ func ReloadToken() error {
User: user, User: user,
} }
cacheKey := BuildCacheKeyByToken(token) cacheKey := userService.BuildCacheKeyByToken(token)
if authorization.Remember { if authorization.Remember {
// 记住登录有效期两周 // 记住登录有效期两周
global.Cache.Set(cacheKey, authorization, RememberEffectiveTime) cache.GlobalCache.Set(cacheKey, authorization, RememberEffectiveTime)
} else { } else {
global.Cache.Set(cacheKey, authorization, NotRememberEffectiveTime) cache.GlobalCache.Set(cacheKey, authorization, NotRememberEffectiveTime)
} }
log.Debugf("重新加载用户「%v」授权Token「%v」到缓存", user.Nickname, token) log.Debugf("重新加载用户「%v」授权Token「%v」到缓存", user.Nickname, token)
} }

227
server/config/config.go Normal file
View File

@ -0,0 +1,227 @@
package config
import (
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"next-terminal/server/utils"
"github.com/mitchellh/go-homedir"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
var GlobalCfg *Config
type Config struct {
Debug bool
Demo bool
Container bool
DB string
Server *Server
Mysql *Mysql
Sqlite *Sqlite
ResetPassword string
ResetTotp string
EncryptionKey string
EncryptionPassword []byte
NewEncryptionKey string
Guacd *Guacd
Sshd *Sshd
}
type Mysql struct {
Hostname string
Port int
Username string
Password string
Database string
}
type Sqlite struct {
File string
}
type Server struct {
Addr string
Cert string
Key string
}
type Guacd struct {
Hostname string
Port int
Recording string
Drive string
}
type Sshd struct {
Enable bool
Addr string
Key string
}
func SetupConfig() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yml")
viper.AddConfigPath("/etc/next-terminal/")
viper.AddConfigPath("$HOME/.next-terminal")
viper.AddConfigPath(".")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
pflag.String("db", "sqlite", "db mode")
pflag.String("sqlite.file", path.Join("/usr/local/next-terminal/data", "sqlite", "next-terminal.db"), "sqlite db file")
pflag.String("mysql.hostname", "127.0.0.1", "mysql hostname")
pflag.Int("mysql.port", 3306, "mysql port")
pflag.String("mysql.username", "mysql", "mysql username")
pflag.String("mysql.password", "mysql", "mysql password")
pflag.String("mysql.database", "next-terminal", "mysql database")
pflag.String("server.addr", "", "server listen addr")
pflag.String("server.cert", "", "tls cert file")
pflag.String("server.key", "", "tls key file")
pflag.String("reset-password", "", "")
pflag.String("encryption-key", "", "")
pflag.String("new-encryption-key", "", "")
pflag.String("guacd.hostname", "127.0.0.1", "")
pflag.Int("guacd.port", 4822, "")
pflag.String("guacd.recording", "/usr/local/next-terminal/data/recording", "")
pflag.String("guacd.drive", "/usr/local/next-terminal/data/drive", "")
pflag.Bool("sshd.enable", false, "true or false")
pflag.String("sshd.addr", "", "sshd server listen addr")
pflag.String("sshd.key", "~/.ssh/id_rsa", "sshd public key filepath")
pflag.Parse()
if err := viper.BindPFlags(pflag.CommandLine); err != nil {
return nil, err
}
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
sshdKey, err := homedir.Expand(viper.GetString("sshd.key"))
if err != nil {
return nil, err
}
guacdRecording, err := homedir.Expand(viper.GetString("guacd.recording"))
if err != nil {
return nil, err
}
guacdDrive, err := homedir.Expand(viper.GetString("guacd.drive"))
if err != nil {
return nil, err
}
var config = &Config{
DB: viper.GetString("db"),
Mysql: &Mysql{
Hostname: viper.GetString("mysql.hostname"),
Port: viper.GetInt("mysql.port"),
Username: viper.GetString("mysql.username"),
Password: viper.GetString("mysql.password"),
Database: viper.GetString("mysql.database"),
},
Sqlite: &Sqlite{
File: viper.GetString("sqlite.file"),
},
Server: &Server{
Addr: viper.GetString("server.addr"),
Cert: viper.GetString("server.cert"),
Key: viper.GetString("server.key"),
},
ResetPassword: viper.GetString("reset-password"),
ResetTotp: viper.GetString("reset-totp"),
Debug: viper.GetBool("debug"),
Demo: viper.GetBool("demo"),
Container: viper.GetBool("container"),
EncryptionKey: viper.GetString("encryption-key"),
NewEncryptionKey: viper.GetString("new-encryption-key"),
Guacd: &Guacd{
Hostname: viper.GetString("guacd.hostname"),
Port: viper.GetInt("guacd.port"),
Recording: guacdRecording,
Drive: guacdDrive,
},
Sshd: &Sshd{
Enable: viper.GetBool("sshd.enable"),
Addr: viper.GetString("sshd.addr"),
Key: sshdKey,
},
}
if config.EncryptionKey == "" {
config.EncryptionKey = "next-terminal"
}
md5Sum := fmt.Sprintf("%x", md5.Sum([]byte(config.EncryptionKey)))
config.EncryptionPassword = []byte(md5Sum)
// 自动创建数据存放目录
if err := utils.MkdirP(config.Guacd.Recording); err != nil {
panic(fmt.Sprintf("创建文件夹 %v 失败: %v", config.Guacd.Recording, err.Error()))
}
if err := utils.MkdirP(config.Guacd.Drive); err != nil {
panic(fmt.Sprintf("创建文件夹 %v 失败: %v", config.Guacd.Drive, err.Error()))
}
if config.DB == "sqlite" {
sqliteDir := filepath.Dir(config.Sqlite.File)
sqliteDir, err := homedir.Expand(sqliteDir)
if err != nil {
return nil, err
}
if err := utils.MkdirP(sqliteDir); err != nil {
panic(fmt.Sprintf("创建文件夹 %v 失败: %v", sqliteDir, err.Error()))
}
}
if config.Sshd.Enable && !utils.FileExists(sshdKey) {
fmt.Printf("检测到本地RSA私钥文件不存在: %v \n", sshdKey)
sshdKeyDir := filepath.Dir(sshdKey)
if !utils.FileExists(sshdKeyDir) {
if err := utils.MkdirP(sshdKeyDir); err != nil {
panic(fmt.Sprintf("创建文件夹 %v 失败: %v", sshdKeyDir, err.Error()))
}
}
// 自动创建 ID_RSA 密钥
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
//使用X509规范,对公钥私钥进行格式化
x509PrivateKey := x509.MarshalPKCS1PrivateKey(privateKey)
block := pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509PrivateKey,
}
privateKeyFile, _ := os.Create(sshdKey)
if err := pem.Encode(privateKeyFile, &block); err != nil {
panic(err)
}
_ = privateKeyFile.Close()
fmt.Printf("自动创建RSA私钥文件成功: %v \n", sshdKey)
}
return config, nil
}
func init() {
var err error
GlobalCfg, err = SetupConfig()
if err != nil {
panic(err)
}
}

View File

@ -1,8 +1,31 @@
package constant package constant
import "next-terminal/pkg/guacd" import (
"next-terminal/server/guacd"
)
const ( const (
Version = "v1.2.0"
Banner = `
_______ __ ___________ .__ .__
\ \ ____ ___ ____/ |_ \__ ___/__________ _____ |__| ____ _____ | |
/ | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | |
/ | \ ___/ > < | | | |\ ___/| | \/ Y Y \ | | \/ __ \| |__
\____|__ /\___ >__/\_ \ |__| |____| \___ >__| |__|_| /__|___| (____ /____/
\/ \/ \/ \/ \/ \/ \/ %s
`
)
const Token = "X-Auth-Token"
const (
SSH = "ssh"
RDP = "rdp"
VNC = "vnc"
Telnet = "telnet"
K8s = "kubernetes"
AccessRuleAllow = "allow" // 允许访问 AccessRuleAllow = "allow" // 允许访问
AccessRuleReject = "reject" // 拒绝访问 AccessRuleReject = "reject" // 拒绝访问
@ -27,15 +50,16 @@ const (
Connected = "connected" // 会话状态:已连接 Connected = "connected" // 会话状态:已连接
Disconnected = "disconnected" // 会话状态:已断开连接 Disconnected = "disconnected" // 会话状态:已断开连接
Guacd = "guacd" // 接入模式guacd Guacd = "guacd" // 接入模式guacd
Naive = "naive" // 接入模式:原生 Naive = "naive" // 接入模式:原生
Terminal = "terminal" // 接入模式:终端
TypeUser = "user" // 普通用户 TypeUser = "user" // 普通用户
TypeAdmin = "admin" // 管理员 TypeAdmin = "admin" // 管理员
) )
var SSHParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, SshMode} var SSHParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, SshMode}
var RDPParameterNames = []string{guacd.Domain, guacd.RemoteApp, guacd.RemoteAppDir, guacd.RemoteAppArgs} var RDPParameterNames = []string{guacd.Domain, guacd.RemoteApp, guacd.RemoteAppDir, guacd.RemoteAppArgs, guacd.EnableDrive, guacd.DrivePath}
var VNCParameterNames = []string{guacd.ColorDepth, guacd.Cursor, guacd.SwapRedBlue, guacd.DestHost, guacd.DestPort} var VNCParameterNames = []string{guacd.ColorDepth, guacd.Cursor, guacd.SwapRedBlue, guacd.DestHost, guacd.DestPort}
var TelnetParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, guacd.UsernameRegex, guacd.PasswordRegex, guacd.LoginSuccessRegex, guacd.LoginFailureRegex} var TelnetParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, guacd.UsernameRegex, guacd.PasswordRegex, guacd.LoginSuccessRegex, guacd.LoginFailureRegex}
var KubernetesParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, guacd.Namespace, guacd.Pod, guacd.Container, guacd.UesSSL, guacd.ClientCert, guacd.ClientKey, guacd.CaCert, guacd.IgnoreCert} var KubernetesParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, guacd.Namespace, guacd.Pod, guacd.Container, guacd.UesSSL, guacd.ClientCert, guacd.ClientKey, guacd.CaCert, guacd.IgnoreCert}

13
server/global/cache/cache.go vendored Normal file
View File

@ -0,0 +1,13 @@
package cache
import (
"time"
"github.com/patrickmn/go-cache"
)
var GlobalCache *cache.Cache
func init() {
GlobalCache = cache.New(5*time.Minute, 10*time.Minute)
}

View File

@ -0,0 +1,16 @@
package cron
import "github.com/robfig/cron/v3"
var GlobalCron *cron.Cron
type Job cron.Job
func init() {
GlobalCron = cron.New(cron.WithSeconds())
GlobalCron.Start()
}
func JobId(jobId int) cron.EntryID {
return cron.EntryID(jobId)
}

View File

@ -0,0 +1,129 @@
package gateway
import (
"context"
"errors"
"fmt"
"net"
"os"
"next-terminal/server/config"
"next-terminal/server/utils"
"golang.org/x/crypto/ssh"
)
// Gateway 接入网关
type Gateway struct {
ID string // 接入网关ID
Connected bool // 是否已连接
LocalHost string // 隧道映射到本地的IP地址
SshClient *ssh.Client
Message string // 失败原因
tunnels map[string]*Tunnel
Add chan *Tunnel
Del chan string
exit chan bool
}
func NewGateway(id, localhost string, connected bool, message string, client *ssh.Client) *Gateway {
return &Gateway{
ID: id,
LocalHost: localhost,
Connected: connected,
Message: message,
SshClient: client,
Add: make(chan *Tunnel),
Del: make(chan string),
tunnels: map[string]*Tunnel{},
exit: make(chan bool, 1),
}
}
func (g *Gateway) Run() {
for {
select {
case t := <-g.Add:
g.tunnels[t.ID] = t
go t.Run()
case k := <-g.Del:
if _, ok := g.tunnels[k]; ok {
g.tunnels[k].Close()
delete(g.tunnels, k)
}
case <-g.exit:
return
}
}
}
func (g *Gateway) Close() {
g.exit <- true
if g.SshClient != nil {
_ = g.SshClient.Close()
}
if len(g.tunnels) > 0 {
for _, tunnel := range g.tunnels {
tunnel.Close()
}
}
}
func (g *Gateway) OpenSshTunnel(id, ip string, port int) (exposedIP string, exposedPort int, err error) {
if !g.Connected {
return "", 0, errors.New(g.Message)
}
localPort, err := utils.GetAvailablePort()
if err != nil {
return "", 0, err
}
localHost := g.LocalHost
if localHost == "" {
if config.GlobalCfg.Container {
localIp, err := utils.GetLocalIp()
if err != nil {
hostname, err := os.Hostname()
if err != nil {
return "", 0, err
} else {
localHost = hostname
}
} else {
localHost = localIp
}
} else {
localHost = "localhost"
}
}
localAddr := fmt.Sprintf("%s:%d", localHost, localPort)
listener, err := net.Listen("tcp", localAddr)
if err != nil {
return "", 0, err
}
ctx, cancel := context.WithCancel(context.Background())
tunnel := &Tunnel{
ID: id,
LocalHost: g.LocalHost,
LocalPort: localPort,
Gateway: g,
RemoteHost: ip,
RemotePort: port,
ctx: ctx,
cancel: cancel,
listener: listener,
}
g.Add <- tunnel
return tunnel.LocalHost, tunnel.LocalPort, nil
}
func (g Gateway) CloseSshTunnel(id string) {
if g.tunnels[id] != nil {
g.tunnels[id].Close()
}
}

View File

@ -0,0 +1,42 @@
package gateway
type Manager struct {
gateways map[string]*Gateway
Add chan *Gateway
Del chan string
}
func NewManager() *Manager {
return &Manager{
Add: make(chan *Gateway),
Del: make(chan string),
gateways: map[string]*Gateway{},
}
}
func (m *Manager) Run() {
for {
select {
case g := <-m.Add:
m.gateways[g.ID] = g
go g.Run()
case k := <-m.Del:
if _, ok := m.gateways[k]; ok {
m.gateways[k].Close()
delete(m.gateways, k)
}
}
}
}
func (m Manager) GetById(id string) *Gateway {
return m.gateways[id]
}
var GlobalGatewayManager *Manager
func init() {
GlobalGatewayManager = NewManager()
go GlobalGatewayManager.Run()
}

View File

@ -0,0 +1,63 @@
package gateway
import (
"context"
"fmt"
"io"
"net"
"next-terminal/server/log"
)
type Tunnel struct {
ID string // 唯一标识
LocalHost string // 本地监听地址
LocalPort int // 本地端口
RemoteHost string // 远程连接地址
RemotePort int // 远程端口
Gateway *Gateway
ctx context.Context
cancel context.CancelFunc
listener net.Listener
err error
}
func (r *Tunnel) Run() {
localAddr := fmt.Sprintf("%s:%d", r.LocalHost, r.LocalPort)
log.Debugf("等待客户端访问 [%v] ...", localAddr)
localConn, err := r.listener.Accept()
if err != nil {
r.err = err
return
}
log.Debugf("客户端 [%v] 已连接至 [%v]", localConn.RemoteAddr().String(), localAddr)
remoteAddr := fmt.Sprintf("%s:%d", r.RemoteHost, r.RemotePort)
log.Debugf("连接远程主机 [%v] ...", remoteAddr)
remoteConn, err := r.Gateway.SshClient.Dial("tcp", remoteAddr)
if err != nil {
log.Debugf("连接远程主机 [%v] 失败", remoteAddr)
r.err = err
return
}
log.Debugf("连接远程主机 [%v] 成功", remoteAddr)
go copyConn(localConn, remoteConn)
go copyConn(remoteConn, localConn)
log.Debugf("开始转发数据 [%v]->[%v]", localAddr, remoteAddr)
go func() {
<-r.ctx.Done()
_ = r.listener.Close()
_ = localConn.Close()
_ = remoteConn.Close()
log.Debugf("SSH隧道 [%v]-[%v] 已关闭", localAddr, remoteAddr)
}()
}
func (r Tunnel) Close() {
r.cancel()
}
func copyConn(writer, reader net.Conn) {
_, _ = io.Copy(writer, reader)
}

View File

@ -0,0 +1,70 @@
package security
import "sort"
type Security struct {
ID string
Rule string
IP string
Priority int64 // 越小优先级越高
}
type Manager struct {
securities map[string]*Security
values []*Security
Add chan *Security
Del chan string
}
func NewManager() *Manager {
return &Manager{
Add: make(chan *Security),
Del: make(chan string),
securities: map[string]*Security{},
}
}
func (m *Manager) Run() {
for {
select {
case s := <-m.Add:
m.securities[s.ID] = s
m.LoadData()
case s := <-m.Del:
if _, ok := m.securities[s]; ok {
delete(m.securities, s)
m.LoadData()
}
}
}
}
func (m *Manager) Clear() {
m.securities = map[string]*Security{}
}
func (m *Manager) LoadData() {
var values []*Security
for _, security := range m.securities {
values = append(values, security)
}
sort.Slice(values, func(i, j int) bool {
// 优先级数字越小代表优先级越高,因此此处用小于号
return values[i].Priority < values[j].Priority
})
m.values = values
}
func (m Manager) Values() []*Security {
return m.values
}
var GlobalSecurityManager *Manager
func init() {
GlobalSecurityManager = NewManager()
go GlobalSecurityManager.Run()
}

View File

@ -0,0 +1,97 @@
package session
import (
"fmt"
"next-terminal/server/guacd"
"next-terminal/server/term"
"github.com/gorilla/websocket"
)
type Session struct {
ID string
Protocol string
Mode string
WebSocket *websocket.Conn
GuacdTunnel *guacd.Tunnel
NextTerminal *term.NextTerminal
Observer *Manager
}
type Manager struct {
id string
sessions map[string]*Session
Add chan *Session
Del chan string
exit chan bool
}
func NewManager() *Manager {
return &Manager{
Add: make(chan *Session),
Del: make(chan string),
sessions: map[string]*Session{},
exit: make(chan bool, 1),
}
}
func NewObserver(id string) *Manager {
return &Manager{
id: id,
Add: make(chan *Session),
Del: make(chan string),
sessions: map[string]*Session{},
exit: make(chan bool, 1),
}
}
func (m *Manager) Run() {
defer fmt.Printf("Session Manager %v End\n", m.id)
fmt.Printf("Session Manager %v Run\n", m.id)
for {
select {
case s := <-m.Add:
m.sessions[s.ID] = s
case k := <-m.Del:
if _, ok := m.sessions[k]; ok {
ss := m.sessions[k]
if ss.GuacdTunnel != nil {
_ = ss.GuacdTunnel.Close()
}
if ss.NextTerminal != nil {
_ = ss.NextTerminal.Close()
}
if ss.WebSocket != nil {
_ = ss.WebSocket.Close()
}
if ss.Observer != nil {
ss.Observer.Close()
}
delete(m.sessions, k)
}
case <-m.exit:
return
}
}
}
func (m *Manager) Close() {
m.exit <- true
}
func (m Manager) GetById(id string) *Session {
return m.sessions[id]
}
func (m Manager) All() map[string]*Session {
return m.sessions
}
var GlobalSessionManager *Manager
func init() {
GlobalSessionManager = NewManager()
go GlobalSessionManager.Run()
}

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time"
) )
const ( const (
@ -69,9 +70,10 @@ type Configuration struct {
Parameters map[string]string Parameters map[string]string
} }
func NewConfiguration() (ret Configuration) { func NewConfiguration() (config *Configuration) {
ret.Parameters = make(map[string]string) config = &Configuration{}
return ret config.Parameters = make(map[string]string)
return config
} }
func (opt *Configuration) SetParameter(name, value string) { func (opt *Configuration) SetParameter(name, value string) {
@ -129,13 +131,13 @@ type Tunnel struct {
rw *bufio.ReadWriter rw *bufio.ReadWriter
conn net.Conn conn net.Conn
UUID string UUID string
Config Configuration Config *Configuration
IsOpen bool IsOpen bool
} }
func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) { func NewTunnel(address string, config *Configuration) (ret *Tunnel, err error) {
conn, err := net.Dial("tcp", address) conn, err := net.DialTimeout("tcp", address, 5*time.Second)
if err != nil { if err != nil {
return return
} }
@ -151,11 +153,13 @@ func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) {
} }
if err := ret.WriteInstructionAndFlush(NewInstruction("select", selectArg)); err != nil { if err := ret.WriteInstructionAndFlush(NewInstruction("select", selectArg)); err != nil {
_ = conn.Close()
return nil, err return nil, err
} }
args, err := ret.expect("args") args, err := ret.expect("args")
if err != nil { if err != nil {
_ = conn.Close()
return return
} }
@ -165,18 +169,23 @@ func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) {
// send size // send size
if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, dpi)); err != nil { if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, dpi)); err != nil {
_ = conn.Close()
return nil, err return nil, err
} }
if err := ret.WriteInstructionAndFlush(NewInstruction("audio", "audio/L8", "audio/L16")); err != nil { if err := ret.WriteInstructionAndFlush(NewInstruction("audio", "audio/L8", "audio/L16")); err != nil {
_ = conn.Close()
return nil, err return nil, err
} }
if err := ret.WriteInstructionAndFlush(NewInstruction("video")); err != nil { if err := ret.WriteInstructionAndFlush(NewInstruction("video")); err != nil {
_ = conn.Close()
return nil, err return nil, err
} }
if err := ret.WriteInstructionAndFlush(NewInstruction("image", "image/jpeg", "image/png", "image/webp")); err != nil { if err := ret.WriteInstructionAndFlush(NewInstruction("image", "image/jpeg", "image/png", "image/webp")); err != nil {
_ = conn.Close()
return nil, err return nil, err
} }
if err := ret.WriteInstructionAndFlush(NewInstruction("timezone", "Asia/Shanghai")); err != nil { if err := ret.WriteInstructionAndFlush(NewInstruction("timezone", "Asia/Shanghai")); err != nil {
_ = conn.Close()
return nil, err return nil, err
} }
@ -191,6 +200,7 @@ func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) {
} }
// send connect // send connect
if err := ret.WriteInstructionAndFlush(NewInstruction("connect", parameters...)); err != nil { if err := ret.WriteInstructionAndFlush(NewInstruction("connect", parameters...)); err != nil {
_ = conn.Close()
return nil, err return nil, err
} }
@ -200,6 +210,7 @@ func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) {
} }
if len(ready.Args) == 0 { if len(ready.Args) == 0 {
_ = conn.Close()
return nil, errors.New("no connection id received") return nil, errors.New("no connection id received")
} }
@ -288,5 +299,6 @@ func (opt *Tunnel) expect(opcode string) (instruction Instruction, err error) {
func (opt *Tunnel) Close() error { func (opt *Tunnel) Close() error {
opt.IsOpen = false opt.IsOpen = false
_ = opt.rw.Flush()
return opt.conn.Close() return opt.conn.Close()
} }

View File

@ -10,7 +10,7 @@ import (
"strings" "strings"
"time" "time"
"next-terminal/pkg/config" "next-terminal/server/config"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"

View File

@ -0,0 +1,34 @@
package model
import "next-terminal/server/utils"
// AccessGateway 接入网关
type AccessGateway struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Name string `gorm:"type:varchar(500)" json:"name"`
IP string `gorm:"type:varchar(500)" json:"ip"`
Port int `gorm:"type:int(5)" json:"port"`
Localhost string `gorm:"type:varchar(200)" json:"localhost"` // 隧道映射到本地的地址
AccountType string `gorm:"type:varchar(50)" json:"accountType"`
Username string `gorm:"type:varchar(200)" json:"username"`
Password string `gorm:"type:varchar(500)" json:"password"`
PrivateKey string `gorm:"type:text" json:"privateKey"`
Passphrase string `gorm:"type:varchar(500)" json:"passphrase"`
Created utils.JsonTime `json:"created"`
}
func (r *AccessGateway) TableName() string {
return "access_gateways"
}
type AccessGatewayForPage struct {
ID string `json:"id"`
Name string `json:"name"`
IP string `json:"ip"`
Port int `json:"port"`
AccountType string `json:"accountType"`
Username string `json:"username"`
Created utils.JsonTime `json:"created"`
Connected bool `json:"connected"`
Message string `json:"message"`
}

View File

@ -1,10 +1,10 @@
package model package model
type AccessSecurity struct { type AccessSecurity struct {
ID string `json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Rule string `json:"rule"` Rule string `gorm:"type:varchar(20)" json:"rule"`
IP string `json:"ip"` IP string `gorm:"type:varchar(500)" json:"ip"`
Source string `json:"source"` Source string `gorm:"type:varchar(500)" json:"source"`
Priority int64 `json:"priority"` // 越小优先级越高 Priority int64 `json:"priority"` // 越小优先级越高
} }

View File

@ -4,39 +4,41 @@ import (
"next-terminal/server/utils" "next-terminal/server/utils"
) )
type AssetProto string
type Asset struct { type Asset struct {
ID string `gorm:"primary_key " json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Name string `json:"name"` Name string `gorm:"type:varchar(500)" json:"name"`
Protocol string `json:"protocol"` Protocol string `gorm:"type:varchar(20)" json:"protocol"`
IP string `json:"ip"` IP string `gorm:"type:varchar(200)" json:"ip"`
Port int `json:"port"` Port int `json:"port"`
AccountType string `json:"accountType"` AccountType string `gorm:"type:varchar(20)" json:"accountType"`
Username string `json:"username"` Username string `gorm:"type:varchar(200)" json:"username"`
Password string `json:"password"` Password string `gorm:"type:varchar(500)" json:"password"`
CredentialId string `gorm:"index" json:"credentialId"` CredentialId string `gorm:"index,type:varchar(36)" json:"credentialId"`
PrivateKey string `json:"privateKey"` PrivateKey string `gorm:"type:text" json:"privateKey"`
Passphrase string `json:"passphrase"` Passphrase string `gorm:"type:varchar(500)" json:"passphrase"`
Description string `json:"description"` Description string `json:"description"`
Active bool `json:"active"` Active bool `json:"active"`
Created utils.JsonTime `json:"created"` Created utils.JsonTime `json:"created"`
Tags string `json:"tags"` Tags string `json:"tags"`
Owner string `gorm:"index" json:"owner"` Owner string `gorm:"index,type:varchar(36)" json:"owner"`
Encrypted bool `json:"encrypted"` Encrypted bool `json:"encrypted"`
AccessGatewayId string `gorm:"type:varchar(36)" json:"accessGatewayId"`
} }
type AssetForPage struct { type AssetForPage struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
IP string `json:"ip"` IP string `json:"ip"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Port int `json:"port"` Port int `json:"port"`
Active bool `json:"active"` Active bool `json:"active"`
Created utils.JsonTime `json:"created"` Created utils.JsonTime `json:"created"`
Tags string `json:"tags"` Tags string `json:"tags"`
Owner string `json:"owner"` Owner string `json:"owner"`
OwnerName string `json:"ownerName"` OwnerName string `json:"ownerName"`
SharerCount int64 `json:"sharerCount"` SshMode string `json:"sshMode"`
SshMode string `json:"sshMode"`
} }
func (r *Asset) TableName() string { func (r *Asset) TableName() string {

View File

@ -5,11 +5,11 @@ import (
) )
type Command struct { type Command struct {
ID string `gorm:"primary_key" json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Name string `json:"name"` Name string `gorm:"type:varchar(500)" json:"name"`
Content string `json:"content"` Content string `json:"content"`
Created utils.JsonTime `json:"created"` Created utils.JsonTime `json:"created"`
Owner string `gorm:"index" json:"owner"` Owner string `gorm:"index,type:varchar(36)" json:"owner"`
} }
type CommandForPage struct { type CommandForPage struct {

View File

@ -5,15 +5,15 @@ import (
) )
type Credential struct { type Credential struct {
ID string `gorm:"primary_key" json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Name string `json:"name"` Name string `gorm:"type:varchar(500)" json:"name"`
Type string `json:"type"` Type string `gorm:"type:varchar(50)" json:"type"`
Username string `json:"username"` Username string `gorm:"type:varchar(200)" json:"username"`
Password string `json:"password"` Password string `gorm:"type:varchar(500)" json:"password"`
PrivateKey string `json:"privateKey"` PrivateKey string `gorm:"type:text" json:"privateKey"`
Passphrase string `json:"passphrase"` Passphrase string `gorm:"type:varchar(500)" json:"passphrase"`
Created utils.JsonTime `json:"created"` Created utils.JsonTime `json:"created"`
Owner string `gorm:"index" json:"owner"` Owner string `gorm:"index,type:varchar(36)" json:"owner"`
Encrypted bool `json:"encrypted"` Encrypted bool `json:"encrypted"`
} }

View File

@ -5,14 +5,14 @@ import (
) )
type Job struct { type Job struct {
ID string `gorm:"primary_key" json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
CronJobId int `json:"cronJobId"` CronJobId int `json:"cronJobId"`
Name string `json:"name"` Name string `gorm:"type:varchar(500)" json:"name"`
Func string `json:"func"` Func string `gorm:"type:varchar(200)" json:"func"`
Cron string `json:"cron"` Cron string `gorm:"type:varchar(100)" json:"cron"`
Mode string `json:"mode"` Mode string `gorm:"type:varchar(50)" json:"mode"`
ResourceIds string `json:"resourceIds"` ResourceIds string `json:"resourceIds"`
Status string `json:"status"` Status string `gorm:"type:varchar(20)" json:"status"`
Metadata string `json:"metadata"` Metadata string `json:"metadata"`
Created utils.JsonTime `json:"created"` Created utils.JsonTime `json:"created"`
Updated utils.JsonTime `json:"updated"` Updated utils.JsonTime `json:"updated"`

View File

@ -5,24 +5,15 @@ import (
) )
type LoginLog struct { type LoginLog struct {
ID string `gorm:"primary_key" json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
UserId string `gorm:"index" json:"userId"` Username string `gorm:"index,type:varchar(200)" json:"username"`
ClientIP string `json:"clientIp"` ClientIP string `gorm:"type:varchar(200)" json:"clientIp"`
ClientUserAgent string `json:"clientUserAgent"` ClientUserAgent string `gorm:"type:varchar(500)" json:"clientUserAgent"`
LoginTime utils.JsonTime `json:"loginTime"`
LogoutTime utils.JsonTime `json:"logoutTime"`
Remember bool `json:"remember"`
}
type LoginLogForPage struct {
ID string `json:"id"`
UserId string `json:"userId"`
UserName string `json:"userName"`
ClientIP string `json:"clientIp"`
ClientUserAgent string `json:"clientUserAgent"`
LoginTime utils.JsonTime `json:"loginTime"` LoginTime utils.JsonTime `json:"loginTime"`
LogoutTime utils.JsonTime `json:"logoutTime"` LogoutTime utils.JsonTime `json:"logoutTime"`
Remember bool `json:"remember"` Remember bool `json:"remember"`
State string `gorm:"type:varchar(1)" json:"state"` // 成功 1 失败 0
Reason string `gorm:"type:varchar(500)" json:"reason"`
} }
func (r *LoginLog) TableName() string { func (r *LoginLog) TableName() string {

View File

@ -1,9 +0,0 @@
package model
type Num struct {
I string `gorm:"primary_key" json:"i"`
}
func (r *Num) TableName() string {
return "nums"
}

View File

@ -1,11 +1,12 @@
package model package model
type ResourceSharer struct { type ResourceSharer struct {
ID string `gorm:"primary_key" json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
ResourceId string `gorm:"index" json:"resourceId"` ResourceId string `gorm:"index,type:varchar(36)" json:"resourceId"`
ResourceType string `gorm:"index" json:"resourceType"` ResourceType string `gorm:"index,type:varchar(36)" json:"resourceType"`
UserId string `gorm:"index" json:"userId"` StrategyId string `gorm:"index,type:varchar(36)" json:"strategyId"`
UserGroupId string `gorm:"index" json:"userGroupId"` UserId string `gorm:"index,type:varchar(36)" json:"userId"`
UserGroupId string `gorm:"index,type:varchar(36)" json:"userGroupId"`
} }
func (r *ResourceSharer) TableName() string { func (r *ResourceSharer) TableName() string {

View File

@ -5,27 +5,35 @@ import (
) )
type Session struct { type Session struct {
ID string `gorm:"primary_key" json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Protocol string `json:"protocol"` Protocol string `gorm:"type:varchar(20)" json:"protocol"`
IP string `json:"ip"` IP string `gorm:"type:varchar(200)" json:"ip"`
Port int `json:"port"` Port int `json:"port"`
ConnectionId string `json:"connectionId"` ConnectionId string `gorm:"type:varchar(50)" json:"connectionId"`
AssetId string `gorm:"index" json:"assetId"` AssetId string `gorm:"index,type:varchar(36)" json:"assetId"`
Username string `json:"username"` Username string `gorm:"type:varchar(200)" json:"username"`
Password string `json:"password"` Password string `gorm:"type:varchar(500)" json:"password"`
Creator string `gorm:"index" json:"creator"` Creator string `gorm:"index,type:varchar(36)" json:"creator"`
ClientIP string `json:"clientIp"` ClientIP string `gorm:"type:varchar(200)" json:"clientIp"`
Width int `json:"width"` Width int `json:"width"`
Height int `json:"height"` Height int `json:"height"`
Status string `gorm:"index" json:"status"` Status string `gorm:"index,type:varchar(20)" json:"status"`
Recording string `json:"recording"` Recording string `gorm:"type:varchar(1000)" json:"recording"`
PrivateKey string `json:"privateKey"` PrivateKey string `gorm:"type:text" json:"privateKey"`
Passphrase string `json:"passphrase"` Passphrase string `gorm:"type:varchar(500)" json:"passphrase"`
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message"` Message string `json:"message"`
ConnectedTime utils.JsonTime `json:"connectedTime"` ConnectedTime utils.JsonTime `json:"connectedTime"`
DisconnectedTime utils.JsonTime `json:"disconnectedTime"` DisconnectedTime utils.JsonTime `json:"disconnectedTime"`
Mode string `json:"mode"` Mode string `gorm:"type:varchar(10)" json:"mode"`
Upload string `gorm:"type:varchar(1)" json:"upload"` // 1 = true, 0 = false
Download string `gorm:"type:varchar(1)" json:"download"`
Delete string `gorm:"type:varchar(1)" json:"delete"`
Rename string `gorm:"type:varchar(1)" json:"rename"`
Edit string `gorm:"type:varchar(1)" json:"edit"`
CreateDir string `gorm:"type:varchar(1)" json:"createDir"`
StorageId string `gorm:"type:varchar(36)" json:"storageId"`
AccessGatewayId string `gorm:"type:varchar(36)" json:"accessGatewayId"`
} }
func (r *Session) TableName() string { func (r *Session) TableName() string {
@ -54,3 +62,12 @@ type SessionForPage struct {
Message string `json:"message"` Message string `json:"message"`
Mode string `json:"mode"` Mode string `json:"mode"`
} }
type SessionForAccess struct {
AssetId string `json:"assetId"`
Protocol string `json:"protocol"`
IP string `json:"ip"`
Port int `json:"port"`
Username string `json:"username"`
AccessCount int64 `json:"accessCount"`
}

29
server/model/storage.go Normal file
View File

@ -0,0 +1,29 @@
package model
import "next-terminal/server/utils"
type Storage struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Name string `gorm:"type:varchar(500)" json:"name"`
IsShare bool `json:"isShare"` // 是否共享
LimitSize int64 `json:"limitSize"` // 大小限制,单位字节
IsDefault bool `json:"isDefault"` // 是否为用户默认的
Owner string `gorm:"index,type:varchar(36)" json:"owner"`
Created utils.JsonTime `json:"created"`
}
func (r *Storage) TableName() string {
return "storages"
}
type StorageForPage struct {
ID string `gorm:"primary_key " json:"id"`
Name string `json:"name"`
IsShare bool `json:"isShare"` // 是否共享
LimitSize int64 `json:"limitSize"` // 大小限制,单位字节
UsedSize int64 `json:"usedSize"`
IsDefault bool `json:"isDefault"` // 是否为用户默认的
Owner string `gorm:"index" json:"owner"`
OwnerName string `json:"ownerName"`
Created utils.JsonTime `json:"created"`
}

19
server/model/strategy.go Normal file
View File

@ -0,0 +1,19 @@
package model
import "next-terminal/server/utils"
type Strategy struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Name string `gorm:"type:varchar(500)" json:"name"`
Upload string `gorm:"type:varchar(1)" json:"upload"` // 1 = true, 0 = false
Download string `gorm:"type:varchar(1)" json:"download"`
Delete string `gorm:"type:varchar(1)" json:"delete"`
Rename string `gorm:"type:varchar(1)" json:"rename"`
Edit string `gorm:"type:varchar(1)" json:"edit"`
CreateDir string `gorm:"type:varchar(1)" json:"createDir"`
Created utils.JsonTime `json:"created"`
}
func (r *Strategy) TableName() string {
return "strategies"
}

View File

@ -5,16 +5,16 @@ import (
) )
type User struct { type User struct {
ID string `gorm:"primary_key" json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Username string `gorm:"index" json:"username"` Username string `gorm:"index,type:varchar(200)" json:"username"`
Password string `json:"password"` Password string `gorm:"type:varchar(500)" json:"password"`
Nickname string `json:"nickname"` Nickname string `gorm:"type:varchar(500)" json:"nickname"`
TOTPSecret string `json:"-"` TOTPSecret string `json:"-"`
Online bool `json:"online"` Online bool `json:"online"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Created utils.JsonTime `json:"created"` Created utils.JsonTime `json:"created"`
Type string `json:"type"` Type string `gorm:"type:varchar(20)" json:"type"`
Mail string `json:"mail"` Mail string `gorm:"type:varchar(500)" json:"mail"`
} }
type UserForPage struct { type UserForPage struct {

View File

@ -5,8 +5,8 @@ import (
) )
type UserGroup struct { type UserGroup struct {
ID string `gorm:"primary_key" json:"id"` ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
Name string `json:"name"` Name string `gorm:"type:varchar(500)" json:"name"`
Created utils.JsonTime `json:"created"` Created utils.JsonTime `json:"created"`
} }

View File

@ -0,0 +1,85 @@
package repository
import (
"next-terminal/server/model"
"gorm.io/gorm"
)
type AccessGatewayRepository struct {
DB *gorm.DB
}
func NewAccessGatewayRepository(db *gorm.DB) *AccessGatewayRepository {
accessGatewayRepository = &AccessGatewayRepository{DB: db}
return accessGatewayRepository
}
func (r AccessGatewayRepository) Find(pageIndex, pageSize int, ip, name, order, field string) (o []model.AccessGatewayForPage, total int64, err error) {
t := model.AccessGateway{}
db := r.DB.Table(t.TableName())
dbCounter := r.DB.Table(t.TableName())
if len(ip) > 0 {
db = db.Where("ip like ?", "%"+ip+"%")
dbCounter = dbCounter.Where("ip like ?", "%"+ip+"%")
}
if len(name) > 0 {
db = db.Where("name like ?", "%"+name+"%")
dbCounter = dbCounter.Where("name like ?", "%"+name+"%")
}
err = dbCounter.Count(&total).Error
if err != nil {
return nil, 0, err
}
if order == "descend" {
order = "desc"
} else {
order = "asc"
}
if field == "ip" {
field = "ip"
} else if field == "name" {
field = "name"
} else {
field = "created"
}
err = db.Order(field + " " + order).Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Error
if o == nil {
o = make([]model.AccessGatewayForPage, 0)
}
return
}
func (r AccessGatewayRepository) Create(o *model.AccessGateway) error {
return r.DB.Create(o).Error
}
func (r AccessGatewayRepository) UpdateById(o *model.AccessGateway, id string) error {
o.ID = id
return r.DB.Updates(o).Error
}
func (r AccessGatewayRepository) DeleteById(id string) error {
return r.DB.Where("id = ?", id).Delete(model.AccessGateway{}).Error
}
func (r AccessGatewayRepository) FindById(id string) (o model.AccessGateway, err error) {
err = r.DB.Where("id = ?", id).First(&o).Error
return
}
func (r AccessGatewayRepository) FindAll() (o []model.AccessGateway, err error) {
t := model.AccessGateway{}
db := r.DB.Table(t.TableName())
err = db.Find(&o).Error
if o == nil {
o = make([]model.AccessGateway, 0)
}
return
}

View File

@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"next-terminal/pkg/constant" "next-terminal/server/config"
"next-terminal/pkg/global" "next-terminal/server/constant"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/utils" "next-terminal/server/utils"
@ -44,11 +44,21 @@ func (r AssetRepository) FindByProtocolAndIds(protocol string, assetIds []string
} }
func (r AssetRepository) FindByProtocolAndUser(protocol string, account model.User) (o []model.Asset, err error) { func (r AssetRepository) FindByProtocolAndUser(protocol string, account model.User) (o []model.Asset, err error) {
db := r.DB.Table("assets").Select("assets.id,assets.name,assets.ip,assets.port,assets.protocol,assets.active,assets.owner,assets.created, users.nickname as owner_name,COUNT(resource_sharers.user_id) as sharer_count").Joins("left join users on assets.owner = users.id").Joins("left join resource_sharers on assets.id = resource_sharers.resource_id").Group("assets.id") db := r.DB.Table("assets").Select("assets.id,assets.name,assets.ip,assets.port,assets.protocol,assets.active,assets.owner,assets.created,assets.tags,assets.description, users.nickname as owner_name").Joins("left join users on assets.owner = users.id").Joins("left join resource_sharers on assets.id = resource_sharers.resource_id").Group("assets.id")
if constant.TypeUser == account.Type { if constant.TypeUser == account.Type {
owner := account.ID owner := account.ID
db = db.Where("assets.owner = ? or resource_sharers.user_id = ?", owner, owner) db = db.Where("assets.owner = ? or resource_sharers.user_id = ?", owner, owner)
// 查询用户所在用户组列表
userGroupIds, err := userGroupRepository.FindUserGroupIdsByUserId(account.ID)
if err != nil {
return nil, err
}
if len(userGroupIds) > 0 {
db = db.Or("resource_sharers.user_group_id in ?", userGroupIds)
}
} }
if len(protocol) > 0 { if len(protocol) > 0 {
@ -59,7 +69,7 @@ func (r AssetRepository) FindByProtocolAndUser(protocol string, account model.Us
} }
func (r AssetRepository) Find(pageIndex, pageSize int, name, protocol, tags string, account model.User, owner, sharer, userGroupId, ip, order, field string) (o []model.AssetForPage, total int64, err error) { func (r AssetRepository) Find(pageIndex, pageSize int, name, protocol, tags string, account model.User, owner, sharer, userGroupId, ip, order, field string) (o []model.AssetForPage, total int64, err error) {
db := r.DB.Table("assets").Select("assets.id,assets.name,assets.ip,assets.port,assets.protocol,assets.active,assets.owner,assets.created,assets.tags, users.nickname as owner_name,COUNT(resource_sharers.user_id) as sharer_count").Joins("left join users on assets.owner = users.id").Joins("left join resource_sharers on assets.id = resource_sharers.resource_id").Group("assets.id") db := r.DB.Table("assets").Select("assets.id,assets.name,assets.ip,assets.port,assets.protocol,assets.active,assets.owner,assets.created,assets.tags,assets.description, users.nickname as owner_name").Joins("left join users on assets.owner = users.id").Joins("left join resource_sharers on assets.id = resource_sharers.resource_id").Group("assets.id")
dbCounter := r.DB.Table("assets").Select("DISTINCT assets.id").Joins("left join resource_sharers on assets.id = resource_sharers.resource_id").Group("assets.id") dbCounter := r.DB.Table("assets").Select("DISTINCT assets.id").Joins("left join resource_sharers on assets.id = resource_sharers.resource_id").Group("assets.id")
if constant.TypeUser == account.Type { if constant.TypeUser == account.Type {
@ -111,7 +121,7 @@ func (r AssetRepository) Find(pageIndex, pageSize int, name, protocol, tags stri
if len(tags) > 0 { if len(tags) > 0 {
tagArr := strings.Split(tags, ",") tagArr := strings.Split(tags, ",")
for i := range tagArr { for i := range tagArr {
if global.Config.DB == "sqlite" { if config.GlobalCfg.DB == "sqlite" {
db = db.Where("(',' || assets.tags || ',') LIKE ?", "%,"+tagArr[i]+",%") db = db.Where("(',' || assets.tags || ',') LIKE ?", "%,"+tagArr[i]+",%")
dbCounter = dbCounter.Where("(',' || assets.tags || ',') LIKE ?", "%,"+tagArr[i]+",%") dbCounter = dbCounter.Where("(',' || assets.tags || ',') LIKE ?", "%,"+tagArr[i]+",%")
} else { } else {
@ -189,7 +199,7 @@ func (r AssetRepository) Encrypt(item *model.Asset, password []byte) error {
} }
func (r AssetRepository) Create(o *model.Asset) (err error) { func (r AssetRepository) Create(o *model.Asset) (err error) {
if err := r.Encrypt(o, global.Config.EncryptionPassword); err != nil { if err := r.Encrypt(o, config.GlobalCfg.EncryptionPassword); err != nil {
return err return err
} }
if err = r.DB.Create(o).Error; err != nil { if err = r.DB.Create(o).Error; err != nil {
@ -245,7 +255,7 @@ func (r AssetRepository) Decrypt(item *model.Asset, password []byte) error {
func (r AssetRepository) FindByIdAndDecrypt(id string) (o model.Asset, err error) { func (r AssetRepository) FindByIdAndDecrypt(id string) (o model.Asset, err error) {
err = r.DB.Where("id = ?", id).First(&o).Error err = r.DB.Where("id = ?", id).First(&o).Error
if err == nil { if err == nil {
err = r.Decrypt(&o, global.Config.EncryptionPassword) err = r.Decrypt(&o, config.GlobalCfg.EncryptionPassword)
} }
return return
} }
@ -260,8 +270,17 @@ func (r AssetRepository) UpdateActiveById(active bool, id string) error {
return r.DB.Exec(sql, active, id).Error return r.DB.Exec(sql, active, id).Error
} }
func (r AssetRepository) DeleteById(id string) error { func (r AssetRepository) DeleteById(id string) (err error) {
return r.DB.Where("id = ?", id).Delete(&model.Asset{}).Error return r.DB.Transaction(func(tx *gorm.DB) error {
err = tx.Where("id = ?", id).Delete(&model.Asset{}).Error
if err != nil {
return err
}
// 删除资产属性
err = tx.Where("asset_id = ?", id).Delete(&model.AssetAttribute{}).Error
return err
})
} }
func (r AssetRepository) Count() (total int64, err error) { func (r AssetRepository) Count() (total int64, err error) {
@ -269,6 +288,11 @@ func (r AssetRepository) Count() (total int64, err error) {
return return
} }
func (r AssetRepository) CountByProtocol(protocol string) (total int64, err error) {
err = r.DB.Find(&model.Asset{}).Where("protocol = ?", protocol).Count(&total).Error
return
}
func (r AssetRepository) CountByUserId(userId string) (total int64, err error) { func (r AssetRepository) CountByUserId(userId string) (total int64, err error) {
db := r.DB.Joins("left join resource_sharers on assets.id = resource_sharers.resource_id") db := r.DB.Joins("left join resource_sharers on assets.id = resource_sharers.resource_id")
@ -287,9 +311,27 @@ func (r AssetRepository) CountByUserId(userId string) (total int64, err error) {
return return
} }
func (r AssetRepository) CountByUserIdAndProtocol(userId, protocol string) (total int64, err error) {
db := r.DB.Joins("left join resource_sharers on assets.id = resource_sharers.resource_id")
db = db.Where("( assets.owner = ? or resource_sharers.user_id = ? ) and assets.protocol = ?", userId, userId, protocol)
// 查询用户所在用户组列表
userGroupIds, err := userGroupRepository.FindUserGroupIdsByUserId(userId)
if err != nil {
return 0, err
}
if len(userGroupIds) > 0 {
db = db.Or("resource_sharers.user_group_id in ?", userGroupIds)
}
err = db.Find(&model.Asset{}).Count(&total).Error
return
}
func (r AssetRepository) FindTags() (o []string, err error) { func (r AssetRepository) FindTags() (o []string, err error) {
var assets []model.Asset var assets []model.Asset
err = r.DB.Not("tags = ?", "").Find(&assets).Error err = r.DB.Not("tags = '' or tags = '-' ").Find(&assets).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -359,7 +401,7 @@ func (r AssetRepository) FindAttrById(assetId string) (o []model.AssetAttribute,
return o, err return o, err
} }
func (r AssetRepository) FindAssetAttrMapByAssetId(assetId string) (map[string]interface{}, error) { func (r AssetRepository) FindAssetAttrMapByAssetId(assetId string) (map[string]string, error) {
asset, err := r.FindById(assetId) asset, err := r.FindById(assetId)
if err != nil { if err != nil {
return nil, err return nil, err
@ -383,7 +425,7 @@ func (r AssetRepository) FindAssetAttrMapByAssetId(assetId string) (map[string]i
parameterNames = constant.KubernetesParameterNames parameterNames = constant.KubernetesParameterNames
} }
propertiesMap := propertyRepository.FindAllMap() propertiesMap := propertyRepository.FindAllMap()
var attributeMap = make(map[string]interface{}) var attributeMap = make(map[string]string)
for name := range propertiesMap { for name := range propertiesMap {
if utils.Contains(parameterNames, name) { if utils.Contains(parameterNames, name) {
attributeMap[name] = propertiesMap[name] attributeMap[name] = propertiesMap[name]

View File

@ -1,7 +1,7 @@
package repository package repository
import ( import (
"next-terminal/pkg/constant" "next-terminal/server/constant"
"next-terminal/server/model" "next-terminal/server/model"
"gorm.io/gorm" "gorm.io/gorm"
@ -80,3 +80,17 @@ func (r CommandRepository) UpdateById(o *model.Command, id string) error {
func (r CommandRepository) DeleteById(id string) error { func (r CommandRepository) DeleteById(id string) error {
return r.DB.Where("id = ?", id).Delete(&model.Command{}).Error return r.DB.Where("id = ?", id).Delete(&model.Command{}).Error
} }
func (r CommandRepository) FindByUser(account model.User) (o []model.CommandForPage, err error) {
db := r.DB.Table("commands").Select("commands.id,commands.name,commands.content,commands.owner,commands.created, users.nickname as owner_name,COUNT(resource_sharers.user_id) as sharer_count").Joins("left join users on commands.owner = users.id").Joins("left join resource_sharers on commands.id = resource_sharers.resource_id").Group("commands.id")
if constant.TypeUser == account.Type {
owner := account.ID
db = db.Where("commands.owner = ? or resource_sharers.user_id = ?", owner, owner)
}
err = db.Order("commands.name asc").Find(&o).Error
if o == nil {
o = make([]model.CommandForPage, 0)
}
return
}

View File

@ -3,8 +3,8 @@ package repository
import ( import (
"encoding/base64" "encoding/base64"
"next-terminal/pkg/constant" "next-terminal/server/config"
"next-terminal/pkg/global" "next-terminal/server/constant"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/utils" "next-terminal/server/utils"
@ -69,7 +69,7 @@ func (r CredentialRepository) Find(pageIndex, pageSize int, name, order, field s
} }
func (r CredentialRepository) Create(o *model.Credential) (err error) { func (r CredentialRepository) Create(o *model.Credential) (err error) {
if err := r.Encrypt(o, global.Config.EncryptionPassword); err != nil { if err := r.Encrypt(o, config.GlobalCfg.EncryptionPassword); err != nil {
return err return err
} }
if err = r.DB.Create(o).Error; err != nil { if err = r.DB.Create(o).Error; err != nil {
@ -151,7 +151,7 @@ func (r CredentialRepository) Decrypt(item *model.Credential, password []byte) e
func (r CredentialRepository) FindByIdAndDecrypt(id string) (o model.Credential, err error) { func (r CredentialRepository) FindByIdAndDecrypt(id string) (o model.Credential, err error) {
err = r.DB.Where("id = ?", id).First(&o).Error err = r.DB.Where("id = ?", id).First(&o).Error
if err == nil { if err == nil {
err = r.Decrypt(&o, global.Config.EncryptionPassword) err = r.Decrypt(&o, config.GlobalCfg.EncryptionPassword)
} }
return return
} }

View File

@ -1,5 +1,8 @@
package repository package repository
/**
* 定义了相关模型的持久化层,方便相互之间调用
*/
var ( var (
userRepository *UserRepository userRepository *UserRepository
userGroupRepository *UserGroupRepository userGroupRepository *UserGroupRepository
@ -9,9 +12,11 @@ var (
propertyRepository *PropertyRepository propertyRepository *PropertyRepository
commandRepository *CommandRepository commandRepository *CommandRepository
sessionRepository *SessionRepository sessionRepository *SessionRepository
numRepository *NumRepository
accessSecurityRepository *AccessSecurityRepository accessSecurityRepository *AccessSecurityRepository
accessGatewayRepository *AccessGatewayRepository
jobRepository *JobRepository jobRepository *JobRepository
jobLogRepository *JobLogRepository jobLogRepository *JobLogRepository
loginLogRepository *LoginLogRepository loginLogRepository *LoginLogRepository
storageRepository *StorageRepository
strategyRepository *StrategyRepository
) )

View File

@ -44,10 +44,10 @@ func (r JobRepository) Find(pageIndex, pageSize int, name, status, order, field
if field == "name" { if field == "name" {
field = "name" field = "name"
} else if field == "created" { } else if field == "updated" {
field = "created"
} else {
field = "updated" field = "updated"
} else {
field = "created"
} }
err = db.Order(field + " " + order).Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Error err = db.Order(field + " " + order).Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Error
@ -63,6 +63,12 @@ func (r JobRepository) FindByFunc(function string) (o []model.Job, err error) {
return return
} }
func (r JobRepository) FindAll() (o []model.Job, err error) {
db := r.DB
err = db.Find(&o).Error
return
}
func (r JobRepository) Create(o *model.Job) (err error) { func (r JobRepository) Create(o *model.Job) (err error) {
return r.DB.Create(o).Error return r.DB.Create(o).Error
} }

View File

@ -1,6 +1,8 @@
package repository package repository
import ( import (
"time"
"next-terminal/server/model" "next-terminal/server/model"
"gorm.io/gorm" "gorm.io/gorm"
@ -24,6 +26,20 @@ func (r JobLogRepository) FindByJobId(jobId string) (o []model.JobLog, err error
return return
} }
func (r JobLogRepository) FindOutTimeLog(dayLimit int) (o []model.JobLog, err error) {
limitTime := time.Now().Add(time.Duration(-dayLimit*24) * time.Hour)
err = r.DB.Where("timestamp < ?", limitTime).Find(&o).Error
return
}
func (r JobLogRepository) DeleteByJobId(jobId string) error { func (r JobLogRepository) DeleteByJobId(jobId string) error {
return r.DB.Where("job_id = ?", jobId).Delete(model.JobLog{}).Error return r.DB.Where("job_id = ?", jobId).Delete(model.JobLog{}).Error
} }
func (r JobLogRepository) DeleteByIdIn(ids []string) error {
return r.DB.Where("id in ?", ids).Delete(&model.JobLog{}).Error
}
func (r JobLogRepository) DeleteById(id string) error {
return r.DB.Where("id = ?", id).Delete(&model.JobLog{}).Error
}

View File

@ -1,6 +1,8 @@
package repository package repository
import ( import (
"time"
"next-terminal/server/model" "next-terminal/server/model"
"gorm.io/gorm" "gorm.io/gorm"
@ -15,19 +17,24 @@ func NewLoginLogRepository(db *gorm.DB) *LoginLogRepository {
return loginLogRepository return loginLogRepository
} }
func (r LoginLogRepository) Find(pageIndex, pageSize int, userId, clientIp string) (o []model.LoginLogForPage, total int64, err error) { func (r LoginLogRepository) Find(pageIndex, pageSize int, username, clientIp, state string) (o []model.LoginLog, total int64, err error) {
m := model.LoginLog{}
db := r.DB.Table(m.TableName())
dbCounter := r.DB.Table(m.TableName())
db := r.DB.Table("login_logs").Select("login_logs.id,login_logs.user_id,login_logs.client_ip,login_logs.client_user_agent,login_logs.login_time, login_logs.logout_time, users.nickname as user_name").Joins("left join users on login_logs.user_id = users.id") if username != "" {
dbCounter := r.DB.Table("login_logs").Select("DISTINCT login_logs.id") db = db.Where("username like ?", "%"+username+"%")
dbCounter = dbCounter.Where("username like ?", "%"+username+"%")
if userId != "" {
db = db.Where("login_logs.user_id = ?", userId)
dbCounter = dbCounter.Where("login_logs.user_id = ?", userId)
} }
if clientIp != "" { if clientIp != "" {
db = db.Where("login_logs.client_ip like ?", "%"+clientIp+"%") db = db.Where("client_ip like ?", "%"+clientIp+"%")
dbCounter = dbCounter.Where("login_logs.client_ip like ?", "%"+clientIp+"%") dbCounter = dbCounter.Where("client_ip like ?", "%"+clientIp+"%")
}
if state != "" {
db = db.Where("state = ?", state)
dbCounter = dbCounter.Where("state = ?", state)
} }
err = dbCounter.Count(&total).Error err = dbCounter.Count(&total).Error
@ -35,20 +42,26 @@ func (r LoginLogRepository) Find(pageIndex, pageSize int, userId, clientIp strin
return nil, 0, err return nil, 0, err
} }
err = db.Order("login_logs.login_time desc").Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&o).Error err = db.Order("login_time desc").Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&o).Error
if o == nil { if o == nil {
o = make([]model.LoginLogForPage, 0) o = make([]model.LoginLog, 0)
} }
return return
} }
func (r LoginLogRepository) FindAliveLoginLogs() (o []model.LoginLog, err error) { func (r LoginLogRepository) FindAliveLoginLogs() (o []model.LoginLog, err error) {
err = r.DB.Where("logout_time is null").Find(&o).Error err = r.DB.Where("state = '1' and logout_time is null").Find(&o).Error
return return
} }
func (r LoginLogRepository) FindAliveLoginLogsByUserId(userId string) (o []model.LoginLog, err error) { func (r LoginLogRepository) FindAliveLoginLogsByUsername(username string) (o []model.LoginLog, err error) {
err = r.DB.Where("logout_time is null and user_id = ?", userId).Find(&o).Error err = r.DB.Where("state = '1' and logout_time is null and username = ?", username).Find(&o).Error
return
}
func (r LoginLogRepository) FindOutTimeLog(dayLimit int) (o []model.LoginLog, err error) {
limitTime := time.Now().Add(time.Duration(-dayLimit*24) * time.Hour)
err = r.DB.Where("(state = '0' and login_time < ?) or (state = '1' and logout_time < ?) or (state is null and logout_time < ?)", limitTime, limitTime, limitTime).Find(&o).Error
return return
} }
@ -60,6 +73,10 @@ func (r LoginLogRepository) DeleteByIdIn(ids []string) (err error) {
return r.DB.Where("id in ?", ids).Delete(&model.LoginLog{}).Error return r.DB.Where("id in ?", ids).Delete(&model.LoginLog{}).Error
} }
func (r LoginLogRepository) DeleteById(id string) (err error) {
return r.DB.Where("id = ?", id).Delete(&model.LoginLog{}).Error
}
func (r LoginLogRepository) FindById(id string) (o model.LoginLog, err error) { func (r LoginLogRepository) FindById(id string) (o model.LoginLog, err error) {
err = r.DB.Where("id = ?", id).First(&o).Error err = r.DB.Where("id = ?", id).First(&o).Error
return return

View File

@ -1,26 +0,0 @@
package repository
import (
"next-terminal/server/model"
"gorm.io/gorm"
)
type NumRepository struct {
DB *gorm.DB
}
func NewNumRepository(db *gorm.DB) *NumRepository {
numRepository = &NumRepository{DB: db}
return numRepository
}
func (r NumRepository) FindAll() (o []model.Num, err error) {
err = r.DB.Find(&o).Error
return
}
func (r NumRepository) Create(o *model.Num) (err error) {
err = r.DB.Create(o).Error
return
}

View File

@ -1,7 +1,6 @@
package repository package repository
import ( import (
"next-terminal/pkg/guacd"
"next-terminal/server/model" "next-terminal/server/model"
"gorm.io/gorm" "gorm.io/gorm"
@ -33,6 +32,10 @@ func (r PropertyRepository) UpdateByName(o *model.Property, name string) error {
return r.DB.Updates(o).Error return r.DB.Updates(o).Error
} }
func (r PropertyRepository) DeleteByName(name string) error {
return r.DB.Where("name = ?", name).Delete(model.Property{}).Error
}
func (r PropertyRepository) FindByName(name string) (o model.Property, err error) { func (r PropertyRepository) FindByName(name string) (o model.Property, err error) {
err = r.DB.Where("name = ?", name).First(&o).Error err = r.DB.Where("name = ?", name).First(&o).Error
return return
@ -46,19 +49,3 @@ func (r PropertyRepository) FindAllMap() map[string]string {
} }
return propertyMap return propertyMap
} }
func (r PropertyRepository) GetDrivePath() (string, error) {
property, err := r.FindByName(guacd.DrivePath)
if err != nil {
return "", err
}
return property.Value, nil
}
func (r PropertyRepository) GetRecordingPath() (string, error) {
property, err := r.FindByName(guacd.RecordingPath)
if err != nil {
return "", err
}
return property.Value, nil
}

View File

@ -18,14 +18,6 @@ func NewResourceSharerRepository(db *gorm.DB) *ResourceSharerRepository {
return resourceSharerRepository return resourceSharerRepository
} }
func (r *ResourceSharerRepository) FindUserIdsByResourceId(resourceId string) (o []string, err error) {
err = r.DB.Table("resource_sharers").Select("user_id").Where("resource_id = ?", resourceId).Find(&o).Error
if o == nil {
o = make([]string, 0)
}
return
}
func (r *ResourceSharerRepository) OverwriteUserIdsByResourceId(resourceId, resourceType string, userIds []string) (err error) { func (r *ResourceSharerRepository) OverwriteUserIdsByResourceId(resourceId, resourceType string, userIds []string) (err error) {
db := r.DB.Begin() db := r.DB.Begin()
@ -104,7 +96,7 @@ func (r *ResourceSharerRepository) DeleteResourceSharerByResourceId(resourceId s
return r.DB.Where("resource_id = ?", resourceId).Delete(&model.ResourceSharer{}).Error return r.DB.Where("resource_id = ?", resourceId).Delete(&model.ResourceSharer{}).Error
} }
func (r *ResourceSharerRepository) AddSharerResources(userGroupId, userId, resourceType string, resourceIds []string) error { func (r *ResourceSharerRepository) AddSharerResources(userGroupId, userId, strategyId, resourceType string, resourceIds []string) error {
return r.DB.Transaction(func(tx *gorm.DB) (err error) { return r.DB.Transaction(func(tx *gorm.DB) (err error) {
for i := range resourceIds { for i := range resourceIds {
@ -138,11 +130,13 @@ func (r *ResourceSharerRepository) AddSharerResources(userGroupId, userId, resou
return echo.NewHTTPError(400, "参数错误") return echo.NewHTTPError(400, "参数错误")
} }
// 保证同一个资产只能分配给一个用户或者组
id := utils.Sign([]string{resourceId, resourceType, userId, userGroupId}) id := utils.Sign([]string{resourceId, resourceType, userId, userGroupId})
resource := &model.ResourceSharer{ resource := &model.ResourceSharer{
ID: id, ID: id,
ResourceId: resourceId, ResourceId: resourceId,
ResourceType: resourceType, ResourceType: resourceType,
StrategyId: strategyId,
UserId: userId, UserId: userId,
UserGroupId: userGroupId, UserGroupId: userGroupId,
} }
@ -192,3 +186,35 @@ func (r *ResourceSharerRepository) FindAssetIdsByUserId(userId string) (assetIds
return return
} }
func (r *ResourceSharerRepository) FindByResourceIdAndUserId(assetId, userId string) (resourceSharers []model.ResourceSharer, err error) {
// 查询其他用户授权给该用户的资产
groupIds, err := userGroupRepository.FindUserGroupIdsByUserId(userId)
if err != nil {
return
}
db := r.DB.Where("( resource_id = ? and user_id = ? )", assetId, userId)
if len(groupIds) > 0 {
db = db.Or("user_group_id in ?", groupIds)
}
err = db.Find(&resourceSharers).Error
return
}
func (r *ResourceSharerRepository) Find(resourceId, resourceType, userId, userGroupId string) (resourceSharers []model.ResourceSharer, err error) {
db := r.DB
if resourceId != "" {
db = db.Where("resource_id = ?")
}
if resourceType != "" {
db = db.Where("resource_type = ?")
}
if userId != "" {
db = db.Where("user_id = ?")
}
if userGroupId != "" {
db = db.Where("user_group_id = ?")
}
err = db.Find(&resourceSharers).Error
return
}

View File

@ -6,8 +6,8 @@ import (
"path" "path"
"time" "time"
"next-terminal/pkg/constant" "next-terminal/server/config"
"next-terminal/pkg/global" "next-terminal/server/constant"
"next-terminal/server/model" "next-terminal/server/model"
"next-terminal/server/utils" "next-terminal/server/utils"
@ -110,7 +110,7 @@ func (r SessionRepository) Decrypt(item *model.Session) error {
if err != nil { if err != nil {
return err return err
} }
decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword) decryptedCBC, err := utils.AesDecryptCBC(origData, config.GlobalCfg.EncryptionPassword)
if err != nil { if err != nil {
return err return err
} }
@ -121,7 +121,7 @@ func (r SessionRepository) Decrypt(item *model.Session) error {
if err != nil { if err != nil {
return err return err
} }
decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword) decryptedCBC, err := utils.AesDecryptCBC(origData, config.GlobalCfg.EncryptionPassword)
if err != nil { if err != nil {
return err return err
} }
@ -132,7 +132,7 @@ func (r SessionRepository) Decrypt(item *model.Session) error {
if err != nil { if err != nil {
return err return err
} }
decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword) decryptedCBC, err := utils.AesDecryptCBC(origData, config.GlobalCfg.EncryptionPassword)
if err != nil { if err != nil {
return err return err
} }
@ -164,12 +164,9 @@ func (r SessionRepository) DeleteById(id string) error {
} }
func (r SessionRepository) DeleteByIds(sessionIds []string) error { func (r SessionRepository) DeleteByIds(sessionIds []string) error {
drivePath, err := propertyRepository.GetRecordingPath() recordingPath := config.GlobalCfg.Guacd.Recording
if err != nil {
return err
}
for i := range sessionIds { for i := range sessionIds {
if err := os.RemoveAll(path.Join(drivePath, sessionIds[i])); err != nil { if err := os.RemoveAll(path.Join(recordingPath, sessionIds[i])); err != nil {
return err return err
} }
if err := r.DeleteById(sessionIds[i]); err != nil { if err := r.DeleteById(sessionIds[i]); err != nil {
@ -188,35 +185,27 @@ func (r SessionRepository) CountOnlineSession() (total int64, err error) {
return return
} }
type D struct {
Day string `json:"day"`
Count int `json:"count"`
Protocol string `json:"protocol"`
}
func (r SessionRepository) CountSessionByDay(day int) (results []D, err error) {
today := time.Now().Format("20060102")
sql := "select t1.`day`, count(t2.id) as count\nfrom (\n SELECT @date := DATE_ADD(@date, INTERVAL - 1 DAY) day\n FROM (SELECT @date := DATE_ADD('" + today + "', INTERVAL + 1 DAY) FROM nums) as t0\n LIMIT ?\n )\n as t1\n left join\n (\n select DATE(s.connected_time) as day, s.id\n from sessions as s\n WHERE protocol = ? and DATE(connected_time) <= '" + today + "'\n AND DATE(connected_time) > DATE_SUB('" + today + "', INTERVAL ? DAY)\n ) as t2 on t1.day = t2.day\ngroup by t1.day"
protocols := []string{"rdp", "ssh", "vnc", "telnet"}
for i := range protocols {
var result []D
err = r.DB.Raw(sql, day, protocols[i], day).Scan(&result).Error
if err != nil {
return nil, err
}
for j := range result {
result[j].Protocol = protocols[i]
}
results = append(results, result...)
}
return
}
func (r SessionRepository) EmptyPassword() error { func (r SessionRepository) EmptyPassword() error {
sql := "update sessions set password = '-',private_key = '-', passphrase = '-' where 1=1" sql := "update sessions set password = '-',private_key = '-', passphrase = '-' where 1=1"
return r.DB.Exec(sql).Error return r.DB.Exec(sql).Error
} }
func (r SessionRepository) CountByStatus(status string) (total int64, err error) {
err = r.DB.Find(&model.Session{}).Where("status = ?", status).Count(&total).Error
return
}
func (r SessionRepository) OverviewAccess(account model.User) (o []model.SessionForAccess, err error) {
db := r.DB
if constant.TypeUser == account.Type {
sql := "SELECT s.asset_id, s.ip, s.port, s.protocol, s.username, count(s.asset_id) AS access_count FROM sessions AS s where s.creator = ? GROUP BY s.asset_id, s.ip, s.port, s.protocol, s.username ORDER BY access_count DESC limit 10"
err = db.Raw(sql, []string{account.ID}).Scan(&o).Error
} else {
sql := "SELECT s.asset_id, s.ip, s.port, s.protocol, s.username, count(s.asset_id) AS access_count FROM sessions AS s GROUP BY s.asset_id, s.ip, s.port, s.protocol, s.username ORDER BY access_count DESC limit 10"
err = db.Raw(sql).Scan(&o).Error
}
if o == nil {
o = make([]model.SessionForAccess, 0)
}
return
}

View File

@ -0,0 +1,87 @@
package repository
import (
"next-terminal/server/model"
"gorm.io/gorm"
)
type StorageRepository struct {
DB *gorm.DB
}
func NewStorageRepository(db *gorm.DB) *StorageRepository {
storageRepository = &StorageRepository{DB: db}
return storageRepository
}
func (r StorageRepository) Find(pageIndex, pageSize int, name, order, field string) (o []model.StorageForPage, total int64, err error) {
m := model.Storage{}
db := r.DB.Table(m.TableName()).Select("storages.id,storages.name,storages.is_share,storages.limit_size,storages.is_default,storages.owner,storages.created, users.nickname as owner_name").Joins("left join users on storages.owner = users.id")
dbCounter := r.DB.Table(m.TableName())
if len(name) > 0 {
db = db.Where("name like ?", "%"+name+"%")
dbCounter = dbCounter.Where("name like ?", "%"+name+"%")
}
err = dbCounter.Count(&total).Error
if err != nil {
return nil, 0, err
}
if order == "ascend" {
order = "asc"
} else {
order = "desc"
}
if field == "name" {
field = "storages.name"
} else {
field = "storages.created"
}
err = db.Order(field + " " + order).Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Error
if o == nil {
o = make([]model.StorageForPage, 0)
}
return
}
func (r StorageRepository) FindShares() (o []model.Storage, err error) {
m := model.Storage{}
db := r.DB.Table(m.TableName()).Where("is_share = 1")
err = db.Find(&o).Error
return
}
func (r StorageRepository) DeleteById(id string) error {
return r.DB.Where("id = ?", id).Delete(model.Storage{}).Error
}
func (r StorageRepository) Create(m *model.Storage) error {
return r.DB.Create(m).Error
}
func (r StorageRepository) UpdateById(o *model.Storage, id string) error {
o.ID = id
return r.DB.Updates(o).Error
}
func (r StorageRepository) FindByOwnerIdAndDefault(owner string, isDefault bool) (m model.Storage, err error) {
err = r.DB.Where("owner = ? and is_default = ?", owner, isDefault).First(&m).Error
return
}
func (r StorageRepository) FindById(id string) (m model.Storage, err error) {
err = r.DB.Where("id = ?", id).First(&m).Error
return
}
func (r StorageRepository) FindAll() (o []model.Storage) {
if r.DB.Find(&o).Error != nil {
return nil
}
return
}

View File

@ -0,0 +1,73 @@
package repository
import (
"next-terminal/server/model"
"gorm.io/gorm"
)
type StrategyRepository struct {
DB *gorm.DB
}
func NewStrategyRepository(db *gorm.DB) *StrategyRepository {
strategyRepository = &StrategyRepository{DB: db}
return strategyRepository
}
func (r StrategyRepository) FindAll() (o []model.Strategy, err error) {
err = r.DB.Order("name desc").Find(&o).Error
return
}
func (r StrategyRepository) Find(pageIndex, pageSize int, name, order, field string) (o []model.Strategy, total int64, err error) {
m := model.Strategy{}
db := r.DB.Table(m.TableName())
dbCounter := r.DB.Table(m.TableName())
if len(name) > 0 {
db = db.Where("name like ?", "%"+name+"%")
dbCounter = dbCounter.Where("name like ?", "%"+name+"%")
}
err = dbCounter.Count(&total).Error
if err != nil {
return nil, 0, err
}
if order == "ascend" {
order = "asc"
} else {
order = "desc"
}
if field == "name" {
field = "name"
} else {
field = "created"
}
err = db.Order(field + " " + order).Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Error
if o == nil {
o = make([]model.Strategy, 0)
}
return
}
func (r StrategyRepository) DeleteById(id string) error {
return r.DB.Where("id = ?", id).Delete(model.Strategy{}).Error
}
func (r StrategyRepository) Create(m *model.Strategy) error {
return r.DB.Create(m).Error
}
func (r StrategyRepository) UpdateById(o *model.Strategy, id string) error {
o.ID = id
return r.DB.Updates(o).Error
}
func (r StrategyRepository) FindById(id string) (m model.Strategy, err error) {
err = r.DB.Where("id = ?", id).First(&m).Error
return
}

View File

@ -1,7 +1,7 @@
package repository package repository
import ( import (
"next-terminal/pkg/constant" "next-terminal/server/constant"
"next-terminal/server/model" "next-terminal/server/model"
"gorm.io/gorm" "gorm.io/gorm"
@ -105,9 +105,9 @@ func (r UserRepository) Update(o *model.User) error {
return r.DB.Updates(o).Error return r.DB.Updates(o).Error
} }
func (r UserRepository) UpdateOnline(id string, online bool) error { func (r UserRepository) UpdateOnlineByUsername(username string, online bool) error {
sql := "update users set online = ? where id = ?" sql := "update users set online = ? where username = ?"
return r.DB.Exec(sql, online, id).Error return r.DB.Exec(sql, online, username).Error
} }
func (r UserRepository) DeleteById(id string) error { func (r UserRepository) DeleteById(id string) error {
@ -135,3 +135,8 @@ func (r UserRepository) CountOnlineUser() (total int64, err error) {
err = r.DB.Where("online = ?", true).Find(&model.User{}).Count(&total).Error err = r.DB.Where("online = ?", true).Find(&model.User{}).Count(&total).Error
return return
} }
func (r UserRepository) Count() (total int64, err error) {
err = r.DB.Find(&model.User{}).Count(&total).Error
return
}

View File

@ -0,0 +1,65 @@
package service
import (
"next-terminal/server/global/gateway"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/term"
)
type AccessGatewayService struct {
accessGatewayRepository *repository.AccessGatewayRepository
}
func NewAccessGatewayService(accessGatewayRepository *repository.AccessGatewayRepository) *AccessGatewayService {
accessGatewayService = &AccessGatewayService{accessGatewayRepository: accessGatewayRepository}
return accessGatewayService
}
func (r AccessGatewayService) GetGatewayAndReconnectById(accessGatewayId string) (g *gateway.Gateway, err error) {
g = gateway.GlobalGatewayManager.GetById(accessGatewayId)
if g == nil || !g.Connected {
accessGateway, err := r.accessGatewayRepository.FindById(accessGatewayId)
if err != nil {
return nil, err
}
g = r.ReConnect(&accessGateway)
}
return g, nil
}
func (r AccessGatewayService) GetGatewayById(accessGatewayId string) (g *gateway.Gateway, err error) {
g = gateway.GlobalGatewayManager.GetById(accessGatewayId)
return g, nil
}
func (r AccessGatewayService) ReConnectAll() error {
gateways, err := r.accessGatewayRepository.FindAll()
if err != nil {
return err
}
for i := range gateways {
r.ReConnect(&gateways[i])
}
return nil
}
func (r AccessGatewayService) ReConnect(m *model.AccessGateway) *gateway.Gateway {
log.Debugf("重建接入网关「%v」中...", m.Name)
r.DisconnectById(m.ID)
sshClient, err := term.NewSshClient(m.IP, m.Port, m.Username, m.Password, m.PrivateKey, m.Passphrase)
var g *gateway.Gateway
if err != nil {
g = gateway.NewGateway(m.ID, m.Localhost, false, err.Error(), nil)
} else {
g = gateway.NewGateway(m.ID, m.Localhost, true, "", sshClient)
}
gateway.GlobalGatewayManager.Add <- g
log.Debugf("重建接入网关「%v」完成", m.Name)
return g
}
func (r AccessGatewayService) DisconnectById(accessGatewayId string) {
gateway.GlobalGatewayManager.Del <- accessGatewayId
}

60
server/service/asset.go Normal file
View File

@ -0,0 +1,60 @@
package service
import (
"next-terminal/server/config"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
type AssetService struct {
assetRepository *repository.AssetRepository
}
func NewAssetService(assetRepository *repository.AssetRepository) *AssetService {
return &AssetService{assetRepository: assetRepository}
}
func (r AssetService) Encrypt() error {
items, err := r.assetRepository.FindAll()
if err != nil {
return err
}
for i := range items {
item := items[i]
if item.Encrypted {
continue
}
if err := r.assetRepository.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil {
return err
}
if err := r.assetRepository.UpdateById(&item, item.ID); err != nil {
return err
}
}
return nil
}
func (r AssetService) CheckStatus(accessGatewayId string, ip string, port int) (active bool, err error) {
if accessGatewayId != "" && accessGatewayId != "-" {
g, e1 := accessGatewayService.GetGatewayAndReconnectById(accessGatewayId)
if err != nil {
return false, e1
}
uuid := utils.UUID()
exposedIP, exposedPort, e2 := g.OpenSshTunnel(uuid, ip, port)
if e2 != nil {
return false, e2
}
defer g.CloseSshTunnel(uuid)
if g.Connected {
active, err = utils.Tcping(exposedIP, exposedPort)
} else {
active = false
}
} else {
active, err = utils.Tcping(ip, port)
}
return active, err
}

View File

@ -1,7 +1,7 @@
package service package service
import ( import (
"next-terminal/pkg/global" "next-terminal/server/config"
"next-terminal/server/repository" "next-terminal/server/repository"
) )
@ -23,7 +23,7 @@ func (r CredentialService) Encrypt() error {
if item.Encrypted { if item.Encrypted {
continue continue
} }
if err := r.credentialRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil { if err := r.credentialRepository.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil {
return err return err
} }
if err := r.credentialRepository.UpdateById(&item, item.ID); err != nil { if err := r.credentialRepository.UpdateById(&item, item.ID); err != nil {

View File

@ -0,0 +1,5 @@
package service
var (
accessGatewayService *AccessGatewayService
)

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