release v1.2.0
@ -1,7 +1,6 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Go template
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
@ -17,4 +16,12 @@
|
||||
# vendor/
|
||||
|
||||
.gitignore
|
||||
bin
|
||||
data
|
||||
docs
|
||||
guacd
|
||||
logs
|
||||
playground
|
||||
screenshot
|
||||
web/node_modules/
|
||||
.dockerignore
|
||||
|
4
.gitignore
vendored
@ -27,3 +27,7 @@ playground/drive
|
||||
playground/recording
|
||||
|
||||
/log
|
||||
|
||||
|
||||
# next terminal
|
||||
/recording
|
27
Dockerfile
@ -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 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
|
||||
USER root
|
||||
FROM alpine:latest
|
||||
|
||||
LABEL MAINTAINER="helloworld1024@foxmail.com"
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
|
||||
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 TZ Asia/Shanghai
|
||||
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_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
|
||||
RUN ln -snf /usr/share/zoneinfo/$TIME_ZONE /etc/localtime && echo $TIME_ZONE > /etc/timezone
|
||||
|
||||
WORKDIR /usr/local/next-terminal
|
||||
RUN touch config.yml
|
||||
|
||||
COPY --from=builder /app/next-terminal ./
|
||||
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 mkdir recording && mkdir drive
|
||||
ENTRYPOINT /usr/bin/supervisord
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
|
||||
RUN apk add tzdata
|
||||
RUN cp /usr/share/zoneinfo/${TZ} /etc/localtime
|
||||
RUN echo ${TZ} > /etc/timezone
|
||||
ENTRYPOINT ./next-terminal
|
@ -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
@ -1,281 +1,620 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
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.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
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".
|
||||
0. Definitions.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
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.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
1. You may copy and distribute 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 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.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
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.)
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
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.
|
||||
1. Source Code.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
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.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
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.
|
||||
2. Basic Permissions.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
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.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU 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
|
||||
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.
|
||||
14. Revised Versions of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
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.
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
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
|
||||
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.
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE 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.
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
@ -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 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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU 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
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, 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.
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
85
README.md
@ -1,6 +1,6 @@
|
||||
# Next Terminal
|
||||
|
||||
**注意:目前最后一个开源版是 0.5.0 版本,后续版本暂未开源。新版本 Docker 容器不再是 All in one 方式,拆分为了 guacd 和 next-terminal 两个容器。**
|
||||
**说明:最新版前后端代码已经全部开源,1.2.x 及之后版本已去除授权限制。**
|
||||
|
||||

|
||||
|
||||
@ -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
|
||||
|
||||
## 相关截图
|
||||
## 问题反馈
|
||||
|
||||
### 资源占用
|
||||
请提 Issues
|
||||
|
||||
未使用时资源占用非常小
|
||||
## License
|
||||
|
||||

|
||||
|
||||
### 控制面板
|
||||
|
||||
更方便的概览系统信息
|
||||
|
||||

|
||||
|
||||
### 资产管理
|
||||
|
||||
支持多种RDP、SSH、Telnet、VNC,Kubernetes等多种协议的资产
|
||||
|
||||

|
||||
|
||||
#### rdp
|
||||
|
||||

|
||||
|
||||
#### vnc
|
||||
|
||||

|
||||
|
||||
#### ssh
|
||||
|
||||

|
||||
|
||||
### 授权凭证
|
||||
|
||||
极为方便的复用资产认证信息
|
||||
|
||||

|
||||
|
||||
批量执行命令
|
||||
|
||||

|
||||
|
||||
### 在线监控
|
||||
|
||||
实时监控用户的操作,并可以随时断开该会话
|
||||
|
||||

|
||||
|
||||
### 离线回放
|
||||
|
||||
详细的数据回放,定位任何一个可疑操作
|
||||
|
||||

|
||||
|
||||
### 计划任务
|
||||
|
||||
自定义计划任务
|
||||
|
||||

|
||||
|
||||
### 访问安全
|
||||
|
||||
黑白名单访问控制,支持ip、cidr及连续IP
|
||||
|
||||

|
||||
|
||||
### 用户组授权
|
||||
|
||||
灵活的授权策略
|
||||
|
||||

|
||||
|
||||
## 联系方式
|
||||
|
||||
- QQ群 938145268
|
||||
|
||||
- Telegram
|
||||
|
||||
https://t.me/next_terminal
|
||||
Next Terminal 使用 [AGPL-3.0](./LICENSE) 开源协议,请自觉准守。
|
7
build.sh
Normal 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
|
16
config.yml
@ -8,6 +8,18 @@ mysql:
|
||||
password: next-terminal
|
||||
database: next-terminal
|
||||
sqlite:
|
||||
file: 'next-terminal.db'
|
||||
file: './data/sqlite/next-terminal.db'
|
||||
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
@ -1,14 +1,17 @@
|
||||
module next-terminal
|
||||
|
||||
go 1.13
|
||||
go 1.16
|
||||
|
||||
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/gorilla/websocket v1.4.2
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||
github.com/labstack/echo/v4 v4.1.17
|
||||
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/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.12.0
|
||||
@ -18,7 +21,8 @@ require (
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/spf13/viper v1.7.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
|
||||
gorm.io/driver/mysql v1.0.3
|
||||
gorm.io/driver/sqlite v1.1.4
|
||||
|
377
go.sum
@ -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
@ -1,20 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"next-terminal/pkg/config"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/task"
|
||||
"next-terminal/server/api"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/repository"
|
||||
"next-terminal/server/task"
|
||||
|
||||
"github.com/labstack/gommon/log"
|
||||
)
|
||||
|
||||
const Version = "v0.5.0"
|
||||
|
||||
func main() {
|
||||
err := Run()
|
||||
if err != nil {
|
||||
@ -24,47 +22,41 @@ func main() {
|
||||
|
||||
func Run() error {
|
||||
|
||||
fmt.Printf(`
|
||||
_______ __ ___________ .__ .__
|
||||
\ \ ____ ___ ____/ |_ \__ ___/__________ _____ |__| ____ _____ | |
|
||||
/ | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | |
|
||||
/ | \ ___/ > < | | | |\ ___/| | \/ Y Y \ | | \/ __ \| |__
|
||||
\____|__ /\___ >__/\_ \ |__| |____| \___ >__| |__|_| /__|___| (____ /____/
|
||||
\/ \/ \/ \/ \/ \/ \/ ` + Version + "\n\n")
|
||||
fmt.Printf(constant.Banner, constant.Version)
|
||||
|
||||
// 为了兼容之前调用global包的代码 后期预期会改为调用pgk/config
|
||||
global.Config = config.GlobalCfg
|
||||
|
||||
if global.Config.EncryptionKey == "" {
|
||||
global.Config.EncryptionKey = "next-terminal"
|
||||
if config.GlobalCfg.Debug {
|
||||
jsonBytes, err := json.MarshalIndent(config.GlobalCfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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()
|
||||
e := api.SetupRoutes(db)
|
||||
|
||||
if global.Config.ResetPassword != "" {
|
||||
return api.ResetPassword(global.Config.ResetPassword)
|
||||
if config.GlobalCfg.ResetPassword != "" {
|
||||
return api.ResetPassword(config.GlobalCfg.ResetPassword)
|
||||
}
|
||||
if global.Config.ResetTotp != "" {
|
||||
return api.ResetTotp(global.Config.ResetTotp)
|
||||
if config.GlobalCfg.ResetTotp != "" {
|
||||
return api.ResetTotp(config.GlobalCfg.ResetTotp)
|
||||
}
|
||||
|
||||
if global.Config.NewEncryptionKey != "" {
|
||||
return api.ChangeEncryptionKey(global.Config.EncryptionKey, global.Config.NewEncryptionKey)
|
||||
if config.GlobalCfg.NewEncryptionKey != "" {
|
||||
return api.ChangeEncryptionKey(config.GlobalCfg.EncryptionKey, config.GlobalCfg.NewEncryptionKey)
|
||||
}
|
||||
|
||||
sessionRepo := repository.NewSessionRepository(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()
|
||||
|
||||
if global.Config.Server.Cert != "" && global.Config.Server.Key != "" {
|
||||
return e.StartTLS(global.Config.Server.Addr, global.Config.Server.Cert, global.Config.Server.Key)
|
||||
if config.GlobalCfg.Server.Cert != "" && config.GlobalCfg.Server.Key != "" {
|
||||
return e.StartTLS(config.GlobalCfg.Server.Addr, config.GlobalCfg.Server.Cert, config.GlobalCfg.Server.Key)
|
||||
} else {
|
||||
return e.Start(global.Config.Server.Addr)
|
||||
return e.Start(config.GlobalCfg.Server.Addr)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
@ -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
|
||||
}
|
@ -1 +0,0 @@
|
||||
资产名称,ssh,127.0.0.1,22,username,password,privateKey,passphrase,description
|
|
Before Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 125 KiB |
Before Width: | Height: | Size: 304 KiB |
Before Width: | Height: | Size: 119 KiB |
Before Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 231 KiB |
Before Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 218 KiB |
Before Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 552 KiB |
119
server/api/access_gateway.go
Normal 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, "")
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/totp"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/global/cache"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/totp"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -40,11 +43,6 @@ type Authorization struct {
|
||||
User model.User
|
||||
}
|
||||
|
||||
//
|
||||
//type UserServer struct {
|
||||
// repository.UserRepository
|
||||
//}
|
||||
|
||||
func LoginEndpoint(c echo.Context) error {
|
||||
var loginAccount LoginAccount
|
||||
if err := c.Bind(&loginAccount); err != nil {
|
||||
@ -54,25 +52,33 @@ func LoginEndpoint(c echo.Context) error {
|
||||
user, err := userRepository.FindByUsername(loginAccount.Username)
|
||||
|
||||
// 存储登录失败次数信息
|
||||
loginFailCountKey := loginAccount.Username
|
||||
v, ok := global.Cache.Get(loginFailCountKey)
|
||||
loginFailCountKey := c.RealIP() + loginAccount.Username
|
||||
v, ok := cache.GlobalCache.Get(loginFailCountKey)
|
||||
if !ok {
|
||||
v = 1
|
||||
}
|
||||
count := v.(int)
|
||||
if count >= 5 {
|
||||
return Fail(c, -1, "登录失败次数过多,请稍后再试")
|
||||
return Fail(c, -1, "登录失败次数过多,请等待5分钟后再试")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -80,15 +86,42 @@ func LoginEndpoint(c echo.Context) error {
|
||||
return Fail(c, 0, "")
|
||||
}
|
||||
|
||||
token, err := LoginSuccess(c, loginAccount, user)
|
||||
token, err := LoginSuccess(loginAccount, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 保存登录日志
|
||||
if err := SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, true, loginAccount.Remember, token, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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()}, "")
|
||||
|
||||
authorization := Authorization{
|
||||
@ -97,45 +130,20 @@ func LoginSuccess(c echo.Context, loginAccount LoginAccount, user model.User) (t
|
||||
User: user,
|
||||
}
|
||||
|
||||
cacheKey := BuildCacheKeyByToken(token)
|
||||
cacheKey := userService.BuildCacheKeyByToken(token)
|
||||
|
||||
if authorization.Remember {
|
||||
// 记住登录有效期两周
|
||||
global.Cache.Set(cacheKey, authorization, RememberEffectiveTime)
|
||||
cache.GlobalCache.Set(cacheKey, authorization, RememberEffectiveTime)
|
||||
} else {
|
||||
global.Cache.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
|
||||
cache.GlobalCache.Set(cacheKey, authorization, NotRememberEffectiveTime)
|
||||
}
|
||||
|
||||
// 修改登录状态
|
||||
err = userRepository.Update(&model.User{Online: true, ID: user.ID})
|
||||
|
||||
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 {
|
||||
var loginAccount LoginAccount
|
||||
if err := c.Bind(&loginAccount); err != nil {
|
||||
@ -143,47 +151,63 @@ func loginWithTotpEndpoint(c echo.Context) error {
|
||||
}
|
||||
|
||||
// 存储登录失败次数信息
|
||||
loginFailCountKey := loginAccount.Username
|
||||
v, ok := global.Cache.Get(loginFailCountKey)
|
||||
loginFailCountKey := c.RealIP() + loginAccount.Username
|
||||
v, ok := cache.GlobalCache.Get(loginFailCountKey)
|
||||
if !ok {
|
||||
v = 1
|
||||
}
|
||||
count := v.(int)
|
||||
if count >= 5 {
|
||||
return Fail(c, -1, "登录失败次数过多,请稍后再试")
|
||||
return Fail(c, -1, "登录失败次数过多,请等待5分钟后再试")
|
||||
}
|
||||
|
||||
user, err := userRepository.FindByUsername(loginAccount.Username)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if !totp.Validate(loginAccount.TOTP, user.TOTPSecret) {
|
||||
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)
|
||||
}
|
||||
|
||||
token, err := LoginSuccess(c, loginAccount, user)
|
||||
token, err := LoginSuccess(loginAccount, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 保存登录日志
|
||||
if err := SaveLoginLog(c.RealIP(), c.Request().UserAgent(), loginAccount.Username, true, loginAccount.Remember, token, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Success(c, token)
|
||||
}
|
||||
|
||||
func LogoutEndpoint(c echo.Context) error {
|
||||
token := GetToken(c)
|
||||
cacheKey := BuildCacheKeyByToken(token)
|
||||
global.Cache.Delete(cacheKey)
|
||||
cacheKey := userService.BuildCacheKeyByToken(token)
|
||||
cache.GlobalCache.Delete(cacheKey)
|
||||
err := userService.Logout(token)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -192,7 +216,7 @@ func LogoutEndpoint(c echo.Context) error {
|
||||
}
|
||||
|
||||
func ConfirmTOTPEndpoint(c echo.Context) error {
|
||||
if global.Config.Demo {
|
||||
if config.GlobalCfg.Demo {
|
||||
return Fail(c, 0, "演示模式禁止开启两步验证")
|
||||
}
|
||||
account, _ := GetCurrentAccount(c)
|
||||
@ -258,7 +282,7 @@ func ResetTOTPEndpoint(c echo.Context) error {
|
||||
}
|
||||
|
||||
func ChangePasswordEndpoint(c echo.Context) error {
|
||||
if global.Config.Demo {
|
||||
if config.GlobalCfg.Demo {
|
||||
return Fail(c, 0, "演示模式禁止修改密码")
|
||||
}
|
||||
account, _ := GetCurrentAccount(c)
|
||||
@ -313,3 +337,48 @@ func InfoEndpoint(c echo.Context) error {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/global/cache"
|
||||
"next-terminal/server/model"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -41,17 +41,17 @@ func NotFound(c echo.Context, message string) error {
|
||||
}
|
||||
|
||||
func GetToken(c echo.Context) string {
|
||||
token := c.Request().Header.Get(Token)
|
||||
token := c.Request().Header.Get(constant.Token)
|
||||
if len(token) > 0 {
|
||||
return token
|
||||
}
|
||||
return c.QueryParam(Token)
|
||||
return c.QueryParam(constant.Token)
|
||||
}
|
||||
|
||||
func GetCurrentAccount(c echo.Context) (model.User, bool) {
|
||||
token := GetToken(c)
|
||||
cacheKey := BuildCacheKeyByToken(token)
|
||||
get, b := global.Cache.Get(cacheKey)
|
||||
cacheKey := userService.BuildCacheKeyByToken(token)
|
||||
get, b := cache.GlobalCache.Get(cacheKey)
|
||||
if b {
|
||||
return get.(Authorization).User, true
|
||||
}
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
@ -32,6 +32,7 @@ func AssetCreateEndpoint(c echo.Context) error {
|
||||
item.Owner = account.ID
|
||||
item.ID = utils.UUID()
|
||||
item.Created = utils.NowJsonTime()
|
||||
item.Active = true
|
||||
|
||||
if err := assetRepository.Create(&item); err != nil {
|
||||
return err
|
||||
@ -41,10 +42,12 @@ func AssetCreateEndpoint(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建后自动检测资产是否存活
|
||||
go func() {
|
||||
active := utils.Tcping(item.IP, item.Port)
|
||||
_ = assetRepository.UpdateActiveById(active, item.ID)
|
||||
active, _ := assetService.CheckStatus(item.AccessGatewayId, item.IP, item.Port)
|
||||
|
||||
if item.Active != active {
|
||||
_ = assetRepository.UpdateActiveById(active, item.ID)
|
||||
}
|
||||
}()
|
||||
|
||||
return Success(c, item)
|
||||
@ -74,7 +77,6 @@ func AssetImportEndpoint(c echo.Context) error {
|
||||
if total == 0 {
|
||||
return errors.New("csv数据为空")
|
||||
}
|
||||
|
||||
var successCount = 0
|
||||
var errorCount = 0
|
||||
m := echo.Map{}
|
||||
@ -97,6 +99,7 @@ func AssetImportEndpoint(c echo.Context) error {
|
||||
Description: record[8],
|
||||
Created: utils.NowJsonTime(),
|
||||
Owner: account.ID,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
if len(record) >= 10 {
|
||||
@ -110,11 +113,6 @@ func AssetImportEndpoint(c echo.Context) error {
|
||||
m[strconv.Itoa(i)] = err.Error()
|
||||
} else {
|
||||
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")
|
||||
|
||||
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
|
||||
@ -154,8 +153,7 @@ func AssetPagingEndpoint(c echo.Context) error {
|
||||
|
||||
func AssetAllEndpoint(c echo.Context) error {
|
||||
protocol := c.QueryParam("protocol")
|
||||
account, _ := GetCurrentAccount(c)
|
||||
items, _ := assetRepository.FindByProtocolAndUser(protocol, account)
|
||||
items, _ := assetRepository.FindByProtocol(protocol)
|
||||
return Success(c, items)
|
||||
}
|
||||
|
||||
@ -205,7 +203,7 @@ func AssetUpdateEndpoint(c echo.Context) error {
|
||||
item.Description = "-"
|
||||
}
|
||||
|
||||
if err := assetRepository.Encrypt(&item, global.Config.EncryptionPassword); err != nil {
|
||||
if err := assetRepository.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := assetRepository.UpdateById(&item, id); err != nil {
|
||||
@ -267,7 +265,7 @@ func AssetTcpingEndpoint(c echo.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
active := utils.Tcping(item.IP, item.Port)
|
||||
active, err := assetService.CheckStatus(item.AccessGatewayId, item.IP, item.Port)
|
||||
|
||||
if item.Active != active {
|
||||
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) {
|
||||
|
@ -29,6 +29,12 @@ func CommandCreateEndpoint(c echo.Context) error {
|
||||
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 {
|
||||
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
|
||||
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
|
||||
|
@ -6,8 +6,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
@ -106,7 +106,7 @@ func CredentialUpdateEndpoint(c echo.Context) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -121,7 +121,7 @@ func CredentialUpdateEndpoint(c echo.Context) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -131,7 +131,7 @@ func CredentialUpdateEndpoint(c echo.Context) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ func JobUpdateEndpoint(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
item.ID = id
|
||||
if err := jobRepository.UpdateById(&item); err != nil {
|
||||
if err := jobService.UpdateById(&item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ func JobDeleteEndpoint(c echo.Context) error {
|
||||
split := strings.Split(ids, ",")
|
||||
for i := range split {
|
||||
jobId := split[i]
|
||||
if err := jobRepository.DeleteJobById(jobId); err != nil {
|
||||
if err := jobService.DeleteJobById(jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/server/global/cache"
|
||||
"next-terminal/server/log"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
@ -13,10 +13,11 @@ import (
|
||||
func LoginLogPagingEndpoint(c echo.Context) error {
|
||||
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
|
||||
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
|
||||
userId := c.QueryParam("userId")
|
||||
username := c.QueryParam("username")
|
||||
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 {
|
||||
return err
|
||||
@ -33,7 +34,7 @@ func LoginLogDeleteEndpoint(c echo.Context) error {
|
||||
split := strings.Split(ids, ",")
|
||||
for i := range split {
|
||||
token := split[i]
|
||||
global.Cache.Delete(token)
|
||||
cache.GlobalCache.Delete(token)
|
||||
if err := userService.Logout(token); err != nil {
|
||||
log.WithError(err).Error("Cache Delete Failed")
|
||||
}
|
||||
@ -44,3 +45,10 @@ func LoginLogDeleteEndpoint(c echo.Context) error {
|
||||
|
||||
return Success(c, nil)
|
||||
}
|
||||
|
||||
//func LoginLogClearEndpoint(c echo.Context) error {
|
||||
// loginLogs, err := loginLogRepository.FindAliveLoginLogs()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//}
|
||||
|
@ -3,12 +3,12 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/global/cache"
|
||||
"next-terminal/server/global/security"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -33,27 +33,26 @@ func ErrorHandler(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
func TcpWall(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
|
||||
return func(c echo.Context) error {
|
||||
|
||||
if global.Securities == nil {
|
||||
securities := security.GlobalSecurityManager.Values()
|
||||
if len(securities) == 0 {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
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
|
||||
_, ipNet, err := net.ParseCIDR(security.IP)
|
||||
_, ipNet, err := net.ParseCIDR(s.IP)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !ipNet.Contains(net.ParseIP(ip)) {
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
@ -65,16 +64,16 @@ func TcpWall(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
}
|
||||
} else {
|
||||
// IP
|
||||
if security.IP != ip {
|
||||
if s.IP != ip {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if security.Rule == constant.AccessRuleAllow {
|
||||
if s.Rule == constant.AccessRuleAllow {
|
||||
return next(c)
|
||||
}
|
||||
if security.Rule == constant.AccessRuleReject {
|
||||
if c.Request().Header.Get("X-Requested-With") != "" || c.Request().Header.Get(Token) != "" {
|
||||
if s.Rule == constant.AccessRuleReject {
|
||||
if c.Request().Header.Get("X-Requested-With") != "" || c.Request().Header.Get(constant.Token) != "" {
|
||||
return Fail(c, 0, "您的访问请求被拒绝 :(")
|
||||
} else {
|
||||
return c.HTML(666, "您的访问请求被拒绝 :(")
|
||||
@ -88,10 +87,7 @@ func TcpWall(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
|
||||
func Auth(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
|
||||
startWithUrls := []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`)
|
||||
anonymousUrls := []string{"/login", "/static", "/favicon.ico", "/logo.svg", "/asciinema"}
|
||||
|
||||
return func(c echo.Context) error {
|
||||
|
||||
@ -100,32 +96,27 @@ func Auth(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return next(c)
|
||||
}
|
||||
// 路由拦截 - 登录身份、资源权限判断等
|
||||
for i := range startWithUrls {
|
||||
if strings.HasPrefix(uri, startWithUrls[i]) {
|
||||
for i := range anonymousUrls {
|
||||
if strings.HasPrefix(uri, anonymousUrls[i]) {
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
if download.FindString(uri) != "" {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
if recording.FindString(uri) != "" {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
token := GetToken(c)
|
||||
cacheKey := BuildCacheKeyByToken(token)
|
||||
authorization, found := global.Cache.Get(cacheKey)
|
||||
if token == "" {
|
||||
return Fail(c, 401, "您的登录信息已失效,请重新登录后再试。")
|
||||
}
|
||||
cacheKey := userService.BuildCacheKeyByToken(token)
|
||||
authorization, found := cache.GlobalCache.Get(cacheKey)
|
||||
if !found {
|
||||
return Fail(c, 401, "您的登录信息已失效,请重新登录后再试。")
|
||||
}
|
||||
|
||||
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 {
|
||||
global.Cache.Set(cacheKey, authorization, time.Hour*time.Duration(2))
|
||||
cache.GlobalCache.Set(cacheKey, authorization, time.Hour*time.Duration(2))
|
||||
}
|
||||
|
||||
return next(c)
|
||||
|
@ -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
|
||||
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/server/repository"
|
||||
"next-terminal/server/constant"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
@ -44,16 +43,43 @@ func OverviewCounterEndPoint(c echo.Context) error {
|
||||
return Success(c, counter)
|
||||
}
|
||||
|
||||
func OverviewSessionPoint(c echo.Context) (err error) {
|
||||
d := c.QueryParam("d")
|
||||
var results []repository.D
|
||||
if d == "m" {
|
||||
results, err = sessionRepository.CountSessionByDay(30)
|
||||
func OverviewAssetEndPoint(c echo.Context) error {
|
||||
account, _ := GetCurrentAccount(c)
|
||||
var (
|
||||
ssh int64
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
return Success(c, results)
|
||||
return Success(c, access)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
type RU struct {
|
||||
UserGroupId string `json:"userGroupId"`
|
||||
UserId string `json:"userId"`
|
||||
StrategyId string `json:"strategyId"`
|
||||
ResourceType string `json:"resourceType"`
|
||||
ResourceIds []string `json:"resourceIds"`
|
||||
}
|
||||
@ -19,26 +20,16 @@ type UR struct {
|
||||
|
||||
func RSGetSharersEndPoint(c echo.Context) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
var ru RU
|
||||
if err := c.Bind(&ru); err != nil {
|
||||
@ -58,7 +49,7 @@ func ResourceAddByUserIdAssignEndPoint(c echo.Context) error {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -5,27 +5,23 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/pkg/service"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/global/cache"
|
||||
"next-terminal/server/log"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/repository"
|
||||
"next-terminal/server/service"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
const Token = "X-Auth-Token"
|
||||
|
||||
var (
|
||||
userRepository *repository.UserRepository
|
||||
userGroupRepository *repository.UserGroupRepository
|
||||
@ -35,20 +31,23 @@ var (
|
||||
propertyRepository *repository.PropertyRepository
|
||||
commandRepository *repository.CommandRepository
|
||||
sessionRepository *repository.SessionRepository
|
||||
numRepository *repository.NumRepository
|
||||
accessSecurityRepository *repository.AccessSecurityRepository
|
||||
accessGatewayRepository *repository.AccessGatewayRepository
|
||||
jobRepository *repository.JobRepository
|
||||
jobLogRepository *repository.JobLogRepository
|
||||
loginLogRepository *repository.LoginLogRepository
|
||||
storageRepository *repository.StorageRepository
|
||||
strategyRepository *repository.StrategyRepository
|
||||
|
||||
jobService *service.JobService
|
||||
propertyService *service.PropertyService
|
||||
userService *service.UserService
|
||||
sessionService *service.SessionService
|
||||
mailService *service.MailService
|
||||
numService *service.NumService
|
||||
assetService *service.AssetService
|
||||
credentialService *service.CredentialService
|
||||
jobService *service.JobService
|
||||
propertyService *service.PropertyService
|
||||
userService *service.UserService
|
||||
sessionService *service.SessionService
|
||||
mailService *service.MailService
|
||||
assetService *service.AssetService
|
||||
credentialService *service.CredentialService
|
||||
storageService *service.StorageService
|
||||
accessGatewayService *service.AccessGatewayService
|
||||
)
|
||||
|
||||
func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
@ -56,8 +55,10 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
InitRepository(db)
|
||||
InitService()
|
||||
|
||||
cache.GlobalCache.OnEvicted(userService.OnEvicted)
|
||||
|
||||
if err := InitDBData(); err != nil {
|
||||
log.WithError(err).Error("初始化数据异常")
|
||||
log.Errorf("初始化数据异常: %v", err.Error())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@ -68,13 +69,10 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
//e.Logger = log.GetEchoLogger()
|
||||
e.Use(log.Hook())
|
||||
//e.Use(log.Hook())
|
||||
e.File("/", "web/build/index.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("/logo.svg", "web/build/logo.svg")
|
||||
e.File("/favicon.ico", "web/build/favicon.ico")
|
||||
e.Static("/static", "web/build/static")
|
||||
|
||||
@ -93,7 +91,7 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
|
||||
e.GET("/tunnel", TunEndpoint)
|
||||
e.GET("/ssh", SSHEndpoint)
|
||||
|
||||
e.GET("/ssh-monitor", SshMonitor)
|
||||
e.POST("/logout", LogoutEndpoint)
|
||||
e.POST("/change-password", ChangePasswordEndpoint)
|
||||
e.GET("/reload-totp", ReloadTOTPEndpoint)
|
||||
@ -101,15 +99,21 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
e.POST("/confirm-totp", ConfirmTOTPEndpoint)
|
||||
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.PUT("/:id", Admin(UserUpdateEndpoint))
|
||||
users.DELETE("/:id", Admin(UserDeleteEndpoint))
|
||||
users.GET("/:id", Admin(UserGetEndpoint))
|
||||
users.POST("/:id/change-password", Admin(UserChangePasswordEndpoint))
|
||||
users.POST("/:id/reset-totp", Admin(UserResetTotpEndpoint))
|
||||
users.PUT("/:id", UserUpdateEndpoint)
|
||||
users.DELETE("/:id", UserDeleteEndpoint)
|
||||
users.GET("/:id", UserGetEndpoint)
|
||||
users.POST("/:id/change-password", UserChangePasswordEndpoint)
|
||||
users.POST("/:id/reset-totp", UserResetTotpEndpoint)
|
||||
}
|
||||
|
||||
userGroups := e.Group("/user-groups", Admin)
|
||||
@ -119,36 +123,35 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
userGroups.PUT("/:id", UserGroupUpdateEndpoint)
|
||||
userGroups.DELETE("/:id", UserGroupDeleteEndpoint)
|
||||
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.POST("", AssetCreateEndpoint)
|
||||
assets.POST("/import", Admin(AssetImportEndpoint))
|
||||
assets.POST("/import", AssetImportEndpoint)
|
||||
assets.GET("/paging", AssetPagingEndpoint)
|
||||
assets.POST("/:id/tcping", AssetTcpingEndpoint)
|
||||
assets.PUT("/:id", AssetUpdateEndpoint)
|
||||
assets.DELETE("/:id", AssetDeleteEndpoint)
|
||||
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)
|
||||
|
||||
commands := e.Group("/commands")
|
||||
{
|
||||
commands.GET("", CommandAllEndpoint)
|
||||
commands.GET("/paging", CommandPagingEndpoint)
|
||||
commands.POST("", CommandCreateEndpoint)
|
||||
commands.PUT("/:id", CommandUpdateEndpoint)
|
||||
commands.DELETE("/:id", CommandDeleteEndpoint)
|
||||
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("/paging", CredentialPagingEndpoint)
|
||||
@ -156,45 +159,54 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
credentials.PUT("/:id", CredentialUpdateEndpoint)
|
||||
credentials.DELETE("/:id", CredentialDeleteEndpoint)
|
||||
credentials.GET("/:id", CredentialGetEndpoint)
|
||||
credentials.POST("/:id/change-owner", Admin(CredentialChangeOwnerEndpoint))
|
||||
credentials.POST("/:id/change-owner", CredentialChangeOwnerEndpoint)
|
||||
}
|
||||
|
||||
sessions := e.Group("/sessions")
|
||||
{
|
||||
sessions.POST("", SessionCreateEndpoint)
|
||||
sessions.GET("/paging", Admin(SessionPagingEndpoint))
|
||||
sessions.POST("/:id/connect", SessionConnectEndpoint)
|
||||
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.GET("/:id/ls", SessionLsEndpoint)
|
||||
sessions.GET("/:id/stats", SessionStatsEndpoint)
|
||||
|
||||
sessions.POST("/:id/ls", SessionLsEndpoint)
|
||||
sessions.GET("/:id/download", SessionDownloadEndpoint)
|
||||
sessions.POST("/:id/upload", SessionUploadEndpoint)
|
||||
sessions.POST("/:id/edit", SessionEditEndpoint)
|
||||
sessions.POST("/:id/mkdir", SessionMkDirEndpoint)
|
||||
sessions.POST("/:id/rm", SessionRmEndpoint)
|
||||
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.POST("/overwrite-sharers", RSOverwriteSharersEndPoint)
|
||||
resourceSharers.POST("/remove-resources", Admin(ResourceRemoveByUserIdAssignEndPoint))
|
||||
resourceSharers.POST("/add-resources", Admin(ResourceAddByUserIdAssignEndPoint))
|
||||
resourceSharers.GET("", RSGetSharersEndPoint)
|
||||
resourceSharers.POST("/remove-resources", ResourceRemoveByUserIdAssignEndPoint)
|
||||
resourceSharers.POST("/add-resources", ResourceAddByUserIdAssignEndPoint)
|
||||
}
|
||||
|
||||
loginLogs := e.Group("login-logs", Admin)
|
||||
{
|
||||
loginLogs.GET("/paging", LoginLogPagingEndpoint)
|
||||
loginLogs.DELETE("/:id", LoginLogDeleteEndpoint)
|
||||
//loginLogs.DELETE("/clear", LoginLogClearEndpoint)
|
||||
}
|
||||
|
||||
e.GET("/properties", Admin(PropertyGetEndpoint))
|
||||
e.PUT("/properties", Admin(PropertyUpdateEndpoint))
|
||||
|
||||
e.GET("/overview/counter", OverviewCounterEndPoint)
|
||||
e.GET("/overview/sessions", OverviewSessionPoint)
|
||||
overview := e.Group("overview", Admin)
|
||||
{
|
||||
overview.GET("/counter", OverviewCounterEndPoint)
|
||||
overview.GET("/asset", OverviewAssetEndPoint)
|
||||
overview.GET("/access", OverviewAccessEndPoint)
|
||||
}
|
||||
|
||||
jobs := e.Group("/jobs", Admin)
|
||||
{
|
||||
@ -218,6 +230,44 @@ func SetupRoutes(db *gorm.DB) *echo.Echo {
|
||||
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
|
||||
}
|
||||
|
||||
@ -241,29 +291,32 @@ func InitRepository(db *gorm.DB) {
|
||||
propertyRepository = repository.NewPropertyRepository(db)
|
||||
commandRepository = repository.NewCommandRepository(db)
|
||||
sessionRepository = repository.NewSessionRepository(db)
|
||||
numRepository = repository.NewNumRepository(db)
|
||||
accessSecurityRepository = repository.NewAccessSecurityRepository(db)
|
||||
accessGatewayRepository = repository.NewAccessGatewayRepository(db)
|
||||
jobRepository = repository.NewJobRepository(db)
|
||||
jobLogRepository = repository.NewJobLogRepository(db)
|
||||
loginLogRepository = repository.NewLoginLogRepository(db)
|
||||
storageRepository = repository.NewStorageRepository(db)
|
||||
strategyRepository = repository.NewStrategyRepository(db)
|
||||
}
|
||||
|
||||
func InitService() {
|
||||
jobService = service.NewJobService(jobRepository, jobLogRepository, assetRepository, credentialRepository)
|
||||
propertyService = service.NewPropertyService(propertyRepository)
|
||||
userService = service.NewUserService(userRepository, loginLogRepository)
|
||||
sessionService = service.NewSessionService(sessionRepository)
|
||||
mailService = service.NewMailService(propertyRepository)
|
||||
numService = service.NewNumService(numRepository)
|
||||
assetService = service.NewAssetService(assetRepository)
|
||||
jobService = service.NewJobService(jobRepository, jobLogRepository, assetRepository, credentialRepository, assetService)
|
||||
credentialService = service.NewCredentialService(credentialRepository)
|
||||
storageService = service.NewStorageService(storageRepository, userRepository, propertyRepository)
|
||||
accessGatewayService = service.NewAccessGatewayService(accessGatewayRepository)
|
||||
}
|
||||
|
||||
func InitDBData() (err error) {
|
||||
if err := propertyService.InitProperties(); err != nil {
|
||||
if err := propertyService.DeleteDeprecatedProperty(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := numService.InitNums(); err != nil {
|
||||
if err := propertyService.InitProperties(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := userService.InitUser(); err != nil {
|
||||
@ -287,6 +340,12 @@ func InitDBData() (err error) {
|
||||
if err := assetService.Encrypt(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := storageService.InitStorages(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := accessGatewayService.ReConnectAll(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -368,59 +427,46 @@ func ChangeEncryptionKey(oldEncryptionKey, newEncryptionKey string) error {
|
||||
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 {
|
||||
|
||||
var logMode logger.Interface
|
||||
if global.Config.Debug {
|
||||
if config.GlobalCfg.Debug {
|
||||
logMode = logger.Default.LogMode(logger.Info)
|
||||
} else {
|
||||
logMode = logger.Default.LogMode(logger.Silent)
|
||||
}
|
||||
|
||||
fmt.Printf("当前数据库模式为:%v\n", global.Config.DB)
|
||||
fmt.Printf("当前数据库模式为:%v\n", config.GlobalCfg.DB)
|
||||
var err error
|
||||
var db *gorm.DB
|
||||
if global.Config.DB == "mysql" {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
global.Config.Mysql.Username,
|
||||
global.Config.Mysql.Password,
|
||||
global.Config.Mysql.Hostname,
|
||||
global.Config.Mysql.Port,
|
||||
global.Config.Mysql.Database,
|
||||
if config.GlobalCfg.DB == "mysql" {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=60s",
|
||||
config.GlobalCfg.Mysql.Username,
|
||||
config.GlobalCfg.Mysql.Password,
|
||||
config.GlobalCfg.Mysql.Hostname,
|
||||
config.GlobalCfg.Mysql.Port,
|
||||
config.GlobalCfg.Mysql.Database,
|
||||
)
|
||||
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logMode,
|
||||
})
|
||||
} 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,
|
||||
})
|
||||
}
|
||||
|
||||
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{},
|
||||
&model.Credential{}, &model.Property{}, &model.ResourceSharer{}, &model.UserGroup{}, &model.UserGroupMember{},
|
||||
&model.LoginLog{}, &model.Num{}, &model.Job{}, &model.JobLog{}, &model.AccessSecurity{}); err != nil {
|
||||
log.WithError(err).Panic("初始化数据库表结构异常")
|
||||
&model.LoginLog{}, &model.Job{}, &model.JobLog{}, &model.AccessSecurity{}, &model.AccessGateway{},
|
||||
&model.Storage{}, &model.Strategy{}); err != nil {
|
||||
log.Errorf("初始化数据库表结构异常: %v", err.Error())
|
||||
os.Exit(0)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/global/security"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
@ -24,9 +24,14 @@ func SecurityCreateEndpoint(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
// 更新内存中的安全规则
|
||||
if err := ReloadAccessSecurity(); err != nil {
|
||||
return err
|
||||
rule := &security.Security{
|
||||
ID: item.ID,
|
||||
IP: item.IP,
|
||||
Rule: item.Rule,
|
||||
Priority: item.Priority,
|
||||
}
|
||||
security.GlobalSecurityManager.Add <- rule
|
||||
|
||||
return Success(c, "")
|
||||
}
|
||||
|
||||
@ -36,15 +41,18 @@ func ReloadAccessSecurity() error {
|
||||
return err
|
||||
}
|
||||
if len(rules) > 0 {
|
||||
var securities []*global.Security
|
||||
// 先清空
|
||||
security.GlobalSecurityManager.Clear()
|
||||
// 再添加到全局的安全管理器中
|
||||
for i := 0; i < len(rules); i++ {
|
||||
rule := global.Security{
|
||||
IP: rules[i].IP,
|
||||
Rule: rules[i].Rule,
|
||||
rule := &security.Security{
|
||||
ID: rules[i].ID,
|
||||
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
|
||||
}
|
||||
@ -81,9 +89,15 @@ func SecurityUpdateEndpoint(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
// 更新内存中的安全规则
|
||||
if err := ReloadAccessSecurity(); err != nil {
|
||||
return err
|
||||
security.GlobalSecurityManager.Del <- id
|
||||
rule := &security.Security{
|
||||
ID: item.ID,
|
||||
IP: item.IP,
|
||||
Rule: item.Rule,
|
||||
Priority: item.Priority,
|
||||
}
|
||||
security.GlobalSecurityManager.Add <- rule
|
||||
|
||||
return Success(c, nil)
|
||||
}
|
||||
|
||||
@ -92,15 +106,14 @@ func SecurityDeleteEndpoint(c echo.Context) error {
|
||||
|
||||
split := strings.Split(ids, ",")
|
||||
for i := range split {
|
||||
jobId := split[i]
|
||||
if err := accessSecurityRepository.DeleteById(jobId); err != nil {
|
||||
id := split[i]
|
||||
if err := accessSecurityRepository.DeleteById(id); err != nil {
|
||||
return err
|
||||
}
|
||||
// 更新内存中的安全规则
|
||||
security.GlobalSecurityManager.Del <- id
|
||||
}
|
||||
// 更新内存中的安全规则
|
||||
if err := ReloadAccessSecurity(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Success(c, nil)
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
@ -13,14 +13,18 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/global/session"
|
||||
"next-terminal/server/guacd"
|
||||
"next-terminal/server/log"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/service"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pkg/sftp"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
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 {
|
||||
|
||||
var recording string
|
||||
if items[i].Mode == constant.Naive {
|
||||
if items[i].Mode == constant.Naive || items[i].Mode == constant.Terminal {
|
||||
recording = items[i].Recording
|
||||
} else {
|
||||
recording = items[i].Recording + "/recording"
|
||||
@ -78,14 +82,28 @@ func SessionDeleteEndpoint(c echo.Context) error {
|
||||
func SessionConnectEndpoint(c echo.Context) error {
|
||||
sessionId := c.Param("id")
|
||||
|
||||
session := model.Session{}
|
||||
session.ID = sessionId
|
||||
session.Status = constant.Connected
|
||||
session.ConnectedTime = utils.NowJsonTime()
|
||||
s := model.Session{}
|
||||
s.ID = sessionId
|
||||
s.Status = constant.Connected
|
||||
s.ConnectedTime = utils.NowJsonTime()
|
||||
|
||||
if err := sessionRepository.UpdateById(&session, sessionId); err != nil {
|
||||
if err := sessionRepository.UpdateById(&s, sessionId); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -104,18 +122,48 @@ var mutex sync.Mutex
|
||||
func CloseSessionById(sessionId string, code int, reason string) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
observable, _ := global.Store.Get(sessionId)
|
||||
if observable != nil {
|
||||
log.Debugf("会话%v创建者退出,原因:%v", sessionId, reason)
|
||||
observable.Subject.Close(code, reason)
|
||||
nextSession := session.GlobalSessionManager.GetById(sessionId)
|
||||
if nextSession != nil {
|
||||
log.Debugf("[%v] 会话关闭,原因:%v", sessionId, reason)
|
||||
WriteCloseMessage(nextSession.WebSocket, nextSession.Mode, code, reason)
|
||||
|
||||
for i := 0; i < len(observable.Observers); i++ {
|
||||
observable.Observers[i].Close(code, reason)
|
||||
log.Debugf("强制踢出会话%v的观察者", sessionId)
|
||||
if nextSession.Observer != nil {
|
||||
obs := nextSession.Observer.All()
|
||||
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)
|
||||
if err != nil {
|
||||
return
|
||||
@ -131,17 +179,17 @@ func CloseSessionById(sessionId string, code int, reason string) {
|
||||
return
|
||||
}
|
||||
|
||||
session := model.Session{}
|
||||
session.ID = sessionId
|
||||
session.Status = constant.Disconnected
|
||||
session.DisconnectedTime = utils.NowJsonTime()
|
||||
session.Code = code
|
||||
session.Message = reason
|
||||
session.Password = "-"
|
||||
session.PrivateKey = "-"
|
||||
session.Passphrase = "-"
|
||||
ss := model.Session{}
|
||||
ss.ID = sessionId
|
||||
ss.Status = constant.Disconnected
|
||||
ss.DisconnectedTime = utils.NowJsonTime()
|
||||
ss.Code = code
|
||||
ss.Message = reason
|
||||
ss.Password = "-"
|
||||
ss.PrivateKey = "-"
|
||||
ss.Passphrase = "-"
|
||||
|
||||
_ = sessionRepository.UpdateById(&session, sessionId)
|
||||
_ = sessionRepository.UpdateById(&ss, sessionId)
|
||||
}
|
||||
|
||||
func SessionResizeEndpoint(c echo.Context) error {
|
||||
@ -150,11 +198,10 @@ func SessionResizeEndpoint(c echo.Context) error {
|
||||
sessionId := c.Param("id")
|
||||
|
||||
if len(width) == 0 || len(height) == 0 {
|
||||
panic("参数异常")
|
||||
return errors.New("参数异常")
|
||||
}
|
||||
|
||||
intWidth, _ := strconv.Atoi(width)
|
||||
|
||||
intHeight, _ := strconv.Atoi(height)
|
||||
|
||||
if err := sessionRepository.UpdateWindowSizeById(intWidth, intHeight, sessionId); err != nil {
|
||||
@ -175,37 +222,83 @@ func SessionCreateEndpoint(c echo.Context) error {
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session := &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,
|
||||
var (
|
||||
upload = "1"
|
||||
download = "1"
|
||||
_delete = "1"
|
||||
rename = "1"
|
||||
edit = "1"
|
||||
fileSystem = "1"
|
||||
)
|
||||
if asset.Owner != user.ID && constant.TypeUser == user.Type {
|
||||
// 普通用户访问非自己创建的资产需要校验权限
|
||||
resourceSharers, err := resourceSharerRepository.FindByResourceIdAndUserId(assetId, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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" {
|
||||
@ -215,28 +308,41 @@ func SessionCreateEndpoint(c echo.Context) error {
|
||||
}
|
||||
|
||||
if credential.Type == constant.Custom {
|
||||
session.Username = credential.Username
|
||||
session.Password = credential.Password
|
||||
s.Username = credential.Username
|
||||
s.Password = credential.Password
|
||||
} else {
|
||||
session.Username = credential.Username
|
||||
session.PrivateKey = credential.PrivateKey
|
||||
session.Passphrase = credential.Passphrase
|
||||
s.Username = credential.Username
|
||||
s.PrivateKey = credential.PrivateKey
|
||||
s.Passphrase = credential.Passphrase
|
||||
}
|
||||
}
|
||||
|
||||
if err := sessionRepository.Create(session); err != nil {
|
||||
if err := sessionRepository.Create(s); err != nil {
|
||||
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 {
|
||||
sessionId := c.Param("id")
|
||||
session, err := sessionRepository.FindById(sessionId)
|
||||
s, err := sessionRepository.FindById(sessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Upload != "1" {
|
||||
return errors.New("禁止操作")
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return err
|
||||
@ -251,77 +357,94 @@ func SessionUploadEndpoint(c echo.Context) error {
|
||||
remoteDir := c.QueryParam("dir")
|
||||
remoteFile := path.Join(remoteDir, filename)
|
||||
|
||||
if "ssh" == session.Protocol {
|
||||
tun, ok := global.Store.Get(sessionId)
|
||||
if !ok {
|
||||
return errors.New("获取sftp客户端失败")
|
||||
if "ssh" == s.Protocol {
|
||||
nextSession := session.GlobalSessionManager.GetById(sessionId)
|
||||
if nextSession == nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
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 {
|
||||
if _, err = io.Copy(dstFile, src); err != nil {
|
||||
return err
|
||||
}
|
||||
return Success(c, nil)
|
||||
} else if "rdp" == s.Protocol {
|
||||
return StorageUpload(c, file, s.StorageId)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func SessionDownloadEndpoint(c echo.Context) error {
|
||||
func SessionEditEndpoint(c echo.Context) error {
|
||||
sessionId := c.Param("id")
|
||||
session, err := sessionRepository.FindById(sessionId)
|
||||
s, err := sessionRepository.FindById(sessionId)
|
||||
if err != nil {
|
||||
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")
|
||||
// 获取带后缀的文件名称
|
||||
filenameWithSuffix := path.Base(remoteFile)
|
||||
if "ssh" == session.Protocol {
|
||||
tun, ok := global.Store.Get(sessionId)
|
||||
if !ok {
|
||||
return errors.New("获取sftp客户端失败")
|
||||
if "ssh" == s.Protocol {
|
||||
nextSession := session.GlobalSessionManager.GetById(sessionId)
|
||||
if nextSession == nil {
|
||||
return errors.New("获取会话失败")
|
||||
}
|
||||
|
||||
dstFile, err := tun.Subject.NextTerminal.SftpClient.Open(remoteFile)
|
||||
dstFile, err := nextSession.NextTerminal.SftpClient.Open(remoteFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -335,105 +458,51 @@ func SessionDownloadEndpoint(c echo.Context) error {
|
||||
}
|
||||
|
||||
return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(buff.Bytes()))
|
||||
} 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
|
||||
}
|
||||
return c.Attachment(path.Join(drivePath, remoteFile), filenameWithSuffix)
|
||||
} else if "rdp" == s.Protocol {
|
||||
storageId := s.StorageId
|
||||
return StorageDownload(c, remoteFile, storageId)
|
||||
}
|
||||
|
||||
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 {
|
||||
sessionId := c.Param("id")
|
||||
session, err := sessionRepository.FindByIdAndDecrypt(sessionId)
|
||||
s, err := sessionRepository.FindByIdAndDecrypt(sessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remoteDir := c.QueryParam("dir")
|
||||
if "ssh" == session.Protocol {
|
||||
tun, ok := global.Store.Get(sessionId)
|
||||
if !ok {
|
||||
return errors.New("获取sftp客户端失败")
|
||||
|
||||
remoteDir := c.FormValue("dir")
|
||||
if "ssh" == s.Protocol {
|
||||
nextSession := session.GlobalSessionManager.GetById(sessionId)
|
||||
if nextSession == nil {
|
||||
return errors.New("获取会话失败")
|
||||
}
|
||||
|
||||
if tun.Subject.NextTerminal == nil {
|
||||
nextTerminal, err := CreateNextTerminalBySession(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tun.Subject.NextTerminal = nextTerminal
|
||||
}
|
||||
|
||||
if tun.Subject.NextTerminal.SftpClient == nil {
|
||||
sftpClient, err := sftp.NewClient(tun.Subject.NextTerminal.SshClient)
|
||||
if nextSession.NextTerminal.SftpClient == nil {
|
||||
sftpClient, err := sftp.NewClient(nextSession.NextTerminal.SshClient)
|
||||
if err != nil {
|
||||
log.Errorf("创建sftp客户端失败:%v", err.Error())
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
var files = make([]File, 0)
|
||||
var files = make([]service.File, 0)
|
||||
for i := range fileInfos {
|
||||
|
||||
// 忽略因此文件
|
||||
// 忽略隐藏文件
|
||||
if strings.HasPrefix(fileInfos[i].Name(), ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
file := 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{
|
||||
file := service.File{
|
||||
Name: fileInfos[i].Name(),
|
||||
Path: path.Join(remoteDir, fileInfos[i].Name()),
|
||||
IsDir: fileInfos[i].IsDir(),
|
||||
@ -447,115 +516,87 @@ func SessionLsEndpoint(c echo.Context) error {
|
||||
}
|
||||
|
||||
return Success(c, files)
|
||||
} else if "rdp" == s.Protocol {
|
||||
storageId := s.StorageId
|
||||
return StorageLs(c, remoteDir, storageId)
|
||||
}
|
||||
|
||||
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 {
|
||||
sessionId := c.Param("id")
|
||||
session, err := sessionRepository.FindById(sessionId)
|
||||
s, err := sessionRepository.FindById(sessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remoteDir := c.QueryParam("dir")
|
||||
if "ssh" == session.Protocol {
|
||||
tun, ok := global.Store.Get(sessionId)
|
||||
if !ok {
|
||||
return errors.New("获取sftp客户端失败")
|
||||
}
|
||||
if err := tun.Subject.NextTerminal.SftpClient.Mkdir(remoteDir); err != nil {
|
||||
return err
|
||||
}
|
||||
return Success(c, nil)
|
||||
} 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
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(path.Join(drivePath, remoteDir), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return Success(c, nil)
|
||||
if s.Upload != "1" {
|
||||
return errors.New("禁止操作")
|
||||
}
|
||||
remoteDir := c.QueryParam("dir")
|
||||
if "ssh" == s.Protocol {
|
||||
nextSession := session.GlobalSessionManager.GetById(sessionId)
|
||||
if nextSession == nil {
|
||||
return errors.New("获取会话失败")
|
||||
}
|
||||
if err := nextSession.NextTerminal.SftpClient.Mkdir(remoteDir); err != nil {
|
||||
return err
|
||||
}
|
||||
return Success(c, nil)
|
||||
} else if "rdp" == s.Protocol {
|
||||
return StorageMkDir(c, remoteDir, s.StorageId)
|
||||
}
|
||||
|
||||
return errors.New("当前协议不支持此操作")
|
||||
}
|
||||
|
||||
func SessionRmEndpoint(c echo.Context) error {
|
||||
sessionId := c.Param("id")
|
||||
session, err := sessionRepository.FindById(sessionId)
|
||||
s, err := sessionRepository.FindById(sessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := c.QueryParam("key")
|
||||
if "ssh" == session.Protocol {
|
||||
tun, ok := global.Store.Get(sessionId)
|
||||
if !ok {
|
||||
return errors.New("获取sftp客户端失败")
|
||||
if s.Delete != "1" {
|
||||
return errors.New("禁止操作")
|
||||
}
|
||||
// 文件夹或者文件
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
fileInfos, err := sftpClient.ReadDir(key)
|
||||
fileInfos, err := sftpClient.ReadDir(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if err := sftpClient.RemoveDirectory(key); err != nil {
|
||||
if err := sftpClient.RemoveDirectory(file); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := sftpClient.Remove(key); err != nil {
|
||||
if err := sftpClient.Remove(file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return Success(c, nil)
|
||||
} else if "rdp" == session.Protocol {
|
||||
if strings.Contains(key, "../") {
|
||||
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)
|
||||
} else if "rdp" == s.Protocol {
|
||||
return StorageRm(c, file, s.StorageId)
|
||||
}
|
||||
|
||||
return errors.New("当前协议不支持此操作")
|
||||
@ -563,58 +604,80 @@ func SessionRmEndpoint(c echo.Context) error {
|
||||
|
||||
func SessionRenameEndpoint(c echo.Context) error {
|
||||
sessionId := c.Param("id")
|
||||
session, err := sessionRepository.FindById(sessionId)
|
||||
s, err := sessionRepository.FindById(sessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Rename != "1" {
|
||||
return errors.New("禁止操作")
|
||||
}
|
||||
oldName := c.QueryParam("oldName")
|
||||
newName := c.QueryParam("newName")
|
||||
if "ssh" == session.Protocol {
|
||||
tun, ok := global.Store.Get(sessionId)
|
||||
if !ok {
|
||||
return errors.New("获取sftp客户端失败")
|
||||
if "ssh" == s.Protocol {
|
||||
nextSession := session.GlobalSessionManager.GetById(sessionId)
|
||||
if nextSession == nil {
|
||||
return errors.New("获取会话失败")
|
||||
}
|
||||
|
||||
sftpClient := tun.Subject.NextTerminal.SftpClient
|
||||
sftpClient := nextSession.NextTerminal.SftpClient
|
||||
|
||||
if err := sftpClient.Rename(oldName, newName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Success(c, nil)
|
||||
} else if "rdp" == session.Protocol {
|
||||
if strings.Contains(oldName, "../") {
|
||||
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)
|
||||
} else if "rdp" == s.Protocol {
|
||||
return StorageRename(c, oldName, newName, s.StorageId)
|
||||
}
|
||||
return errors.New("当前协议不支持此操作")
|
||||
}
|
||||
|
||||
func SessionRecordingEndpoint(c echo.Context) error {
|
||||
sessionId := c.Param("id")
|
||||
session, err := sessionRepository.FindById(sessionId)
|
||||
s, err := sessionRepository.FindById(sessionId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var recording string
|
||||
if session.Mode == constant.Naive {
|
||||
recording = session.Recording
|
||||
if s.Mode == constant.Naive || s.Mode == constant.Terminal {
|
||||
recording = s.Recording
|
||||
} else {
|
||||
recording = session.Recording + "/recording"
|
||||
recording = s.Recording + "/recording"
|
||||
}
|
||||
|
||||
log.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(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)
|
||||
}
|
||||
|
@ -1,18 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/guacd"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/pkg/term"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/global/session"
|
||||
"next-terminal/server/guacd"
|
||||
"next-terminal/server/log"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/term"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
@ -27,17 +32,44 @@ var UpGrader = websocket.Upgrader{
|
||||
}
|
||||
|
||||
const (
|
||||
Connected = "connected"
|
||||
Data = "data"
|
||||
Resize = "resize"
|
||||
Closed = "closed"
|
||||
Closed = 0
|
||||
Connected = 1
|
||||
Data = 2
|
||||
Resize = 3
|
||||
Ping = 4
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Type int `json:"type"`
|
||||
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 {
|
||||
Cols int `json:"cols"`
|
||||
Rows int `json:"rows"`
|
||||
@ -50,92 +82,73 @@ func SSHEndpoint(c echo.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
defer ws.Close()
|
||||
|
||||
sessionId := c.QueryParam("sessionId")
|
||||
cols, _ := strconv.Atoi(c.QueryParam("cols"))
|
||||
rows, _ := strconv.Atoi(c.QueryParam("rows"))
|
||||
|
||||
session, err := sessionRepository.FindByIdAndDecrypt(sessionId)
|
||||
s, err := sessionRepository.FindByIdAndDecrypt(sessionId)
|
||||
if err != nil {
|
||||
msg := Message{
|
||||
Type: Closed,
|
||||
Content: "get sshSession error." + err.Error(),
|
||||
}
|
||||
_ = WriteMessage(ws, msg)
|
||||
return err
|
||||
return WriteMessage(ws, NewMessage(Closed, "获取会话失败"))
|
||||
}
|
||||
|
||||
user, _ := GetCurrentAccount(c)
|
||||
if constant.TypeUser == user.Type {
|
||||
// 检测是否有访问权限
|
||||
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)
|
||||
}
|
||||
if err := permissionCheck(c, s.AssetId); err != nil {
|
||||
return WriteMessage(ws, NewMessage(Closed, err.Error()))
|
||||
}
|
||||
|
||||
var (
|
||||
username = session.Username
|
||||
password = session.Password
|
||||
privateKey = session.PrivateKey
|
||||
passphrase = session.Passphrase
|
||||
ip = session.IP
|
||||
port = session.Port
|
||||
username = s.Username
|
||||
password = s.Password
|
||||
privateKey = s.PrivateKey
|
||||
passphrase = s.Passphrase
|
||||
ip = s.IP
|
||||
port = s.Port
|
||||
)
|
||||
|
||||
recording := ""
|
||||
propertyMap := propertyRepository.FindAllMap()
|
||||
if propertyMap[guacd.EnableRecording] == "true" {
|
||||
recording = path.Join(propertyMap[guacd.RecordingPath], sessionId, "recording.cast")
|
||||
}
|
||||
|
||||
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 s.AccessGatewayId != "" && s.AccessGatewayId != "-" {
|
||||
g, err := accessGatewayService.GetGatewayAndReconnectById(s.AccessGatewayId)
|
||||
if err != nil {
|
||||
return WriteMessage(ws, NewMessage(Closed, "获取接入网关失败:"+err.Error()))
|
||||
}
|
||||
|
||||
return err
|
||||
if !g.Connected {
|
||||
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 {
|
||||
log.Errorf("创建SSH客户端失败:%v", err.Error())
|
||||
msg := Message{
|
||||
Type: Closed,
|
||||
Content: err.Error(),
|
||||
}
|
||||
err := WriteMessage(ws, msg)
|
||||
return WriteMessage(ws, NewMessage(Closed, "创建SSH客户端失败:"+err.Error()))
|
||||
}
|
||||
|
||||
if err := nextTerminal.RequestPty(xterm, rows, cols); err != nil {
|
||||
return err
|
||||
}
|
||||
tun.NextTerminal = nextTerminal
|
||||
|
||||
var observers []global.Tun
|
||||
observable := global.Observable{
|
||||
Subject: &tun,
|
||||
Observers: observers,
|
||||
if err := nextTerminal.Shell(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
global.Store.Set(sessionId, &observable)
|
||||
|
||||
sess := model.Session{
|
||||
ConnectionId: sessionId,
|
||||
Width: cols,
|
||||
@ -149,106 +162,209 @@ func SSHEndpoint(c echo.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := Message{
|
||||
Type: Connected,
|
||||
Content: "",
|
||||
if err := WriteMessage(ws, NewMessage(Connected, "")); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = 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 {
|
||||
_, message, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
// web socket会话关闭后主动关闭ssh会话
|
||||
CloseSessionById(sessionId, Normal, "正常退出")
|
||||
quitChan <- true
|
||||
quitChan <- true
|
||||
log.Debugf("WebSocket已关闭")
|
||||
CloseSessionById(sessionId, Normal, "用户正常退出")
|
||||
cancel()
|
||||
break
|
||||
}
|
||||
|
||||
var msg Message
|
||||
err = json.Unmarshal(message, &msg)
|
||||
msg, err := ParseMessage(string(message))
|
||||
if err != nil {
|
||||
log.Warnf("解析Json失败: %v, 原始字符串:%v", err, string(message))
|
||||
log.Warnf("消息解码失败: %v, 原始字符串:%v", err, string(message))
|
||||
continue
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case Resize:
|
||||
var winSize WindowSize
|
||||
err = json.Unmarshal([]byte(msg.Content), &winSize)
|
||||
decodeString, err := base64.StdEncoding.DecodeString(msg.Content)
|
||||
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
|
||||
}
|
||||
if err := nextTerminal.WindowChange(winSize.Rows, winSize.Cols); err != nil {
|
||||
log.Warnf("更改SSH会话窗口大小失败: %v", err)
|
||||
continue
|
||||
}
|
||||
_ = sessionRepository.UpdateWindowSizeById(winSize.Rows, winSize.Cols, sessionId)
|
||||
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 {
|
||||
log.Debugf("SSH会话写入失败: %v", err)
|
||||
msg := Message{
|
||||
Type: Closed,
|
||||
Content: "the remote connection is closed.",
|
||||
}
|
||||
_ = WriteMessage(ws, msg)
|
||||
CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
|
||||
}
|
||||
case Ping:
|
||||
_, _, err := nextTerminal.SshClient.Conn.SendRequest("helloworld1024@foxmail.com", true, nil)
|
||||
if err != nil {
|
||||
CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
|
||||
} else {
|
||||
_ = WriteMessage(ws, NewMessage(Ping, ""))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
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
|
||||
for {
|
||||
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)
|
||||
if !utils.Contains(assetIds, assetId) {
|
||||
return errors.New("您没有权限访问此资产")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteMessage(ws *websocket.Conn, msg Message) error {
|
||||
message, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
message := []byte(msg.ToString())
|
||||
return ws.WriteMessage(websocket.TextMessage, message)
|
||||
}
|
||||
|
||||
func CreateNextTerminalBySession(session model.Session) (*term.NextTerminal, error) {
|
||||
@ -260,5 +376,46 @@ func CreateNextTerminalBySession(session model.Session) (*term.NextTerminal, err
|
||||
ip = session.IP
|
||||
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
|
||||
}
|
||||
|
6
server/api/ssh_command_deal.go
Normal file
@ -0,0 +1,6 @@
|
||||
package api
|
||||
|
||||
func DealCommand(enterKeys []rune) {
|
||||
println(string(enterKeys))
|
||||
|
||||
}
|
468
server/api/sshd.go
Normal 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
@ -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
@ -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
@ -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
@ -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: "Dragon’s 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)
|
||||
}
|
@ -1,26 +1,34 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/guacd"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/global/session"
|
||||
"next-terminal/server/guacd"
|
||||
"next-terminal/server/log"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
TunnelClosed int = -1
|
||||
Normal int = 0
|
||||
NotFoundSession int = 800
|
||||
NewTunnelError int = 801
|
||||
ForcedDisconnect int = 802
|
||||
TunnelClosed int = -1
|
||||
Normal int = 0
|
||||
NotFoundSession int = 800
|
||||
NewTunnelError int = 801
|
||||
ForcedDisconnect int = 802
|
||||
AccessGatewayUnAvailable int = 803
|
||||
AccessGatewayCreateError int = 804
|
||||
AccessGatewayConnectError int = 804
|
||||
)
|
||||
|
||||
func TunEndpoint(c echo.Context) error {
|
||||
@ -44,111 +52,63 @@ func TunEndpoint(c echo.Context) error {
|
||||
|
||||
propertyMap := propertyRepository.FindAllMap()
|
||||
|
||||
var session model.Session
|
||||
var s model.Session
|
||||
|
||||
if len(connectionId) > 0 {
|
||||
session, err = sessionRepository.FindByConnectionId(connectionId)
|
||||
s, err = sessionRepository.FindByConnectionId(connectionId)
|
||||
if err != nil {
|
||||
log.Warnf("会话不存在")
|
||||
return err
|
||||
}
|
||||
if session.Status != constant.Connected {
|
||||
log.Warnf("会话未在线")
|
||||
if s.Status != constant.Connected {
|
||||
return errors.New("会话未在线")
|
||||
}
|
||||
configuration.ConnectionID = connectionId
|
||||
sessionId = session.ID
|
||||
configuration.SetParameter("width", strconv.Itoa(session.Width))
|
||||
configuration.SetParameter("height", strconv.Itoa(session.Height))
|
||||
sessionId = s.ID
|
||||
configuration.SetParameter("width", strconv.Itoa(s.Width))
|
||||
configuration.SetParameter("height", strconv.Itoa(s.Height))
|
||||
configuration.SetParameter("dpi", "96")
|
||||
} else {
|
||||
configuration.SetParameter("width", width)
|
||||
configuration.SetParameter("height", height)
|
||||
configuration.SetParameter("dpi", dpi)
|
||||
session, err = sessionRepository.FindByIdAndDecrypt(sessionId)
|
||||
s, err = sessionRepository.FindByIdAndDecrypt(sessionId)
|
||||
if err != nil {
|
||||
CloseSessionById(sessionId, NotFoundSession, "会话不存在")
|
||||
return err
|
||||
}
|
||||
|
||||
if propertyMap[guacd.EnableRecording] == "true" {
|
||||
configuration.SetParameter(guacd.RecordingPath, path.Join(propertyMap[guacd.RecordingPath], sessionId))
|
||||
configuration.SetParameter(guacd.CreateRecordingPath, propertyMap[guacd.CreateRecordingPath])
|
||||
} else {
|
||||
configuration.SetParameter(guacd.RecordingPath, "")
|
||||
}
|
||||
|
||||
configuration.Protocol = session.Protocol
|
||||
switch configuration.Protocol {
|
||||
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)
|
||||
setConfig(propertyMap, s, configuration)
|
||||
var (
|
||||
ip = s.IP
|
||||
port = s.Port
|
||||
)
|
||||
if s.AccessGatewayId != "" && s.AccessGatewayId != "-" {
|
||||
g, err := accessGatewayService.GetGatewayAndReconnectById(s.AccessGatewayId)
|
||||
if err != nil {
|
||||
disconnect(ws, AccessGatewayUnAvailable, "获取接入网关失败:"+err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
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", session.Username)
|
||||
configuration.SetParameter("password", session.Password)
|
||||
case "telnet":
|
||||
configuration.SetParameter("username", session.Username)
|
||||
configuration.SetParameter("password", session.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:
|
||||
log.WithField("configuration.Protocol", configuration.Protocol).Error("UnSupport Protocol")
|
||||
return Fail(c, 400, "不支持的协议")
|
||||
if !g.Connected {
|
||||
disconnect(ws, AccessGatewayUnAvailable, "接入网关不可用:"+g.Message)
|
||||
return nil
|
||||
}
|
||||
exposedIP, exposedPort, err := g.OpenSshTunnel(s.ID, ip, port)
|
||||
if err != nil {
|
||||
disconnect(ws, AccessGatewayCreateError, "创建SSH隧道失败:"+err.Error())
|
||||
return nil
|
||||
}
|
||||
defer g.CloseSshTunnel(s.ID)
|
||||
ip = exposedIP
|
||||
port = exposedPort
|
||||
}
|
||||
|
||||
configuration.SetParameter("hostname", session.IP)
|
||||
configuration.SetParameter("port", strconv.Itoa(session.Port))
|
||||
configuration.SetParameter("hostname", ip)
|
||||
configuration.SetParameter("port", strconv.Itoa(port))
|
||||
|
||||
// 加载资产配置的属性,优先级比全局配置的高,因此最后加载,覆盖掉全局配置
|
||||
attributes, _ := assetRepository.FindAttrById(session.AssetId)
|
||||
attributes, err := assetRepository.FindAssetAttrMapByAssetId(s.AssetId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(attributes) > 0 {
|
||||
for i := range attributes {
|
||||
attribute := attributes[i]
|
||||
configuration.SetParameter(attribute.Name, attribute.Value)
|
||||
}
|
||||
setAssetConfig(attributes, s, configuration)
|
||||
}
|
||||
}
|
||||
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 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
|
||||
}
|
||||
|
||||
tun := global.Tun{
|
||||
Protocol: session.Protocol,
|
||||
Mode: session.Mode,
|
||||
WebSocket: ws,
|
||||
Tunnel: tunnel,
|
||||
nextSession := &session.Session{
|
||||
ID: sessionId,
|
||||
Protocol: s.Protocol,
|
||||
Mode: s.Mode,
|
||||
WebSocket: ws,
|
||||
GuacdTunnel: guacdTunnel,
|
||||
}
|
||||
|
||||
if len(session.ConnectionId) == 0 {
|
||||
|
||||
var observers []global.Tun
|
||||
observable := global.Observable{
|
||||
Subject: &tun,
|
||||
Observers: observers,
|
||||
if len(s.ConnectionId) == 0 {
|
||||
if configuration.Protocol == constant.SSH {
|
||||
nextTerminal, err := CreateNextTerminalBySession(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextSession.NextTerminal = nextTerminal
|
||||
}
|
||||
|
||||
global.Store.Set(sessionId, &observable)
|
||||
|
||||
nextSession.Observer = session.NewObserver(sessionId)
|
||||
session.GlobalSessionManager.Add <- nextSession
|
||||
go nextSession.Observer.Run()
|
||||
sess := model.Session{
|
||||
ConnectionId: tunnel.UUID,
|
||||
ConnectionId: guacdTunnel.UUID,
|
||||
Width: intWidth,
|
||||
Height: intHeight,
|
||||
Status: constant.Connecting,
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 监控会话
|
||||
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))
|
||||
// 要监控会话
|
||||
forObsSession := session.GlobalSessionManager.GetById(sessionId)
|
||||
if forObsSession == nil {
|
||||
disconnect(ws, NotFoundSession, "获取会话失败")
|
||||
return nil
|
||||
}
|
||||
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() {
|
||||
GuacdLoop:
|
||||
for {
|
||||
instruction, err := tunnel.Read()
|
||||
if err != nil {
|
||||
if connectionId == "" {
|
||||
CloseSessionById(sessionId, TunnelClosed, "远程连接关闭")
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debugf("[%v:%v] WebSocket 已关闭,即将关闭 Guacd 连接...", sessionId, connectionId)
|
||||
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
|
||||
}
|
||||
err = ws.WriteMessage(websocket.TextMessage, instruction)
|
||||
if err != nil {
|
||||
if connectionId == "" {
|
||||
CloseSessionById(sessionId, Normal, "正常退出")
|
||||
if len(instruction) == 0 {
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
_, message, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
if connectionId == "" {
|
||||
CloseSessionById(sessionId, Normal, "正常退出")
|
||||
log.Debugf("[%v:%v] WebSocket已关闭", sessionId, connectionId)
|
||||
// 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
|
||||
}
|
||||
_, err = tunnel.WriteAndFlush(message)
|
||||
_, err = guacdTunnel.WriteAndFlush(message)
|
||||
if err != nil {
|
||||
if connectionId == "" {
|
||||
CloseSessionById(sessionId, TunnelClosed, "远程连接关闭")
|
||||
}
|
||||
break
|
||||
CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
|
||||
}
|
||||
}
|
||||
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()))
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/pkg/log"
|
||||
"next-terminal/server/global/cache"
|
||||
"next-terminal/server/log"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
"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
|
||||
if err := c.Bind(&item); err != nil {
|
||||
return err
|
||||
@ -20,7 +22,6 @@ func UserCreateEndpoint(c echo.Context) error {
|
||||
password := item.Password
|
||||
|
||||
var pass []byte
|
||||
var err error
|
||||
if pass, err = utils.Encoder.Encode([]byte(password)); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -32,6 +33,10 @@ func UserCreateEndpoint(c echo.Context) error {
|
||||
if err := userRepository.Create(&item); err != nil {
|
||||
return err
|
||||
}
|
||||
err = storageService.CreateStorageByUser(&item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.Mail != "" {
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
for j := range loginLogs {
|
||||
global.Cache.Delete(loginLogs[j].ID)
|
||||
if err := userService.Logout(loginLogs[j].ID); err != nil {
|
||||
log.WithError(err).WithField("id:", loginLogs[j].ID).Error("Cache Deleted Error")
|
||||
token := loginLogs[j].ID
|
||||
cacheKey := userService.BuildCacheKeyByToken(token)
|
||||
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, "强制下线错误")
|
||||
}
|
||||
}
|
||||
@ -107,6 +118,10 @@ func UserDeleteEndpoint(c echo.Context) error {
|
||||
if err := userRepository.DeleteById(userId); err != nil {
|
||||
return err
|
||||
}
|
||||
// 删除用户的默认磁盘空间
|
||||
if err := storageService.DeleteStorageById(userId, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return Success(c, nil)
|
||||
@ -125,7 +140,10 @@ func UserGetEndpoint(c echo.Context) error {
|
||||
|
||||
func UserChangePasswordEndpoint(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
password := c.QueryParam("password")
|
||||
password := c.FormValue("password")
|
||||
if password == "" {
|
||||
return Fail(c, -1, "请输入密码")
|
||||
}
|
||||
|
||||
user, err := userRepository.FindById(id)
|
||||
if err != nil {
|
||||
@ -172,9 +190,11 @@ func ReloadToken() error {
|
||||
for i := range loginLogs {
|
||||
loginLog := loginLogs[i]
|
||||
token := loginLog.ID
|
||||
user, err := userRepository.FindById(loginLog.UserId)
|
||||
user, err := userRepository.FindByUsername(loginLog.Username)
|
||||
if err != nil {
|
||||
log.Debugf("用户「%v」获取失败,忽略", loginLog.UserId)
|
||||
if errors.Is(gorm.ErrRecordNotFound, err) {
|
||||
_ = loginLogRepository.DeleteById(token)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@ -184,13 +204,13 @@ func ReloadToken() error {
|
||||
User: user,
|
||||
}
|
||||
|
||||
cacheKey := BuildCacheKeyByToken(token)
|
||||
cacheKey := userService.BuildCacheKeyByToken(token)
|
||||
|
||||
if authorization.Remember {
|
||||
// 记住登录有效期两周
|
||||
global.Cache.Set(cacheKey, authorization, RememberEffectiveTime)
|
||||
cache.GlobalCache.Set(cacheKey, authorization, RememberEffectiveTime)
|
||||
} else {
|
||||
global.Cache.Set(cacheKey, authorization, NotRememberEffectiveTime)
|
||||
cache.GlobalCache.Set(cacheKey, authorization, NotRememberEffectiveTime)
|
||||
}
|
||||
log.Debugf("重新加载用户「%v」授权Token「%v」到缓存", user.Nickname, token)
|
||||
}
|
||||
|
227
server/config/config.go
Normal 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)
|
||||
}
|
||||
}
|
@ -1,8 +1,31 @@
|
||||
package constant
|
||||
|
||||
import "next-terminal/pkg/guacd"
|
||||
import (
|
||||
"next-terminal/server/guacd"
|
||||
)
|
||||
|
||||
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" // 允许访问
|
||||
AccessRuleReject = "reject" // 拒绝访问
|
||||
|
||||
@ -27,15 +50,16 @@ const (
|
||||
Connected = "connected" // 会话状态:已连接
|
||||
Disconnected = "disconnected" // 会话状态:已断开连接
|
||||
|
||||
Guacd = "guacd" // 接入模式:guacd
|
||||
Naive = "naive" // 接入模式:原生
|
||||
Guacd = "guacd" // 接入模式:guacd
|
||||
Naive = "naive" // 接入模式:原生
|
||||
Terminal = "terminal" // 接入模式:终端
|
||||
|
||||
TypeUser = "user" // 普通用户
|
||||
TypeAdmin = "admin" // 管理员
|
||||
)
|
||||
|
||||
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 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}
|
13
server/global/cache/cache.go
vendored
Normal 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)
|
||||
}
|
16
server/global/cron/cron.go
Normal 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)
|
||||
}
|
129
server/global/gateway/gateway.go
Normal 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()
|
||||
}
|
||||
}
|
42
server/global/gateway/manager.go
Normal 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()
|
||||
}
|
63
server/global/gateway/tunnel.go
Normal 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)
|
||||
}
|
70
server/global/security/security.go
Normal 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()
|
||||
}
|
97
server/global/session/session.go
Normal 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()
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -69,9 +70,10 @@ type Configuration struct {
|
||||
Parameters map[string]string
|
||||
}
|
||||
|
||||
func NewConfiguration() (ret Configuration) {
|
||||
ret.Parameters = make(map[string]string)
|
||||
return ret
|
||||
func NewConfiguration() (config *Configuration) {
|
||||
config = &Configuration{}
|
||||
config.Parameters = make(map[string]string)
|
||||
return config
|
||||
}
|
||||
|
||||
func (opt *Configuration) SetParameter(name, value string) {
|
||||
@ -129,13 +131,13 @@ type Tunnel struct {
|
||||
rw *bufio.ReadWriter
|
||||
conn net.Conn
|
||||
UUID string
|
||||
Config Configuration
|
||||
Config *Configuration
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@ -151,11 +153,13 @@ func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) {
|
||||
}
|
||||
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("select", selectArg)); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args, err := ret.expect("args")
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
@ -165,18 +169,23 @@ func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) {
|
||||
|
||||
// send size
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, dpi)); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("audio", "audio/L8", "audio/L16")); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("video")); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("image", "image/jpeg", "image/png", "image/webp")); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("timezone", "Asia/Shanghai")); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -191,6 +200,7 @@ func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) {
|
||||
}
|
||||
// send connect
|
||||
if err := ret.WriteInstructionAndFlush(NewInstruction("connect", parameters...)); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -200,6 +210,7 @@ func NewTunnel(address string, config Configuration) (ret *Tunnel, err error) {
|
||||
}
|
||||
|
||||
if len(ready.Args) == 0 {
|
||||
_ = conn.Close()
|
||||
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 {
|
||||
opt.IsOpen = false
|
||||
_ = opt.rw.Flush()
|
||||
return opt.conn.Close()
|
||||
}
|
@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/config"
|
||||
"next-terminal/server/config"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/sirupsen/logrus"
|
34
server/model/access_gateway.go
Normal 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"`
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package model
|
||||
|
||||
type AccessSecurity struct {
|
||||
ID string `json:"id"`
|
||||
Rule string `json:"rule"`
|
||||
IP string `json:"ip"`
|
||||
Source string `json:"source"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
Rule string `gorm:"type:varchar(20)" json:"rule"`
|
||||
IP string `gorm:"type:varchar(500)" json:"ip"`
|
||||
Source string `gorm:"type:varchar(500)" json:"source"`
|
||||
Priority int64 `json:"priority"` // 越小优先级越高
|
||||
}
|
||||
|
||||
|
@ -4,39 +4,41 @@ import (
|
||||
"next-terminal/server/utils"
|
||||
)
|
||||
|
||||
type AssetProto string
|
||||
|
||||
type Asset struct {
|
||||
ID string `gorm:"primary_key " json:"id"`
|
||||
Name string `json:"name"`
|
||||
Protocol string `json:"protocol"`
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
AccountType string `json:"accountType"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
CredentialId string `gorm:"index" json:"credentialId"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Description string `json:"description"`
|
||||
Active bool `json:"active"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Tags string `json:"tags"`
|
||||
Owner string `gorm:"index" json:"owner"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
Name string `gorm:"type:varchar(500)" json:"name"`
|
||||
Protocol string `gorm:"type:varchar(20)" json:"protocol"`
|
||||
IP string `gorm:"type:varchar(200)" json:"ip"`
|
||||
Port int `json:"port"`
|
||||
AccountType string `gorm:"type:varchar(20)" json:"accountType"`
|
||||
Username string `gorm:"type:varchar(200)" json:"username"`
|
||||
Password string `gorm:"type:varchar(500)" json:"password"`
|
||||
CredentialId string `gorm:"index,type:varchar(36)" json:"credentialId"`
|
||||
PrivateKey string `gorm:"type:text" json:"privateKey"`
|
||||
Passphrase string `gorm:"type:varchar(500)" json:"passphrase"`
|
||||
Description string `json:"description"`
|
||||
Active bool `json:"active"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Tags string `json:"tags"`
|
||||
Owner string `gorm:"index,type:varchar(36)" json:"owner"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
AccessGatewayId string `gorm:"type:varchar(36)" json:"accessGatewayId"`
|
||||
}
|
||||
|
||||
type AssetForPage struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Protocol string `json:"protocol"`
|
||||
Port int `json:"port"`
|
||||
Active bool `json:"active"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Tags string `json:"tags"`
|
||||
Owner string `json:"owner"`
|
||||
OwnerName string `json:"ownerName"`
|
||||
SharerCount int64 `json:"sharerCount"`
|
||||
SshMode string `json:"sshMode"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Protocol string `json:"protocol"`
|
||||
Port int `json:"port"`
|
||||
Active bool `json:"active"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Tags string `json:"tags"`
|
||||
Owner string `json:"owner"`
|
||||
OwnerName string `json:"ownerName"`
|
||||
SshMode string `json:"sshMode"`
|
||||
}
|
||||
|
||||
func (r *Asset) TableName() string {
|
||||
|
@ -5,11 +5,11 @@ import (
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
Name string `gorm:"type:varchar(500)" json:"name"`
|
||||
Content string `json:"content"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Owner string `gorm:"index" json:"owner"`
|
||||
Owner string `gorm:"index,type:varchar(36)" json:"owner"`
|
||||
}
|
||||
|
||||
type CommandForPage struct {
|
||||
|
@ -5,15 +5,15 @@ import (
|
||||
)
|
||||
|
||||
type Credential struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
Name string `gorm:"type:varchar(500)" json:"name"`
|
||||
Type string `gorm:"type:varchar(50)" json:"type"`
|
||||
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"`
|
||||
Owner string `gorm:"index" json:"owner"`
|
||||
Owner string `gorm:"index,type:varchar(36)" json:"owner"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
}
|
||||
|
||||
|
@ -5,14 +5,14 @@ import (
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
CronJobId int `json:"cronJobId"`
|
||||
Name string `json:"name"`
|
||||
Func string `json:"func"`
|
||||
Cron string `json:"cron"`
|
||||
Mode string `json:"mode"`
|
||||
Name string `gorm:"type:varchar(500)" json:"name"`
|
||||
Func string `gorm:"type:varchar(200)" json:"func"`
|
||||
Cron string `gorm:"type:varchar(100)" json:"cron"`
|
||||
Mode string `gorm:"type:varchar(50)" json:"mode"`
|
||||
ResourceIds string `json:"resourceIds"`
|
||||
Status string `json:"status"`
|
||||
Status string `gorm:"type:varchar(20)" json:"status"`
|
||||
Metadata string `json:"metadata"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Updated utils.JsonTime `json:"updated"`
|
||||
|
@ -5,24 +5,15 @@ import (
|
||||
)
|
||||
|
||||
type LoginLog struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
UserId string `gorm:"index" json:"userId"`
|
||||
ClientIP string `json:"clientIp"`
|
||||
ClientUserAgent string `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"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
Username string `gorm:"index,type:varchar(200)" json:"username"`
|
||||
ClientIP string `gorm:"type:varchar(200)" json:"clientIp"`
|
||||
ClientUserAgent string `gorm:"type:varchar(500)" json:"clientUserAgent"`
|
||||
LoginTime utils.JsonTime `json:"loginTime"`
|
||||
LogoutTime utils.JsonTime `json:"logoutTime"`
|
||||
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 {
|
||||
|
@ -1,9 +0,0 @@
|
||||
package model
|
||||
|
||||
type Num struct {
|
||||
I string `gorm:"primary_key" json:"i"`
|
||||
}
|
||||
|
||||
func (r *Num) TableName() string {
|
||||
return "nums"
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
package model
|
||||
|
||||
type ResourceSharer struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
ResourceId string `gorm:"index" json:"resourceId"`
|
||||
ResourceType string `gorm:"index" json:"resourceType"`
|
||||
UserId string `gorm:"index" json:"userId"`
|
||||
UserGroupId string `gorm:"index" json:"userGroupId"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
ResourceId string `gorm:"index,type:varchar(36)" json:"resourceId"`
|
||||
ResourceType string `gorm:"index,type:varchar(36)" json:"resourceType"`
|
||||
StrategyId string `gorm:"index,type:varchar(36)" json:"strategyId"`
|
||||
UserId string `gorm:"index,type:varchar(36)" json:"userId"`
|
||||
UserGroupId string `gorm:"index,type:varchar(36)" json:"userGroupId"`
|
||||
}
|
||||
|
||||
func (r *ResourceSharer) TableName() string {
|
||||
|
@ -5,27 +5,35 @@ import (
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
Protocol string `json:"protocol"`
|
||||
IP string `json:"ip"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
Protocol string `gorm:"type:varchar(20)" json:"protocol"`
|
||||
IP string `gorm:"type:varchar(200)" json:"ip"`
|
||||
Port int `json:"port"`
|
||||
ConnectionId string `json:"connectionId"`
|
||||
AssetId string `gorm:"index" json:"assetId"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Creator string `gorm:"index" json:"creator"`
|
||||
ClientIP string `json:"clientIp"`
|
||||
ConnectionId string `gorm:"type:varchar(50)" json:"connectionId"`
|
||||
AssetId string `gorm:"index,type:varchar(36)" json:"assetId"`
|
||||
Username string `gorm:"type:varchar(200)" json:"username"`
|
||||
Password string `gorm:"type:varchar(500)" json:"password"`
|
||||
Creator string `gorm:"index,type:varchar(36)" json:"creator"`
|
||||
ClientIP string `gorm:"type:varchar(200)" json:"clientIp"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Status string `gorm:"index" json:"status"`
|
||||
Recording string `json:"recording"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Status string `gorm:"index,type:varchar(20)" json:"status"`
|
||||
Recording string `gorm:"type:varchar(1000)" json:"recording"`
|
||||
PrivateKey string `gorm:"type:text" json:"privateKey"`
|
||||
Passphrase string `gorm:"type:varchar(500)" json:"passphrase"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
ConnectedTime utils.JsonTime `json:"connectedTime"`
|
||||
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 {
|
||||
@ -54,3 +62,12 @@ type SessionForPage struct {
|
||||
Message string `json:"message"`
|
||||
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
@ -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
@ -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"
|
||||
}
|
@ -5,16 +5,16 @@ import (
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
Username string `gorm:"index" json:"username"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
Username string `gorm:"index,type:varchar(200)" json:"username"`
|
||||
Password string `gorm:"type:varchar(500)" json:"password"`
|
||||
Nickname string `gorm:"type:varchar(500)" json:"nickname"`
|
||||
TOTPSecret string `json:"-"`
|
||||
Online bool `json:"online"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
Type string `json:"type"`
|
||||
Mail string `json:"mail"`
|
||||
Type string `gorm:"type:varchar(20)" json:"type"`
|
||||
Mail string `gorm:"type:varchar(500)" json:"mail"`
|
||||
}
|
||||
|
||||
type UserForPage struct {
|
||||
|
@ -5,8 +5,8 @@ import (
|
||||
)
|
||||
|
||||
type UserGroup struct {
|
||||
ID string `gorm:"primary_key" json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
|
||||
Name string `gorm:"type:varchar(500)" json:"name"`
|
||||
Created utils.JsonTime `json:"created"`
|
||||
}
|
||||
|
||||
|
85
server/repository/access_gateway.go
Normal 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
|
||||
}
|
@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/model"
|
||||
"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) {
|
||||
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 {
|
||||
owner := account.ID
|
||||
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 {
|
||||
@ -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) {
|
||||
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")
|
||||
|
||||
if constant.TypeUser == account.Type {
|
||||
@ -111,7 +121,7 @@ func (r AssetRepository) Find(pageIndex, pageSize int, name, protocol, tags stri
|
||||
if len(tags) > 0 {
|
||||
tagArr := strings.Split(tags, ",")
|
||||
for i := range tagArr {
|
||||
if global.Config.DB == "sqlite" {
|
||||
if config.GlobalCfg.DB == "sqlite" {
|
||||
db = db.Where("(',' || assets.tags || ',') LIKE ?", "%,"+tagArr[i]+",%")
|
||||
dbCounter = dbCounter.Where("(',' || assets.tags || ',') LIKE ?", "%,"+tagArr[i]+",%")
|
||||
} else {
|
||||
@ -189,7 +199,7 @@ func (r AssetRepository) Encrypt(item *model.Asset, password []byte) 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
|
||||
}
|
||||
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) {
|
||||
err = r.DB.Where("id = ?", id).First(&o).Error
|
||||
if err == nil {
|
||||
err = r.Decrypt(&o, global.Config.EncryptionPassword)
|
||||
err = r.Decrypt(&o, config.GlobalCfg.EncryptionPassword)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -260,8 +270,17 @@ func (r AssetRepository) UpdateActiveById(active bool, id string) error {
|
||||
return r.DB.Exec(sql, active, id).Error
|
||||
}
|
||||
|
||||
func (r AssetRepository) DeleteById(id string) error {
|
||||
return r.DB.Where("id = ?", id).Delete(&model.Asset{}).Error
|
||||
func (r AssetRepository) DeleteById(id string) (err 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) {
|
||||
@ -269,6 +288,11 @@ func (r AssetRepository) Count() (total int64, err error) {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -359,7 +401,7 @@ func (r AssetRepository) FindAttrById(assetId string) (o []model.AssetAttribute,
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -383,7 +425,7 @@ func (r AssetRepository) FindAssetAttrMapByAssetId(assetId string) (map[string]i
|
||||
parameterNames = constant.KubernetesParameterNames
|
||||
}
|
||||
propertiesMap := propertyRepository.FindAllMap()
|
||||
var attributeMap = make(map[string]interface{})
|
||||
var attributeMap = make(map[string]string)
|
||||
for name := range propertiesMap {
|
||||
if utils.Contains(parameterNames, name) {
|
||||
attributeMap[name] = propertiesMap[name]
|
||||
|
@ -1,7 +1,7 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@ -80,3 +80,17 @@ func (r CommandRepository) UpdateById(o *model.Command, id string) error {
|
||||
func (r CommandRepository) DeleteById(id string) 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
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package repository
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/model"
|
||||
"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) {
|
||||
if err := r.Encrypt(o, global.Config.EncryptionPassword); err != nil {
|
||||
if err := r.Encrypt(o, config.GlobalCfg.EncryptionPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
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) {
|
||||
err = r.DB.Where("id = ?", id).First(&o).Error
|
||||
if err == nil {
|
||||
err = r.Decrypt(&o, global.Config.EncryptionPassword)
|
||||
err = r.Decrypt(&o, config.GlobalCfg.EncryptionPassword)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
package repository
|
||||
|
||||
/**
|
||||
* 定义了相关模型的持久化层,方便相互之间调用
|
||||
*/
|
||||
var (
|
||||
userRepository *UserRepository
|
||||
userGroupRepository *UserGroupRepository
|
||||
@ -9,9 +12,11 @@ var (
|
||||
propertyRepository *PropertyRepository
|
||||
commandRepository *CommandRepository
|
||||
sessionRepository *SessionRepository
|
||||
numRepository *NumRepository
|
||||
accessSecurityRepository *AccessSecurityRepository
|
||||
accessGatewayRepository *AccessGatewayRepository
|
||||
jobRepository *JobRepository
|
||||
jobLogRepository *JobLogRepository
|
||||
loginLogRepository *LoginLogRepository
|
||||
storageRepository *StorageRepository
|
||||
strategyRepository *StrategyRepository
|
||||
)
|
||||
|
@ -44,10 +44,10 @@ func (r JobRepository) Find(pageIndex, pageSize int, name, status, order, field
|
||||
|
||||
if field == "name" {
|
||||
field = "name"
|
||||
} else if field == "created" {
|
||||
field = "created"
|
||||
} else {
|
||||
} else if field == "updated" {
|
||||
field = "updated"
|
||||
} else {
|
||||
field = "created"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
return r.DB.Create(o).Error
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"next-terminal/server/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@ -24,6 +26,20 @@ func (r JobLogRepository) FindByJobId(jobId string) (o []model.JobLog, err error
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"next-terminal/server/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@ -15,19 +17,24 @@ func NewLoginLogRepository(db *gorm.DB) *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")
|
||||
dbCounter := r.DB.Table("login_logs").Select("DISTINCT login_logs.id")
|
||||
|
||||
if userId != "" {
|
||||
db = db.Where("login_logs.user_id = ?", userId)
|
||||
dbCounter = dbCounter.Where("login_logs.user_id = ?", userId)
|
||||
if username != "" {
|
||||
db = db.Where("username like ?", "%"+username+"%")
|
||||
dbCounter = dbCounter.Where("username like ?", "%"+username+"%")
|
||||
}
|
||||
|
||||
if clientIp != "" {
|
||||
db = db.Where("login_logs.client_ip like ?", "%"+clientIp+"%")
|
||||
dbCounter = dbCounter.Where("login_logs.client_ip like ?", "%"+clientIp+"%")
|
||||
db = db.Where("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
|
||||
@ -35,20 +42,26 @@ func (r LoginLogRepository) Find(pageIndex, pageSize int, userId, clientIp strin
|
||||
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 {
|
||||
o = make([]model.LoginLogForPage, 0)
|
||||
o = make([]model.LoginLog, 0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (r LoginLogRepository) FindAliveLoginLogsByUserId(userId string) (o []model.LoginLog, err error) {
|
||||
err = r.DB.Where("logout_time is null and user_id = ?", userId).Find(&o).Error
|
||||
func (r LoginLogRepository) FindAliveLoginLogsByUsername(username string) (o []model.LoginLog, err 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
|
||||
}
|
||||
|
||||
@ -60,6 +73,10 @@ func (r LoginLogRepository) DeleteByIdIn(ids []string) (err 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) {
|
||||
err = r.DB.Where("id = ?", id).First(&o).Error
|
||||
return
|
||||
|
@ -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
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/guacd"
|
||||
"next-terminal/server/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@ -33,6 +32,10 @@ func (r PropertyRepository) UpdateByName(o *model.Property, name string) 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) {
|
||||
err = r.DB.Where("name = ?", name).First(&o).Error
|
||||
return
|
||||
@ -46,19 +49,3 @@ func (r PropertyRepository) FindAllMap() map[string]string {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -18,14 +18,6 @@ func NewResourceSharerRepository(db *gorm.DB) *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) {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
for i := range resourceIds {
|
||||
@ -138,11 +130,13 @@ func (r *ResourceSharerRepository) AddSharerResources(userGroupId, userId, resou
|
||||
return echo.NewHTTPError(400, "参数错误")
|
||||
}
|
||||
|
||||
// 保证同一个资产只能分配给一个用户或者组
|
||||
id := utils.Sign([]string{resourceId, resourceType, userId, userGroupId})
|
||||
resource := &model.ResourceSharer{
|
||||
ID: id,
|
||||
ResourceId: resourceId,
|
||||
ResourceType: resourceType,
|
||||
StrategyId: strategyId,
|
||||
UserId: userId,
|
||||
UserGroupId: userGroupId,
|
||||
}
|
||||
@ -192,3 +186,35 @@ func (r *ResourceSharerRepository) FindAssetIdsByUserId(userId string) (assetIds
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/model"
|
||||
"next-terminal/server/utils"
|
||||
|
||||
@ -110,7 +110,7 @@ func (r SessionRepository) Decrypt(item *model.Session) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword)
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, config.GlobalCfg.EncryptionPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -121,7 +121,7 @@ func (r SessionRepository) Decrypt(item *model.Session) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword)
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, config.GlobalCfg.EncryptionPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -132,7 +132,7 @@ func (r SessionRepository) Decrypt(item *model.Session) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, global.Config.EncryptionPassword)
|
||||
decryptedCBC, err := utils.AesDecryptCBC(origData, config.GlobalCfg.EncryptionPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -164,12 +164,9 @@ func (r SessionRepository) DeleteById(id string) error {
|
||||
}
|
||||
|
||||
func (r SessionRepository) DeleteByIds(sessionIds []string) error {
|
||||
drivePath, err := propertyRepository.GetRecordingPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordingPath := config.GlobalCfg.Guacd.Recording
|
||||
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
|
||||
}
|
||||
if err := r.DeleteById(sessionIds[i]); err != nil {
|
||||
@ -188,35 +185,27 @@ func (r SessionRepository) CountOnlineSession() (total int64, err error) {
|
||||
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 {
|
||||
sql := "update sessions set password = '-',private_key = '-', passphrase = '-' where 1=1"
|
||||
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
|
||||
}
|
||||
|
87
server/repository/storage.go
Normal 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
|
||||
}
|
73
server/repository/strategy.go
Normal 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
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/constant"
|
||||
"next-terminal/server/constant"
|
||||
"next-terminal/server/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@ -105,9 +105,9 @@ func (r UserRepository) Update(o *model.User) error {
|
||||
return r.DB.Updates(o).Error
|
||||
}
|
||||
|
||||
func (r UserRepository) UpdateOnline(id string, online bool) error {
|
||||
sql := "update users set online = ? where id = ?"
|
||||
return r.DB.Exec(sql, online, id).Error
|
||||
func (r UserRepository) UpdateOnlineByUsername(username string, online bool) error {
|
||||
sql := "update users set online = ? where username = ?"
|
||||
return r.DB.Exec(sql, online, username).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
|
||||
return
|
||||
}
|
||||
|
||||
func (r UserRepository) Count() (total int64, err error) {
|
||||
err = r.DB.Find(&model.User{}).Count(&total).Error
|
||||
return
|
||||
}
|
||||
|
65
server/service/access_gateway.go
Normal 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
@ -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
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"next-terminal/pkg/global"
|
||||
"next-terminal/server/config"
|
||||
"next-terminal/server/repository"
|
||||
)
|
||||
|
||||
@ -23,7 +23,7 @@ func (r CredentialService) Encrypt() error {
|
||||
if item.Encrypted {
|
||||
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
|
||||
}
|
||||
if err := r.credentialRepository.UpdateById(&item, item.ID); err != nil {
|
5
server/service/definitions.go
Normal file
@ -0,0 +1,5 @@
|
||||
package service
|
||||
|
||||
var (
|
||||
accessGatewayService *AccessGatewayService
|
||||
)
|