mirror of
https://github.com/GravitLauncher/Launcher
synced 2024-12-22 08:31:07 +03:00
4.0.0 init commit
This commit is contained in:
commit
a04878f176
284 changed files with 21374 additions and 0 deletions
26
.gitattributes
vendored
Normal file
26
.gitattributes
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
* text eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.sh text eol=lf
|
||||
|
||||
*.patch text eol=lf
|
||||
*.java text eol=lf
|
||||
*.scala text eol=lf
|
||||
*.groovy text eol=lf
|
||||
|
||||
*.gradle text eol=crlf
|
||||
gradle.properties text eol=crlf
|
||||
/gradle/wrapper/gradle-wrapper.properties text eol=crlf
|
||||
*.cfg text eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jar binary
|
||||
*.war binary
|
||||
*.lzma binary
|
||||
*.zip binary
|
||||
*.gzip binary
|
||||
*.dll binary
|
||||
*.so binary
|
||||
*.exe binary
|
||||
|
||||
*.gitattributes text eol=crlf
|
||||
*.gitignore text eol=crlf
|
109
.gitignore
vendored
Normal file
109
.gitignore
vendored
Normal file
|
@ -0,0 +1,109 @@
|
|||
### Eclipse ###
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
tst/
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# CDT-specific (C/C++ Development Tooling)
|
||||
.cproject
|
||||
|
||||
# CDT- autotools
|
||||
.autotools
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
# STS (Spring Tool Suite)
|
||||
.springBeans
|
||||
|
||||
# Code Recommenders
|
||||
.recommenders/
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
|
||||
### Eclipse Patch ###
|
||||
# Eclipse Core
|
||||
.project
|
||||
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
.classpath
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated
|
||||
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### Intellij+all Patch ###
|
||||
.idea/
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
Launcher.iws
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
.gradle/
|
||||
/build/
|
||||
build/
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# Other
|
||||
buildnumber
|
||||
*.directory
|
||||
cmd.bat
|
||||
cmd.sh
|
12
.travis.yml
Normal file
12
.travis.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
language: java
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
script:
|
||||
- ./gradlew assemble build
|
||||
addons:
|
||||
artifacts: true
|
674
LICENSE
Normal file
674
LICENSE
Normal file
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
86
LaunchServer/build.gradle
Normal file
86
LaunchServer/build.gradle
Normal file
|
@ -0,0 +1,86 @@
|
|||
def mainClassName = "LaunchServer"
|
||||
def mainAgentName = "StarterAgent"
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://hub.spigotmc.org/nexus/content/repositories/snapshots"
|
||||
}
|
||||
maven {
|
||||
url "http://maven.geomajas.org/"
|
||||
}
|
||||
maven {
|
||||
url "https://oss.sonatype.org/content/repositories/snapshots"
|
||||
}
|
||||
maven {
|
||||
url "http://repo.md-5.net/content/groups/public"
|
||||
}
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
configurations {
|
||||
bundleOnly
|
||||
bundle
|
||||
hikari
|
||||
bundle.extendsFrom bundleOnly
|
||||
compileOnly.extendsFrom bundle, hikari
|
||||
}
|
||||
|
||||
jar {
|
||||
dependsOn parent.childProjects.Launcher.tasks.build, parent.childProjects.Launcher.tasks.genRuntimeJS, parent.childProjects.Launcher.tasks.jar
|
||||
from { configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) } }
|
||||
from(parent.childProjects.Launcher.tasks.jar.archivePath, parent.childProjects.Launcher.tasks.genRuntimeJS.archivePath)
|
||||
manifest.attributes("Main-Class": mainClassName,
|
||||
"Premain-Class": mainAgentName,
|
||||
"Can-Redefine-Classes": "true",
|
||||
"Can-Retransform-Classes": "true",
|
||||
"Can-Set-Native-Method-Prefix": "true"
|
||||
)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':libLauncher') // pack
|
||||
compileOnly 'org.spigotmc:spigot-api:1.8-R0.1-SNAPSHOT' // api
|
||||
compileOnly 'net.md-5:bungeecord-api:1.8-SNAPSHOT' // api
|
||||
|
||||
compileOnly 'org.ow2.asm:asm-debug-all:5.0.4'
|
||||
bundleOnly 'org.ow2.asm:asm-all:5.0.4'
|
||||
bundle 'org.apache.logging.log4j:log4j-core:2.9.0'
|
||||
bundle 'mysql:mysql-connector-java:8.0.12'
|
||||
bundle 'jline:jline:2.14.6'
|
||||
bundle 'net.sf.proguard:proguard-base:6.0.3'
|
||||
bundle 'org.bouncycastle:bcpkix-jdk15on:1.49'
|
||||
bundle 'org.fusesource.jansi:jansi:1.17.1'
|
||||
bundle 'commons-io:commons-io:2.6'
|
||||
bundle 'org.javassist:javassist:3.23.1-GA'
|
||||
|
||||
bundle 'org.slf4j:slf4j-simple:1.7.25'
|
||||
bundle 'org.slf4j:slf4j-api:1.7.25'
|
||||
|
||||
hikari 'io.micrometer:micrometer-core:1.0.6'
|
||||
hikari('hikari-cp:hikari-cp:2.6.0') {
|
||||
exclude group: 'javassist'
|
||||
exclude group: 'io.micrometer'
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
|
||||
compileOnly('net.sf.launch4j:launch4j:3.12') { // need user
|
||||
exclude group: '*'
|
||||
}
|
||||
|
||||
//compile 'org.mozilla:rhino:1.7.10' will be module
|
||||
}
|
||||
|
||||
task hikari(type: Copy) {
|
||||
into "$buildDir/libs/libraries/hikaricp"
|
||||
from configurations.hikari
|
||||
}
|
||||
|
||||
task dumpLibs(type: Copy) {
|
||||
dependsOn tasks.hikari
|
||||
into "$buildDir/libs/libraries"
|
||||
from configurations.bundle
|
||||
}
|
||||
|
||||
build.dependsOn tasks.dumpLibs
|
|
@ -0,0 +1,592 @@
|
|||
package ru.gravit.launchserver;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.KeyPair;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.hasher.HashedDir;
|
||||
import ru.gravit.launcher.helper.CommonHelper;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.JVMHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.managers.GarbageManager;
|
||||
import ru.gravit.launcher.profiles.ClientProfile;
|
||||
import ru.gravit.launcher.serialize.config.ConfigObject;
|
||||
import ru.gravit.launcher.serialize.config.TextConfigReader;
|
||||
import ru.gravit.launcher.serialize.config.TextConfigWriter;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.BooleanConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.IntegerConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
import ru.gravit.launcher.serialize.signed.SignedObjectHolder;
|
||||
import ru.gravit.launchserver.auth.AuthLimiter;
|
||||
import ru.gravit.launchserver.auth.handler.AuthHandler;
|
||||
import ru.gravit.launchserver.auth.hwid.HWIDHandler;
|
||||
import ru.gravit.launchserver.auth.provider.AuthProvider;
|
||||
import ru.gravit.launchserver.binary.EXEL4JLauncherBinary;
|
||||
import ru.gravit.launchserver.binary.EXELauncherBinary;
|
||||
import ru.gravit.launchserver.binary.JARLauncherBinary;
|
||||
import ru.gravit.launchserver.binary.LauncherBinary;
|
||||
import ru.gravit.launchserver.command.handler.CommandHandler;
|
||||
import ru.gravit.launchserver.command.handler.JLineCommandHandler;
|
||||
import ru.gravit.launchserver.command.handler.StdCommandHandler;
|
||||
import ru.gravit.launchserver.manangers.BuildHookManager;
|
||||
import ru.gravit.launchserver.manangers.ModulesManager;
|
||||
import ru.gravit.launchserver.manangers.SessionManager;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
import ru.gravit.launchserver.socket.ServerSocketHandler;
|
||||
import ru.gravit.launchserver.texture.TextureProvider;
|
||||
|
||||
public final class LaunchServer implements Runnable, AutoCloseable {
|
||||
public static final class Config extends ConfigObject {
|
||||
@LauncherAPI
|
||||
public final int port;
|
||||
|
||||
// Handlers & Providers
|
||||
@LauncherAPI
|
||||
public final AuthHandler authHandler;
|
||||
@LauncherAPI
|
||||
public final AuthProvider authProvider;
|
||||
@LauncherAPI
|
||||
public final TextureProvider textureProvider;
|
||||
@LauncherAPI
|
||||
public final HWIDHandler hwidHandler;
|
||||
|
||||
// Misc options
|
||||
@LauncherAPI
|
||||
public final ExeConf launch4j;
|
||||
@LauncherAPI
|
||||
public final SignConf sign;
|
||||
@LauncherAPI
|
||||
public final boolean compress;
|
||||
@LauncherAPI
|
||||
public final int authRateLimit;
|
||||
@LauncherAPI
|
||||
public final int authRateLimitMilis;
|
||||
@LauncherAPI
|
||||
public final String authRejectString;
|
||||
@LauncherAPI
|
||||
public final String whitelistRejectString;
|
||||
@LauncherAPI
|
||||
public final boolean genMappings;
|
||||
@LauncherAPI
|
||||
public final String binaryName;
|
||||
private final StringConfigEntry address;
|
||||
private final String bindAddress;
|
||||
|
||||
private Config(BlockConfigEntry block, Path coredir) {
|
||||
super(block);
|
||||
address = block.getEntry("address", StringConfigEntry.class);
|
||||
port = VerifyHelper.verifyInt(block.getEntryValue("port", IntegerConfigEntry.class),
|
||||
VerifyHelper.range(0, 65535), "Illegal LaunchServer port");
|
||||
authRateLimit = VerifyHelper.verifyInt(block.getEntryValue("authRateLimit", IntegerConfigEntry.class),
|
||||
VerifyHelper.range(0, 1000000), "Illegal authRateLimit");
|
||||
authRateLimitMilis = VerifyHelper.verifyInt(block.getEntryValue("authRateLimitMilis", IntegerConfigEntry.class),
|
||||
VerifyHelper.range(10, 10000000), "Illegal authRateLimitMillis");
|
||||
bindAddress = block.hasEntry("bindAddress") ?
|
||||
block.getEntryValue("bindAddress", StringConfigEntry.class) : getAddress();
|
||||
authRejectString = block.hasEntry("authRejectString") ?
|
||||
block.getEntryValue("authRejectString", StringConfigEntry.class) : "Вы превысили лимит авторизаций. Подождите некоторое время перед повторной попыткой";
|
||||
whitelistRejectString = block.hasEntry("whitelistRejectString") ?
|
||||
block.getEntryValue("whitelistRejectString", StringConfigEntry.class) : "Вас нет в белом списке";
|
||||
|
||||
|
||||
// Set handlers & providers
|
||||
authHandler = AuthHandler.newHandler(block.getEntryValue("authHandler", StringConfigEntry.class),
|
||||
block.getEntry("authHandlerConfig", BlockConfigEntry.class));
|
||||
authProvider = AuthProvider.newProvider(block.getEntryValue("authProvider", StringConfigEntry.class),
|
||||
block.getEntry("authProviderConfig", BlockConfigEntry.class));
|
||||
textureProvider = TextureProvider.newProvider(block.getEntryValue("textureProvider", StringConfigEntry.class),
|
||||
block.getEntry("textureProviderConfig", BlockConfigEntry.class));
|
||||
hwidHandler = HWIDHandler.newHandler(block.getEntryValue("hwidHandler", StringConfigEntry.class),
|
||||
block.getEntry("hwidHandlerConfig", BlockConfigEntry.class));
|
||||
|
||||
// Set misc config
|
||||
genMappings = block.getEntryValue("proguardPrintMappings", BooleanConfigEntry.class);
|
||||
launch4j = new ExeConf(block.getEntry("launch4J", BlockConfigEntry.class));
|
||||
sign = new SignConf(block.getEntry("signing", BlockConfigEntry.class), coredir);
|
||||
binaryName = block.getEntryValue("binaryName", StringConfigEntry.class);
|
||||
compress = block.getEntryValue("compress", BooleanConfigEntry.class);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public String getAddress() {
|
||||
return address.getValue();
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public String getBindAddress() {
|
||||
return bindAddress;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public SocketAddress getSocketAddress() {
|
||||
return new InetSocketAddress(bindAddress, port);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void setAddress(String address) {
|
||||
this.address.setValue(address);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void verify() {
|
||||
VerifyHelper.verify(getAddress(), VerifyHelper.NOT_EMPTY, "LaunchServer address can't be empty");
|
||||
}
|
||||
}
|
||||
public static class ExeConf extends ConfigObject {
|
||||
public final boolean enabled;
|
||||
public String productName;
|
||||
public String productVer;
|
||||
public String fileDesc;
|
||||
public String fileVer;
|
||||
public String internalName;
|
||||
public String copyright;
|
||||
public String trademarks;
|
||||
|
||||
public String txtFileVersion;
|
||||
public String txtProductVersion;
|
||||
|
||||
private ExeConf(BlockConfigEntry block) {
|
||||
super(block);
|
||||
enabled = block.getEntryValue("enabled", BooleanConfigEntry.class);
|
||||
productName = block.hasEntry("productName") ? block.getEntryValue("productName", StringConfigEntry.class)
|
||||
: "sashok724's Launcher v3 mod by Gravit";
|
||||
productVer = block.hasEntry("productVer") ? block.getEntryValue("productVer", StringConfigEntry.class)
|
||||
: "1.0.0.0";
|
||||
fileDesc = block.hasEntry("fileDesc") ? block.getEntryValue("fileDesc", StringConfigEntry.class)
|
||||
: "sashok724's Launcher v3 mod by Gravit";
|
||||
fileVer = block.hasEntry("fileVer") ? block.getEntryValue("fileVer", StringConfigEntry.class) : "1.0.0.0";
|
||||
internalName = block.hasEntry("internalName") ? block.getEntryValue("internalName", StringConfigEntry.class)
|
||||
: "Launcher";
|
||||
copyright = block.hasEntry("copyright") ? block.getEntryValue("copyright", StringConfigEntry.class)
|
||||
: "© sashok724 LLC";
|
||||
trademarks = block.hasEntry("trademarks") ? block.getEntryValue("trademarks", StringConfigEntry.class)
|
||||
: "This product is licensed under MIT License";
|
||||
txtFileVersion = block.hasEntry("txtFileVersion") ? block.getEntryValue("txtFileVersion", StringConfigEntry.class)
|
||||
: CommonHelper.formatVars("$VERSION$, build $BUILDNUMBER$");
|
||||
txtProductVersion = block.hasEntry("txtProductVersion") ? block.getEntryValue("txtProductVersion", StringConfigEntry.class)
|
||||
: CommonHelper.formatVars("$VERSION$, build $BUILDNUMBER$");
|
||||
}
|
||||
}
|
||||
private final class ProfilesFileVisitor extends SimpleFileVisitor<Path> {
|
||||
private final Collection<SignedObjectHolder<ClientProfile>> result;
|
||||
|
||||
private ProfilesFileVisitor(Collection<SignedObjectHolder<ClientProfile>> result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
LogHelper.info("Syncing '%s' profile", IOHelper.getFileName(file));
|
||||
|
||||
// Read profile
|
||||
ClientProfile profile;
|
||||
try (BufferedReader reader = IOHelper.newReader(file)) {
|
||||
profile = new ClientProfile(TextConfigReader.read(reader, true));
|
||||
}
|
||||
profile.verify();
|
||||
|
||||
// Add SIGNED profile to result list
|
||||
result.add(new SignedObjectHolder<>(profile, privateKey));
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
}
|
||||
public static class SignConf extends ConfigObject {
|
||||
public final boolean enabled;
|
||||
public String algo;
|
||||
public Path key;
|
||||
public boolean hasStorePass;
|
||||
public String storepass;
|
||||
public boolean hasPass;
|
||||
public String pass;
|
||||
public String keyalias;
|
||||
private SignConf(BlockConfigEntry block, Path coredir) {
|
||||
super(block);
|
||||
enabled = block.getEntryValue("enabled", BooleanConfigEntry.class);
|
||||
storepass = null;
|
||||
pass = null;
|
||||
if (enabled) {
|
||||
algo = block.hasEntry("storeType") ? block.getEntryValue("storeType", StringConfigEntry.class) : "JKS";
|
||||
key = coredir.resolve(block.getEntryValue("keyFile", StringConfigEntry.class));
|
||||
hasStorePass = block.hasEntry("keyStorePass");
|
||||
if (hasStorePass) storepass = block.getEntryValue("keyStorePass", StringConfigEntry.class);
|
||||
keyalias = block.getEntryValue("keyAlias", StringConfigEntry.class);
|
||||
hasPass = block.hasEntry("keyPass");
|
||||
if (hasPass) pass = block.getEntryValue("keyPass", StringConfigEntry.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static void main(String... args) throws Throwable {
|
||||
JVMHelper.verifySystemProperties(LaunchServer.class, true);
|
||||
LogHelper.addOutput(IOHelper.WORKING_DIR.resolve("LaunchServer.log"));
|
||||
LogHelper.printVersion("LaunchServer");
|
||||
|
||||
// Start LaunchServer
|
||||
Instant start = Instant.now();
|
||||
try {
|
||||
try (LaunchServer lsrv = new LaunchServer(IOHelper.WORKING_DIR, false)) {
|
||||
lsrv.run();
|
||||
}
|
||||
} catch (Throwable exc) {
|
||||
LogHelper.error(exc);
|
||||
return;
|
||||
}
|
||||
Instant end = Instant.now();
|
||||
LogHelper.debug("LaunchServer started in %dms", Duration.between(start, end).toMillis());
|
||||
}
|
||||
// Constant paths
|
||||
@LauncherAPI
|
||||
public final Path dir;
|
||||
|
||||
@LauncherAPI
|
||||
public final Path configFile;
|
||||
@LauncherAPI
|
||||
public final Path publicKeyFile;
|
||||
@LauncherAPI
|
||||
public final Path privateKeyFile;
|
||||
@LauncherAPI
|
||||
public final Path updatesDir;
|
||||
|
||||
@LauncherAPI
|
||||
public final Path profilesDir;
|
||||
// Server config
|
||||
@LauncherAPI
|
||||
public final Config config;
|
||||
|
||||
@LauncherAPI
|
||||
public final RSAPublicKey publicKey;
|
||||
@LauncherAPI
|
||||
public final RSAPrivateKey privateKey;
|
||||
@LauncherAPI
|
||||
public final boolean portable;
|
||||
// Launcher binary
|
||||
@LauncherAPI
|
||||
public final LauncherBinary launcherBinary;
|
||||
@LauncherAPI
|
||||
public final LauncherBinary launcherEXEBinary;
|
||||
// HWID ban + anti-brutforce
|
||||
@LauncherAPI
|
||||
public final AuthLimiter limiter;
|
||||
@LauncherAPI
|
||||
public final SessionManager sessionManager;
|
||||
// Server
|
||||
@LauncherAPI
|
||||
public final ModulesManager modulesManager;
|
||||
|
||||
@LauncherAPI
|
||||
public final BuildHookManager buildHookManager;
|
||||
@LauncherAPI
|
||||
public final ProguardConf proguardConf;
|
||||
|
||||
@LauncherAPI
|
||||
public final CommandHandler commandHandler;
|
||||
|
||||
@LauncherAPI
|
||||
public final ServerSocketHandler serverSocketHandler;
|
||||
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
|
||||
// Updates and profiles
|
||||
private volatile List<SignedObjectHolder<ClientProfile>> profilesList;
|
||||
|
||||
private volatile Map<String, SignedObjectHolder<HashedDir>> updatesDirMap;
|
||||
|
||||
public LaunchServer(Path dir, boolean portable) throws IOException, InvalidKeySpecException {
|
||||
//setScriptBindings();
|
||||
this.portable = portable;
|
||||
|
||||
// Setup config locations
|
||||
this.dir = dir;
|
||||
configFile = dir.resolve("LaunchServer.cfg");
|
||||
publicKeyFile = dir.resolve("public.key");
|
||||
privateKeyFile = dir.resolve("private.key");
|
||||
updatesDir = dir.resolve("updates");
|
||||
profilesDir = dir.resolve("profiles");
|
||||
|
||||
//Registration handlers and providers
|
||||
AuthHandler.registerHandlers();
|
||||
AuthProvider.registerProviders();
|
||||
TextureProvider.registerProviders();
|
||||
HWIDHandler.registerHandlers();
|
||||
Response.registerResponses();
|
||||
|
||||
// Set command handler
|
||||
CommandHandler localCommandHandler;
|
||||
if (portable)
|
||||
localCommandHandler = new StdCommandHandler(this, false);
|
||||
else
|
||||
try {
|
||||
Class.forName("jline.Terminal");
|
||||
|
||||
// JLine2 available
|
||||
localCommandHandler = new JLineCommandHandler(this);
|
||||
LogHelper.info("JLine2 terminal enabled");
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
localCommandHandler = new StdCommandHandler(this, true);
|
||||
LogHelper.warning("JLine2 isn't in classpath, using std");
|
||||
}
|
||||
commandHandler = localCommandHandler;
|
||||
|
||||
// Set key pair
|
||||
if (IOHelper.isFile(publicKeyFile) && IOHelper.isFile(privateKeyFile)) {
|
||||
LogHelper.info("Reading RSA keypair");
|
||||
publicKey = SecurityHelper.toPublicRSAKey(IOHelper.read(publicKeyFile));
|
||||
privateKey = SecurityHelper.toPrivateRSAKey(IOHelper.read(privateKeyFile));
|
||||
if (!publicKey.getModulus().equals(privateKey.getModulus()))
|
||||
throw new IOException("Private and public key modulus mismatch");
|
||||
} else {
|
||||
LogHelper.info("Generating RSA keypair");
|
||||
KeyPair pair = SecurityHelper.genRSAKeyPair();
|
||||
publicKey = (RSAPublicKey) pair.getPublic();
|
||||
privateKey = (RSAPrivateKey) pair.getPrivate();
|
||||
|
||||
// Write key pair files
|
||||
LogHelper.info("Writing RSA keypair files");
|
||||
IOHelper.write(publicKeyFile, publicKey.getEncoded());
|
||||
IOHelper.write(privateKeyFile, privateKey.getEncoded());
|
||||
}
|
||||
|
||||
// Print keypair fingerprints
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(publicKey.getModulus().toByteArray());
|
||||
LogHelper.subInfo("Modulus CRC32: 0x%08x", crc.getValue());
|
||||
|
||||
// pre init modules
|
||||
modulesManager = new ModulesManager(this);
|
||||
modulesManager.autoload();
|
||||
modulesManager.preInitModules();
|
||||
|
||||
// Read LaunchServer config
|
||||
generateConfigIfNotExists();
|
||||
LogHelper.info("Reading LaunchServer config file");
|
||||
try (BufferedReader reader = IOHelper.newReader(configFile)) {
|
||||
config = new Config(TextConfigReader.read(reader, true), dir);
|
||||
}
|
||||
config.verify();
|
||||
|
||||
// build hooks, anti-brutforce and other
|
||||
buildHookManager = new BuildHookManager();
|
||||
limiter = new AuthLimiter(this);
|
||||
proguardConf = new ProguardConf(this);
|
||||
sessionManager = new SessionManager();
|
||||
GarbageManager.registerNeedGC(sessionManager);
|
||||
GarbageManager.registerNeedGC(limiter);
|
||||
|
||||
// init modules
|
||||
modulesManager.initModules();
|
||||
|
||||
// Set launcher EXE binary
|
||||
launcherBinary = new JARLauncherBinary(this);
|
||||
launcherEXEBinary = binary();
|
||||
syncLauncherBinaries();
|
||||
|
||||
// Sync updates dir
|
||||
if (!IOHelper.isDir(updatesDir))
|
||||
Files.createDirectory(updatesDir);
|
||||
syncUpdatesDir(null);
|
||||
|
||||
// Sync profiles dir
|
||||
if (!IOHelper.isDir(profilesDir))
|
||||
Files.createDirectory(profilesDir);
|
||||
syncProfilesDir();
|
||||
|
||||
|
||||
// Set server socket thread
|
||||
serverSocketHandler = new ServerSocketHandler(this, sessionManager);
|
||||
|
||||
// post init modules
|
||||
modulesManager.postInitModules();
|
||||
}
|
||||
|
||||
private LauncherBinary binary() {
|
||||
if (config.launch4j.enabled) return new EXEL4JLauncherBinary(this);
|
||||
return new EXELauncherBinary(this);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void buildLauncherBinaries() throws IOException {
|
||||
launcherBinary.build();
|
||||
launcherEXEBinary.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
serverSocketHandler.close();
|
||||
|
||||
// Close handlers & providers
|
||||
try {
|
||||
config.authHandler.close();
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
try {
|
||||
config.authProvider.close();
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
try {
|
||||
config.textureProvider.close();
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
try {
|
||||
config.hwidHandler.close();
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
modulesManager.close();
|
||||
// Print last message before death :(
|
||||
LogHelper.info("LaunchServer stopped");
|
||||
}
|
||||
|
||||
private void generateConfigIfNotExists() throws IOException {
|
||||
if (IOHelper.isFile(configFile))
|
||||
return;
|
||||
|
||||
// Create new config
|
||||
LogHelper.info("Creating LaunchServer config");
|
||||
Config newConfig;
|
||||
try (BufferedReader reader = IOHelper.newReader(IOHelper.getResourceURL("ru/gravit/launchserver/defaults/config.cfg"))) {
|
||||
newConfig = new Config(TextConfigReader.read(reader, false), dir);
|
||||
}
|
||||
|
||||
// Set server address
|
||||
if (portable) {
|
||||
LogHelper.warning("Setting LaunchServer address to 'localhost'");
|
||||
newConfig.setAddress("localhost");
|
||||
} else {
|
||||
LogHelper.println("LaunchServer address: ");
|
||||
newConfig.setAddress(commandHandler.readLine());
|
||||
}
|
||||
|
||||
// Write LaunchServer config
|
||||
LogHelper.info("Writing LaunchServer config file");
|
||||
try (BufferedWriter writer = IOHelper.newWriter(configFile)) {
|
||||
TextConfigWriter.write(newConfig.block, writer, true);
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
@SuppressWarnings("ReturnOfCollectionOrArrayField")
|
||||
public Collection<SignedObjectHolder<ClientProfile>> getProfiles() {
|
||||
return profilesList;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public SignedObjectHolder<HashedDir> getUpdateDir(String name) {
|
||||
return updatesDirMap.get(name);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public Set<Entry<String, SignedObjectHolder<HashedDir>>> getUpdateDirs() {
|
||||
return updatesDirMap.entrySet();
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void rebindServerSocket() {
|
||||
serverSocketHandler.close();
|
||||
CommonHelper.newThread("Server Socket Thread", false, serverSocketHandler).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (started.getAndSet(true))
|
||||
throw new IllegalStateException("LaunchServer has been already started");
|
||||
|
||||
// Add shutdown hook, then start LaunchServer
|
||||
if (!portable) {
|
||||
JVMHelper.RUNTIME.addShutdownHook(CommonHelper.newThread(null, false, this::close));
|
||||
CommonHelper.newThread("Command Thread", true, commandHandler).start();
|
||||
}
|
||||
rebindServerSocket();
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void syncLauncherBinaries() throws IOException {
|
||||
LogHelper.info("Syncing launcher binaries");
|
||||
|
||||
// Syncing launcher binary
|
||||
LogHelper.info("Syncing launcher binary file");
|
||||
if (!launcherBinary.sync()) LogHelper.warning("Missing launcher binary file");
|
||||
|
||||
// Syncing launcher EXE binary
|
||||
LogHelper.info("Syncing launcher EXE binary file");
|
||||
if (!launcherEXEBinary.sync() && config.launch4j.enabled)
|
||||
LogHelper.warning("Missing launcher EXE binary file");
|
||||
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void syncProfilesDir() throws IOException {
|
||||
LogHelper.info("Syncing profiles dir");
|
||||
List<SignedObjectHolder<ClientProfile>> newProfies = new LinkedList<>();
|
||||
IOHelper.walk(profilesDir, new ProfilesFileVisitor(newProfies), false);
|
||||
|
||||
// Sort and set new profiles
|
||||
newProfies.sort(Comparator.comparing(a -> a.object));
|
||||
profilesList = Collections.unmodifiableList(newProfies);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void syncUpdatesDir(Collection<String> dirs) throws IOException {
|
||||
LogHelper.info("Syncing updates dir");
|
||||
Map<String, SignedObjectHolder<HashedDir>> newUpdatesDirMap = new HashMap<>(16);
|
||||
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(updatesDir)) {
|
||||
for (Path updateDir : dirStream) {
|
||||
if (Files.isHidden(updateDir))
|
||||
continue; // Skip hidden
|
||||
|
||||
// Resolve name and verify is dir
|
||||
String name = IOHelper.getFileName(updateDir);
|
||||
if (!IOHelper.isDir(updateDir)) {
|
||||
LogHelper.warning("Not update dir: '%s'", name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add from previous map (it's guaranteed to be non-null)
|
||||
if (dirs != null && !dirs.contains(name)) {
|
||||
SignedObjectHolder<HashedDir> hdir = updatesDirMap.get(name);
|
||||
if (hdir != null) {
|
||||
newUpdatesDirMap.put(name, hdir);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync and sign update dir
|
||||
LogHelper.info("Syncing '%s' update dir", name);
|
||||
HashedDir updateHDir = new HashedDir(updateDir, null, true, true);
|
||||
newUpdatesDirMap.put(name, new SignedObjectHolder<>(updateHDir, privateKey));
|
||||
}
|
||||
}
|
||||
updatesDirMap = Collections.unmodifiableMap(newUpdatesDirMap);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package ru.gravit.launchserver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
|
||||
public class ProguardConf {
|
||||
private static final String charsFirst = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ";
|
||||
private static final String chars = "1aAbBcC2dDeEfF3gGhHiI4jJkKl5mMnNoO6pPqQrR7sStT8uUvV9wWxX0yYzZ";
|
||||
private static String generateString(SecureRandom rand, int il) {
|
||||
StringBuffer sb = new StringBuffer(il);
|
||||
sb.append(charsFirst.charAt(rand.nextInt(charsFirst.length())));
|
||||
for (int i = 0; i < il - 1; i++) sb.append(chars.charAt(rand.nextInt(chars.length())));
|
||||
return sb.toString();
|
||||
}
|
||||
private final LaunchServer srv;
|
||||
public final Path proguard;
|
||||
public final Path config;
|
||||
public final Path mappings;
|
||||
public final Path words;
|
||||
public final Set<String> confStrs;
|
||||
|
||||
public ProguardConf(LaunchServer srv) {
|
||||
this.srv = srv;
|
||||
proguard = this.srv.dir.resolve("proguard");
|
||||
config = proguard.resolve("proguard.config");
|
||||
mappings = proguard.resolve("mappings.pro");
|
||||
words = proguard.resolve("random.pro");
|
||||
confStrs = new HashSet<>();
|
||||
prepare(false);
|
||||
confStrs.add(readConf());
|
||||
if (this.srv.config.genMappings) confStrs.add("-printmapping \'" + mappings.toFile().getName() + "\'");
|
||||
confStrs.add("-obfuscationdictionary \'" + words.toFile().getName() + "\'");
|
||||
confStrs.add("-classobfuscationdictionary \'" + words.toFile().getName() + "\'");
|
||||
|
||||
}
|
||||
|
||||
private void genConfig(boolean force) throws IOException {
|
||||
if (IOHelper.exists(config) && !force) return;
|
||||
Files.deleteIfExists(config);
|
||||
config.toFile().createNewFile();
|
||||
try (OutputStream out = IOHelper.newOutput(config); InputStream in = IOHelper.newInput(IOHelper.getResourceURL("ru/gravit/launchserver/defaults/proguard.cfg"))) {
|
||||
IOHelper.transfer(in, out);
|
||||
}
|
||||
}
|
||||
|
||||
public void genWords(boolean force) throws IOException {
|
||||
if (IOHelper.exists(words) && !force) return;
|
||||
Files.deleteIfExists(words);
|
||||
words.toFile().createNewFile();
|
||||
SecureRandom rand = SecurityHelper.newRandom();
|
||||
rand.setSeed(SecureRandom.getSeed(32));
|
||||
try (PrintWriter out = new PrintWriter(new OutputStreamWriter(IOHelper.newOutput(words), IOHelper.UNICODE_CHARSET))) {
|
||||
for (int i = 0; i < Short.MAX_VALUE; i++) out.println(generateString(rand, 24));
|
||||
}
|
||||
}
|
||||
|
||||
public void prepare(boolean force) {
|
||||
try {
|
||||
IOHelper.createParentDirs(config);
|
||||
genWords(force);
|
||||
genConfig(force);
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String readConf() {
|
||||
return "@".concat(config.toFile().getName());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package ru.gravit.launchserver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Collections;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
public class StarterAgent {
|
||||
|
||||
public static final class StarterVisitor extends SimpleFileVisitor<Path> {
|
||||
private Instrumentation inst;
|
||||
|
||||
public StarterVisitor(Instrumentation inst) {
|
||||
this.inst = inst;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (file.toFile().getName().endsWith(".jar")) inst.appendToSystemClassLoaderSearch(new JarFile(file.toFile()));
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
}
|
||||
public static void premain(String agentArgument, Instrumentation inst) {
|
||||
try {
|
||||
Files.walkFileTree(Paths.get("libraries"), Collections.singleton(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new StarterVisitor(inst));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package ru.gravit.launchserver.auth;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
|
||||
public final class AuthException extends IOException {
|
||||
private static final long serialVersionUID = -2586107832847245863L;
|
||||
|
||||
@LauncherAPI
|
||||
public AuthException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getMessage();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package ru.gravit.launchserver.auth;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.NeedGarbageCollection;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
|
||||
public class AuthLimiter implements NeedGarbageCollection {
|
||||
static class AuthEntry {
|
||||
public int value;
|
||||
|
||||
public long ts;
|
||||
|
||||
public AuthEntry(int i, long l) {
|
||||
value = i;
|
||||
ts = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (!(obj instanceof AuthEntry))
|
||||
return false;
|
||||
AuthEntry other = (AuthEntry) obj;
|
||||
if (ts != other.ts)
|
||||
return false;
|
||||
return value == other.value;
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (int) (ts ^ ts >>> 32);
|
||||
result = prime * result + value;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("AuthEntry {value=%s, ts=%s}", value, ts);
|
||||
}
|
||||
}
|
||||
@LauncherAPI
|
||||
public static final long TIMEOUT = 10 * 60 * 1000; //10 минут
|
||||
public final int rateLimit;
|
||||
public final int rateLimitMilis;
|
||||
|
||||
private HashMap<String, AuthEntry> map;
|
||||
|
||||
public AuthLimiter(LaunchServer srv) {
|
||||
map = new HashMap<>();
|
||||
rateLimit = srv.config.authRateLimit;
|
||||
rateLimitMilis = srv.config.authRateLimitMilis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void garbageCollection() {
|
||||
long time = System.currentTimeMillis();
|
||||
long max_timeout = Math.max(rateLimitMilis, TIMEOUT);
|
||||
map.entrySet().removeIf(e -> e.getValue().ts + max_timeout < time);
|
||||
}
|
||||
|
||||
public boolean isLimit(String ip) {
|
||||
if (map.containsKey(ip)) {
|
||||
AuthEntry rate = map.get(ip);
|
||||
long currenttime = System.currentTimeMillis();
|
||||
if (rate.ts + rateLimitMilis < currenttime) rate.value = 0;
|
||||
if (rate.value >= rateLimit && rateLimit > 0) {
|
||||
rate.value++;
|
||||
rate.ts = currenttime;
|
||||
return true;
|
||||
}
|
||||
rate.value++;
|
||||
rate.ts = currenttime;
|
||||
return false;
|
||||
}
|
||||
map.put(ip, new AuthEntry(1, System.currentTimeMillis()));
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package ru.gravit.launchserver.auth;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import com.mysql.cj.jdbc.MysqlDataSource;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.ConfigObject;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.BooleanConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.IntegerConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
|
||||
public final class MySQLSourceConfig extends ConfigObject implements AutoCloseable {
|
||||
@LauncherAPI
|
||||
public static final int TIMEOUT = VerifyHelper.verifyInt(
|
||||
Integer.parseUnsignedInt(System.getProperty("launcher.mysql.idleTimeout", Integer.toString(5000))),
|
||||
VerifyHelper.POSITIVE, "launcher.mysql.idleTimeout can't be <= 5000");
|
||||
private static final int MAX_POOL_SIZE = VerifyHelper.verifyInt(
|
||||
Integer.parseUnsignedInt(System.getProperty("launcher.mysql.maxPoolSize", Integer.toString(3))),
|
||||
VerifyHelper.POSITIVE, "launcher.mysql.maxPoolSize can't be <= 0");
|
||||
|
||||
// Instance
|
||||
private final String poolName;
|
||||
|
||||
// Config
|
||||
private final String address;
|
||||
private final int port;
|
||||
private final boolean useSSL;
|
||||
private final boolean verifyCertificates;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final String database;
|
||||
private String timeZone;
|
||||
|
||||
// Cache
|
||||
private DataSource source;
|
||||
private boolean hikari;
|
||||
|
||||
@LauncherAPI
|
||||
public MySQLSourceConfig(String poolName, BlockConfigEntry block) {
|
||||
super(block);
|
||||
this.poolName = poolName;
|
||||
address = VerifyHelper.verify(block.getEntryValue("address", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "MySQL address can't be empty");
|
||||
port = VerifyHelper.verifyInt(block.getEntryValue("port", IntegerConfigEntry.class),
|
||||
VerifyHelper.range(0, 65535), "Illegal MySQL port");
|
||||
username = VerifyHelper.verify(block.getEntryValue("username", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "MySQL username can't be empty");
|
||||
password = block.getEntryValue("password", StringConfigEntry.class);
|
||||
database = VerifyHelper.verify(block.getEntryValue("database", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "MySQL database can't be empty");
|
||||
timeZone = block.hasEntry("timezone") ? VerifyHelper.verify(block.getEntryValue("timezone", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "MySQL time zone can't be empty") : null;
|
||||
// Password shouldn't be verified
|
||||
useSSL = block.hasEntry("useSSL") ? block.getEntryValue("useSSL", BooleanConfigEntry.class) : true;
|
||||
verifyCertificates = block.hasEntry("verifyCertificates") ? block.getEntryValue("verifyCertificates", BooleanConfigEntry.class) : false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (hikari)
|
||||
((HikariDataSource) source).close();
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public synchronized Connection getConnection() throws SQLException {
|
||||
if (source == null) { // New data source
|
||||
MysqlDataSource mysqlSource = new MysqlDataSource();
|
||||
mysqlSource.setCharacterEncoding("UTF-8");
|
||||
|
||||
// Prep statements cache
|
||||
mysqlSource.setPrepStmtCacheSize(250);
|
||||
mysqlSource.setPrepStmtCacheSqlLimit(2048);
|
||||
mysqlSource.setCachePrepStmts(true);
|
||||
mysqlSource.setUseServerPrepStmts(true);
|
||||
|
||||
// General optimizations
|
||||
mysqlSource.setCacheServerConfiguration(true);
|
||||
mysqlSource.setUseLocalSessionState(true);
|
||||
mysqlSource.setRewriteBatchedStatements(true);
|
||||
mysqlSource.setMaintainTimeStats(false);
|
||||
mysqlSource.setUseUnbufferedInput(false);
|
||||
mysqlSource.setUseReadAheadInput(false);
|
||||
mysqlSource.setUseSSL(useSSL);
|
||||
mysqlSource.setVerifyServerCertificate(verifyCertificates);
|
||||
// Set credentials
|
||||
mysqlSource.setServerName(address);
|
||||
mysqlSource.setPortNumber(port);
|
||||
mysqlSource.setUser(username);
|
||||
mysqlSource.setPassword(password);
|
||||
mysqlSource.setDatabaseName(database);
|
||||
mysqlSource.setTcpNoDelay(true);
|
||||
if (timeZone != null) mysqlSource.setServerTimezone(timeZone);
|
||||
hikari = false;
|
||||
// Try using HikariCP
|
||||
source = mysqlSource;
|
||||
try {
|
||||
Class.forName("com.zaxxer.hikari.HikariDataSource");
|
||||
hikari = true; // Used for shutdown. Not instanceof because of possible classpath error
|
||||
HikariConfig cfg = new HikariConfig();
|
||||
cfg.setUsername(username);
|
||||
cfg.setPassword(password);
|
||||
cfg.setDataSource(mysqlSource);
|
||||
cfg.setPoolName(poolName);
|
||||
cfg.setMinimumIdle(0);
|
||||
cfg.setMaximumPoolSize(MAX_POOL_SIZE);
|
||||
cfg.setIdleTimeout(TIMEOUT * 1000L);
|
||||
// Set HikariCP pool
|
||||
HikariDataSource hikariSource = new HikariDataSource(cfg);
|
||||
// Replace source with hds
|
||||
source = hikariSource;
|
||||
LogHelper.info("HikariCP pooling enabled for '%s'", poolName);
|
||||
return hikariSource.getConnection();
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
LogHelper.warning("HikariCP isn't in classpath for '%s'", poolName);
|
||||
}
|
||||
}
|
||||
return source.getConnection();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package ru.gravit.launchserver.auth.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.ConfigObject;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launchserver.auth.AuthException;
|
||||
import ru.gravit.launchserver.auth.provider.AuthProviderResult;
|
||||
|
||||
public abstract class AuthHandler extends ConfigObject implements AutoCloseable {
|
||||
private static final Map<String, Adapter<AuthHandler>> AUTH_HANDLERS = new ConcurrentHashMap<>(4);
|
||||
private static boolean registredHandl = false;
|
||||
|
||||
@LauncherAPI
|
||||
public static UUID authError(String message) throws AuthException {
|
||||
throw new AuthException(message);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public static AuthHandler newHandler(String name, BlockConfigEntry block) {
|
||||
Adapter<AuthHandler> authHandlerAdapter = VerifyHelper.getMapValue(AUTH_HANDLERS, name,
|
||||
String.format("Unknown auth handler: '%s'", name));
|
||||
return authHandlerAdapter.convert(block);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public static void registerHandler(String name, Adapter<AuthHandler> adapter) {
|
||||
VerifyHelper.verifyIDName(name);
|
||||
VerifyHelper.putIfAbsent(AUTH_HANDLERS, name, Objects.requireNonNull(adapter, "adapter"),
|
||||
String.format("Auth handler has been already registered: '%s'", name));
|
||||
}
|
||||
|
||||
public static void registerHandlers() {
|
||||
if (!registredHandl) {
|
||||
registerHandler("null", NullAuthHandler::new);
|
||||
registerHandler("memory", MemoryAuthHandler::new);
|
||||
|
||||
// Auth handler that doesn't do nothing :D
|
||||
registerHandler("binaryFile", BinaryFileAuthHandler::new);
|
||||
registerHandler("textFile", TextFileAuthHandler::new);
|
||||
registerHandler("mysql", MySQLAuthHandler::new);
|
||||
registredHandl = true;
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected AuthHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public abstract UUID auth(AuthProviderResult authResult) throws IOException;
|
||||
|
||||
@LauncherAPI
|
||||
public abstract UUID checkServer(String username, String serverID) throws IOException;
|
||||
|
||||
@Override
|
||||
public abstract void close() throws IOException;
|
||||
|
||||
@LauncherAPI
|
||||
public abstract boolean joinServer(String username, String accessToken, String serverID) throws IOException;
|
||||
|
||||
@LauncherAPI
|
||||
public abstract UUID usernameToUUID(String username) throws IOException;
|
||||
|
||||
@LauncherAPI
|
||||
public abstract String uuidToUsername(UUID uuid) throws IOException;
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package ru.gravit.launchserver.auth.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
|
||||
public final class BinaryFileAuthHandler extends FileAuthHandler {
|
||||
public BinaryFileAuthHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readAuthFile() throws IOException {
|
||||
try (HInput input = new HInput(IOHelper.newInput(file))) {
|
||||
int count = input.readLength(0);
|
||||
for (int i = 0; i < count; i++) {
|
||||
UUID uuid = input.readUUID();
|
||||
Entry entry = new Entry(input);
|
||||
addAuth(uuid, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeAuthFileTmp() throws IOException {
|
||||
Set<Map.Entry<UUID, Entry>> entrySet = entrySet();
|
||||
try (HOutput output = new HOutput(IOHelper.newOutput(fileTmp))) {
|
||||
output.writeLength(entrySet.size(), 0);
|
||||
for (Map.Entry<UUID, Entry> entry : entrySet) {
|
||||
output.writeUUID(entry.getKey());
|
||||
entry.getValue().write(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package ru.gravit.launchserver.auth.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.CommonHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launchserver.auth.provider.AuthProviderResult;
|
||||
|
||||
public abstract class CachedAuthHandler extends AuthHandler {
|
||||
public static final class Entry {
|
||||
@LauncherAPI
|
||||
public final UUID uuid;
|
||||
private String username;
|
||||
private String accessToken;
|
||||
private String serverID;
|
||||
|
||||
@LauncherAPI
|
||||
public Entry(UUID uuid, String username, String accessToken, String serverID) {
|
||||
this.uuid = Objects.requireNonNull(uuid, "uuid");
|
||||
this.username = Objects.requireNonNull(username, "username");
|
||||
this.accessToken = accessToken == null ? null : SecurityHelper.verifyToken(accessToken);
|
||||
this.serverID = serverID == null ? null : VerifyHelper.verifyServerID(serverID);
|
||||
}
|
||||
}
|
||||
private final Map<UUID, Entry> entryCache = new HashMap<>(1024);
|
||||
|
||||
private final Map<String, UUID> usernamesCache = new HashMap<>(1024);
|
||||
|
||||
@LauncherAPI
|
||||
protected CachedAuthHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected void addEntry(Entry entry) {
|
||||
Entry previous = entryCache.put(entry.uuid, entry);
|
||||
if (previous != null)
|
||||
usernamesCache.remove(CommonHelper.low(previous.username));
|
||||
usernamesCache.put(CommonHelper.low(entry.username), entry.uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final synchronized UUID auth(AuthProviderResult result) throws IOException {
|
||||
Entry entry = getEntry(result.username);
|
||||
if (entry == null || !updateAuth(entry.uuid, entry.username, result.accessToken))
|
||||
return authError(String.format("UUID is null for username '%s'", result.username));
|
||||
|
||||
// Update cached access token (and username case)
|
||||
entry.username = result.username;
|
||||
entry.accessToken = result.accessToken;
|
||||
entry.serverID = null;
|
||||
return entry.uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized UUID checkServer(String username, String serverID) throws IOException {
|
||||
Entry entry = getEntry(username);
|
||||
return entry != null && username.equals(entry.username) &&
|
||||
serverID.equals(entry.serverID) ? entry.uuid : null;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected abstract Entry fetchEntry(String username) throws IOException;
|
||||
|
||||
@LauncherAPI
|
||||
protected abstract Entry fetchEntry(UUID uuid) throws IOException;
|
||||
|
||||
private Entry getEntry(String username) throws IOException {
|
||||
UUID uuid = usernamesCache.get(CommonHelper.low(username));
|
||||
if (uuid != null)
|
||||
return getEntry(uuid);
|
||||
|
||||
// Fetch entry by username
|
||||
Entry entry = fetchEntry(username);
|
||||
if (entry != null)
|
||||
addEntry(entry);
|
||||
|
||||
// Return what we got
|
||||
return entry;
|
||||
}
|
||||
|
||||
private Entry getEntry(UUID uuid) throws IOException {
|
||||
Entry entry = entryCache.get(uuid);
|
||||
if (entry == null) {
|
||||
entry = fetchEntry(uuid);
|
||||
if (entry != null)
|
||||
addEntry(entry);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean joinServer(String username, String accessToken, String serverID) throws IOException {
|
||||
Entry entry = getEntry(username);
|
||||
if (entry == null || !username.equals(entry.username) || !accessToken.equals(entry.accessToken) ||
|
||||
!updateServerID(entry.uuid, serverID))
|
||||
return false; // Account doesn't exist or invalid access token
|
||||
|
||||
// Update cached server ID
|
||||
entry.serverID = serverID;
|
||||
return true;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected abstract boolean updateAuth(UUID uuid, String username, String accessToken) throws IOException;
|
||||
|
||||
@LauncherAPI
|
||||
protected abstract boolean updateServerID(UUID uuid, String serverID) throws IOException;
|
||||
|
||||
@Override
|
||||
public final synchronized UUID usernameToUUID(String username) throws IOException {
|
||||
Entry entry = getEntry(username);
|
||||
return entry == null ? null : entry.uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final synchronized String uuidToUsername(UUID uuid) throws IOException {
|
||||
Entry entry = getEntry(uuid);
|
||||
return entry == null ? null : entry.username;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
package ru.gravit.launchserver.auth.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.CommonHelper;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.profiles.PlayerProfile;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.BooleanConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
import ru.gravit.launcher.serialize.stream.StreamObject;
|
||||
import ru.gravit.launchserver.auth.provider.AuthProviderResult;
|
||||
|
||||
public abstract class FileAuthHandler extends AuthHandler {
|
||||
public static final class Entry extends StreamObject {
|
||||
private String username;
|
||||
private String accessToken;
|
||||
private String serverID;
|
||||
|
||||
@LauncherAPI
|
||||
public Entry(HInput input) throws IOException {
|
||||
username = VerifyHelper.verifyUsername(input.readString(64));
|
||||
if (input.readBoolean()) {
|
||||
accessToken = SecurityHelper.verifyToken(input.readASCII(-SecurityHelper.TOKEN_STRING_LENGTH));
|
||||
if (input.readBoolean())
|
||||
serverID = VerifyHelper.verifyServerID(input.readASCII(41));
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public Entry(String username) {
|
||||
this.username = VerifyHelper.verifyUsername(username);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public Entry(String username, String accessToken, String serverID) {
|
||||
this(username);
|
||||
if (accessToken == null && serverID != null)
|
||||
throw new IllegalArgumentException("Can't set access token while server ID is null");
|
||||
|
||||
// Set and verify access token
|
||||
this.accessToken = accessToken == null ? null : SecurityHelper.verifyToken(accessToken);
|
||||
this.serverID = serverID == null ? null : VerifyHelper.verifyServerID(serverID);
|
||||
}
|
||||
|
||||
private void auth(String username, String accessToken) {
|
||||
this.username = username; // Update username case
|
||||
this.accessToken = accessToken;
|
||||
serverID = null;
|
||||
}
|
||||
|
||||
private boolean checkServer(String username, String serverID) {
|
||||
return username.equals(this.username) && serverID.equals(this.serverID);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public String getServerID() {
|
||||
return serverID;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
private boolean joinServer(String username, String accessToken, String serverID) {
|
||||
if (!username.equals(this.username) || !accessToken.equals(this.accessToken))
|
||||
return false; // Username or access token mismatch
|
||||
|
||||
// Update server ID
|
||||
this.serverID = serverID;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(HOutput output) throws IOException {
|
||||
output.writeString(username, 64);
|
||||
output.writeBoolean(accessToken != null);
|
||||
if (accessToken != null) {
|
||||
output.writeASCII(accessToken, -SecurityHelper.TOKEN_STRING_LENGTH);
|
||||
output.writeBoolean(serverID != null);
|
||||
if (serverID != null)
|
||||
output.writeASCII(serverID, 41);
|
||||
}
|
||||
}
|
||||
}
|
||||
@LauncherAPI
|
||||
public final Path file;
|
||||
@LauncherAPI
|
||||
public final Path fileTmp;
|
||||
|
||||
@LauncherAPI
|
||||
public final boolean offlineUUIDs;
|
||||
// Instance
|
||||
private final SecureRandom random = SecurityHelper.newRandom();
|
||||
|
||||
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
// Storage
|
||||
private final Map<UUID, Entry> entryMap = new HashMap<>(256);
|
||||
|
||||
private final Map<String, UUID> usernamesMap = new HashMap<>(256);
|
||||
|
||||
@LauncherAPI
|
||||
protected FileAuthHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
file = IOHelper.toPath(block.getEntryValue("file", StringConfigEntry.class));
|
||||
fileTmp = IOHelper.toPath(block.getEntryValue("file", StringConfigEntry.class) + ".tmp");
|
||||
offlineUUIDs = block.getEntryValue("offlineUUIDs", BooleanConfigEntry.class);
|
||||
|
||||
// Read auth handler file
|
||||
if (IOHelper.isFile(file)) {
|
||||
LogHelper.info("Reading auth handler file: '%s'", file);
|
||||
try {
|
||||
readAuthFile();
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected final void addAuth(UUID uuid, Entry entry) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
Entry previous = entryMap.put(uuid, entry);
|
||||
if (previous != null)
|
||||
usernamesMap.remove(CommonHelper.low(previous.username));
|
||||
usernamesMap.put(CommonHelper.low(entry.username), uuid);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final UUID auth(AuthProviderResult authResult) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
UUID uuid = usernameToUUID(authResult.username);
|
||||
Entry entry = entryMap.get(uuid);
|
||||
|
||||
// Not registered? Fix it!
|
||||
if (entry == null) {
|
||||
entry = new Entry(authResult.username);
|
||||
|
||||
// Generate UUID
|
||||
uuid = genUUIDFor(authResult.username);
|
||||
entryMap.put(uuid, entry);
|
||||
usernamesMap.put(CommonHelper.low(authResult.username), uuid);
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
entry.auth(authResult.username, authResult.accessToken);
|
||||
return uuid;
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final UUID checkServer(String username, String serverID) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
UUID uuid = usernameToUUID(username);
|
||||
Entry entry = entryMap.get(uuid);
|
||||
|
||||
// Check server (if has such account of course)
|
||||
return entry != null && entry.checkServer(username, serverID) ? uuid : null;
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void close() throws IOException {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
LogHelper.info("Writing auth handler file (%d entries)", entryMap.size());
|
||||
writeAuthFileTmp();
|
||||
IOHelper.move(fileTmp, file);
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected final Set<Map.Entry<UUID, Entry>> entrySet() {
|
||||
return Collections.unmodifiableMap(entryMap).entrySet();
|
||||
}
|
||||
|
||||
private UUID genUUIDFor(String username) {
|
||||
if (offlineUUIDs) {
|
||||
UUID md5UUID = PlayerProfile.offlineUUID(username);
|
||||
if (!entryMap.containsKey(md5UUID))
|
||||
return md5UUID;
|
||||
LogHelper.warning("Offline UUID collision, using random: '%s'", username);
|
||||
}
|
||||
|
||||
// Pick random UUID
|
||||
UUID uuid;
|
||||
do
|
||||
uuid = new UUID(random.nextLong(), random.nextLong());
|
||||
while (entryMap.containsKey(uuid));
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean joinServer(String username, String accessToken, String serverID) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
Entry entry = entryMap.get(usernameToUUID(username));
|
||||
return entry != null && entry.joinServer(username, accessToken, serverID);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected abstract void readAuthFile() throws IOException;
|
||||
|
||||
@Override
|
||||
public final UUID usernameToUUID(String username) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
return usernamesMap.get(CommonHelper.low(username));
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String uuidToUsername(UUID uuid) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
Entry entry = entryMap.get(uuid);
|
||||
return entry == null ? null : entry.username;
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected abstract void writeAuthFileTmp() throws IOException;
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package ru.gravit.launchserver.auth.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.eclipsesource.json.Json;
|
||||
import com.eclipsesource.json.JsonObject;
|
||||
import com.eclipsesource.json.JsonValue;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class JsonAuthHandler extends CachedAuthHandler {
|
||||
|
||||
private static final int TIMEOUT = 10;
|
||||
private final URL url;
|
||||
private final URL urlCheckServer;
|
||||
private final URL urlJoinServer;
|
||||
private final URL urlUsernameToUUID;
|
||||
private final URL urlUUIDToUsername;
|
||||
private final String userKeyName;
|
||||
private final String serverIDKeyName;
|
||||
private final String accessTokenKeyName;
|
||||
private final String uuidKeyName;
|
||||
private final String responseUserKeyName;
|
||||
private final String responseErrorKeyName;
|
||||
|
||||
protected JsonAuthHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
String configUrl = block.getEntryValue("url", StringConfigEntry.class);
|
||||
String configUrlCheckServer = block.getEntryValue("urlCheckServer", StringConfigEntry.class);
|
||||
String configUrlJoinServer = block.getEntryValue("urlJoinServer", StringConfigEntry.class);
|
||||
String configUrlUsernameUUID = block.getEntryValue("urlUsernameToUUID", StringConfigEntry.class);
|
||||
String configUrlUUIDUsername = block.getEntryValue("urlUUIDToUsername", StringConfigEntry.class);
|
||||
userKeyName = VerifyHelper.verify(block.getEntryValue("userKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Username key name can't be empty");
|
||||
serverIDKeyName = VerifyHelper.verify(block.getEntryValue("serverIDKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "ServerID key name can't be empty");
|
||||
uuidKeyName = VerifyHelper.verify(block.getEntryValue("UUIDKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "UUID key name can't be empty");
|
||||
accessTokenKeyName = VerifyHelper.verify(block.getEntryValue("accessTokenKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "AccessToken key name can't be empty");
|
||||
responseUserKeyName = VerifyHelper.verify(block.getEntryValue("responseUserKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Response username key can't be empty");
|
||||
responseErrorKeyName = VerifyHelper.verify(block.getEntryValue("responseErrorKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Response error key can't be empty");
|
||||
url = IOHelper.convertToURL(configUrl);
|
||||
urlCheckServer = IOHelper.convertToURL(configUrlCheckServer);
|
||||
urlJoinServer = IOHelper.convertToURL(configUrlJoinServer);
|
||||
urlUsernameToUUID = IOHelper.convertToURL(configUrlUsernameUUID);
|
||||
urlUUIDToUsername = IOHelper.convertToURL(configUrlUUIDUsername);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID checkServer(String username, String serverID) throws IOException {
|
||||
JsonObject request = Json.object().add(userKeyName, username).add(serverIDKeyName, serverID);
|
||||
JsonObject result = jsonRequest(request, urlCheckServer);
|
||||
String value;
|
||||
if ((value = result.getString(uuidKeyName, null)) != null)
|
||||
return UUID.fromString(value);
|
||||
return super.checkServer(username, serverID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry fetchEntry(String username) throws IOException {
|
||||
JsonObject request = Json.object().add(userKeyName, username);
|
||||
JsonObject result = jsonRequest(request, urlCheckServer);
|
||||
UUID uuid = UUID.fromString(result.getString(uuidKeyName, null));
|
||||
String accessToken = result.getString(accessTokenKeyName, null);
|
||||
String serverID = result.getString(serverIDKeyName, null);
|
||||
if (accessToken == null || serverID == null) return null;
|
||||
|
||||
return new Entry(uuid, username, accessToken, serverID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry fetchEntry(UUID uuid) throws IOException {
|
||||
JsonObject request = Json.object().add(uuidKeyName, uuid.toString());
|
||||
JsonObject result = jsonRequest(request, urlCheckServer);
|
||||
String username = result.getString(userKeyName, null);
|
||||
String accessToken = result.getString(accessTokenKeyName, null);
|
||||
String serverID = result.getString(serverIDKeyName, null);
|
||||
if (username == null || accessToken == null || serverID == null) return null;
|
||||
|
||||
return new Entry(uuid, username, accessToken, serverID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean joinServer(String username, String accessToken, String serverID) throws IOException {
|
||||
JsonObject request = Json.object().add(userKeyName, username).add(serverIDKeyName, serverID).add(accessTokenKeyName, accessToken);
|
||||
jsonRequest(request, urlJoinServer);
|
||||
return super.joinServer(username, accessToken, serverID);
|
||||
}
|
||||
|
||||
public JsonObject jsonRequest(JsonObject request, URL url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
if (TIMEOUT > 0)
|
||||
connection.setConnectTimeout(TIMEOUT);
|
||||
|
||||
OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), Charset.forName("UTF-8"));
|
||||
writer.write(request.toString());
|
||||
writer.flush();
|
||||
writer.close();
|
||||
|
||||
InputStreamReader reader;
|
||||
int statusCode = connection.getResponseCode();
|
||||
|
||||
if (200 <= statusCode && statusCode < 300)
|
||||
reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8);
|
||||
else
|
||||
reader = new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8);
|
||||
JsonValue content = Json.parse(reader);
|
||||
if (!content.isObject())
|
||||
authError("Authentication server response is malformed");
|
||||
|
||||
JsonObject response = content.asObject();
|
||||
String value;
|
||||
|
||||
if ((value = response.getString(responseErrorKeyName, null)) != null)
|
||||
authError(value);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean updateAuth(UUID uuid, String username, String accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean updateServerID(UUID uuid, String serverID) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package ru.gravit.launchserver.auth.handler;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
|
||||
public final class MemoryAuthHandler extends CachedAuthHandler {
|
||||
private static String toUsername(UUID uuid) {
|
||||
byte[] bytes = ByteBuffer.allocate(16).
|
||||
putLong(uuid.getMostSignificantBits()).
|
||||
putLong(uuid.getLeastSignificantBits()).array();
|
||||
|
||||
// Find username end
|
||||
int length = 0;
|
||||
while (length < bytes.length && bytes[length] != 0)
|
||||
length++;
|
||||
|
||||
// Decode and verify
|
||||
return VerifyHelper.verifyUsername(new String(bytes, 0, length, IOHelper.ASCII_CHARSET));
|
||||
}
|
||||
|
||||
private static UUID toUUID(String username) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(Arrays.copyOf(IOHelper.encodeASCII(username), 16));
|
||||
return new UUID(buffer.getLong(), buffer.getLong()); // MOST, LEAST
|
||||
}
|
||||
|
||||
public MemoryAuthHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
LogHelper.warning("Usage of MemoryAuthHandler isn't recommended!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry fetchEntry(String username) {
|
||||
return new Entry(toUUID(username), username, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry fetchEntry(UUID uuid) {
|
||||
return new Entry(uuid, toUsername(uuid), null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean updateAuth(UUID uuid, String username, String accessToken) {
|
||||
return true; // Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean updateServerID(UUID uuid, String serverID) {
|
||||
return true; // Do nothing
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package ru.gravit.launchserver.auth.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
import ru.gravit.launchserver.auth.MySQLSourceConfig;
|
||||
|
||||
public final class MySQLAuthHandler extends CachedAuthHandler {
|
||||
private final MySQLSourceConfig mySQLHolder;
|
||||
private final String uuidColumn;
|
||||
private final String usernameColumn;
|
||||
private final String accessTokenColumn;
|
||||
private final String serverIDColumn;
|
||||
|
||||
// Prepared SQL queries
|
||||
private final String queryByUUIDSQL;
|
||||
private final String queryByUsernameSQL;
|
||||
private final String updateAuthSQL;
|
||||
private final String updateServerIDSQL;
|
||||
|
||||
public MySQLAuthHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
mySQLHolder = new MySQLSourceConfig("authHandlerPool", block);
|
||||
|
||||
// Read query params
|
||||
String table = VerifyHelper.verifyIDName(
|
||||
block.getEntryValue("table", StringConfigEntry.class));
|
||||
uuidColumn = VerifyHelper.verifyIDName(
|
||||
block.getEntryValue("uuidColumn", StringConfigEntry.class));
|
||||
usernameColumn = VerifyHelper.verifyIDName(
|
||||
block.getEntryValue("usernameColumn", StringConfigEntry.class));
|
||||
accessTokenColumn = VerifyHelper.verifyIDName(
|
||||
block.getEntryValue("accessTokenColumn", StringConfigEntry.class));
|
||||
serverIDColumn = VerifyHelper.verifyIDName(
|
||||
block.getEntryValue("serverIDColumn", StringConfigEntry.class));
|
||||
|
||||
// Prepare SQL queries
|
||||
queryByUUIDSQL = String.format("SELECT %s, %s, %s, %s FROM %s WHERE %s=? LIMIT 1",
|
||||
uuidColumn, usernameColumn, accessTokenColumn, serverIDColumn, table, uuidColumn);
|
||||
queryByUsernameSQL = String.format("SELECT %s, %s, %s, %s FROM %s WHERE %s=? LIMIT 1",
|
||||
uuidColumn, usernameColumn, accessTokenColumn, serverIDColumn, table, usernameColumn);
|
||||
updateAuthSQL = String.format("UPDATE %s SET %s=?, %s=?, %s=NULL WHERE %s=? LIMIT 1",
|
||||
table, usernameColumn, accessTokenColumn, serverIDColumn, uuidColumn);
|
||||
updateServerIDSQL = String.format("UPDATE %s SET %s=? WHERE %s=? LIMIT 1",
|
||||
table, serverIDColumn, uuidColumn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mySQLHolder.close();
|
||||
}
|
||||
|
||||
private Entry constructEntry(ResultSet set) throws SQLException {
|
||||
return set.next() ? new Entry(UUID.fromString(set.getString(uuidColumn)), set.getString(usernameColumn),
|
||||
set.getString(accessTokenColumn), set.getString(serverIDColumn)) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry fetchEntry(String username) throws IOException {
|
||||
return query(queryByUsernameSQL, username);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry fetchEntry(UUID uuid) throws IOException {
|
||||
return query(queryByUUIDSQL, uuid.toString());
|
||||
}
|
||||
|
||||
private Entry query(String sql, String value) throws IOException {
|
||||
try {
|
||||
Connection c = mySQLHolder.getConnection();
|
||||
PreparedStatement s = c.prepareStatement(sql);
|
||||
s.setString(1, value);
|
||||
|
||||
// Execute query
|
||||
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||
try (ResultSet set = s.executeQuery()) {
|
||||
return constructEntry(set);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean updateAuth(UUID uuid, String username, String accessToken) throws IOException {
|
||||
try {
|
||||
Connection c = mySQLHolder.getConnection();
|
||||
PreparedStatement s = c.prepareStatement(updateAuthSQL);
|
||||
s.setString(1, username); // Username case
|
||||
s.setString(2, accessToken);
|
||||
s.setString(3, uuid.toString());
|
||||
|
||||
// Execute update
|
||||
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||
return s.executeUpdate() > 0;
|
||||
} catch (SQLException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean updateServerID(UUID uuid, String serverID) throws IOException {
|
||||
try {
|
||||
Connection c = mySQLHolder.getConnection();
|
||||
PreparedStatement s = c.prepareStatement(updateServerIDSQL);
|
||||
s.setString(1, serverID);
|
||||
s.setString(2, uuid.toString());
|
||||
|
||||
// Execute update
|
||||
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||
return s.executeUpdate() > 0;
|
||||
} catch (SQLException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package ru.gravit.launchserver.auth.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launchserver.auth.provider.AuthProviderResult;
|
||||
|
||||
public final class NullAuthHandler extends AuthHandler {
|
||||
private volatile AuthHandler handler;
|
||||
|
||||
public NullAuthHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID auth(AuthProviderResult authResult) throws IOException {
|
||||
return getHandler().auth(authResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID checkServer(String username, String serverID) throws IOException {
|
||||
return getHandler().checkServer(username, serverID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
AuthHandler handler = this.handler;
|
||||
if (handler != null)
|
||||
handler.close();
|
||||
}
|
||||
|
||||
private AuthHandler getHandler() {
|
||||
return VerifyHelper.verify(handler, Objects::nonNull, "Backend auth handler wasn't set");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean joinServer(String username, String accessToken, String serverID) throws IOException {
|
||||
return getHandler().joinServer(username, accessToken, serverID);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void setBackend(AuthHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID usernameToUUID(String username) throws IOException {
|
||||
return getHandler().usernameToUUID(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uuidToUsername(UUID uuid) throws IOException {
|
||||
return getHandler().uuidToUsername(uuid);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package ru.gravit.launchserver.auth.handler;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.TextConfigReader;
|
||||
import ru.gravit.launcher.serialize.config.TextConfigWriter;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.ConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.ConfigEntry.Type;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
|
||||
public final class TextFileAuthHandler extends FileAuthHandler {
|
||||
private static StringConfigEntry cc(String value) {
|
||||
StringConfigEntry entry = new StringConfigEntry(value, true, 4);
|
||||
entry.setComment(0, "\n\t"); // Pre-name
|
||||
entry.setComment(2, " "); // Pre-value
|
||||
return entry;
|
||||
}
|
||||
|
||||
public TextFileAuthHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readAuthFile() throws IOException {
|
||||
BlockConfigEntry authFile;
|
||||
try (BufferedReader reader = IOHelper.newReader(file)) {
|
||||
authFile = TextConfigReader.read(reader, false);
|
||||
}
|
||||
|
||||
// Read auths from config block
|
||||
Set<Map.Entry<String, ConfigEntry<?>>> entrySet = authFile.getValue().entrySet();
|
||||
for (Map.Entry<String, ConfigEntry<?>> entry : entrySet) {
|
||||
UUID uuid = UUID.fromString(entry.getKey());
|
||||
ConfigEntry<?> value = VerifyHelper.verify(entry.getValue(),
|
||||
v -> v.getType() == Type.BLOCK, "Illegal config entry type: " + uuid);
|
||||
|
||||
// Get auth entry data
|
||||
BlockConfigEntry authBlock = (BlockConfigEntry) value;
|
||||
String username = authBlock.getEntryValue("username", StringConfigEntry.class);
|
||||
String accessToken = authBlock.hasEntry("accessToken") ?
|
||||
authBlock.getEntryValue("accessToken", StringConfigEntry.class) : null;
|
||||
String serverID = authBlock.hasEntry("serverID") ?
|
||||
authBlock.getEntryValue("serverID", StringConfigEntry.class) : null;
|
||||
|
||||
// Add auth entry
|
||||
addAuth(uuid, new Entry(username, accessToken, serverID));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeAuthFileTmp() throws IOException {
|
||||
boolean next = false;
|
||||
|
||||
// Write auth blocks to map
|
||||
Set<Map.Entry<UUID, Entry>> entrySet = entrySet();
|
||||
Map<String, ConfigEntry<?>> map = new LinkedHashMap<>(entrySet.size());
|
||||
for (Map.Entry<UUID, Entry> entry : entrySet) {
|
||||
UUID uuid = entry.getKey();
|
||||
Entry auth = entry.getValue();
|
||||
|
||||
// Set auth entry data
|
||||
Map<String, ConfigEntry<?>> authMap = new LinkedHashMap<>(entrySet.size());
|
||||
authMap.put("username", cc(auth.getUsername()));
|
||||
String accessToken = auth.getAccessToken();
|
||||
if (accessToken != null)
|
||||
authMap.put("accessToken", cc(accessToken));
|
||||
String serverID = auth.getServerID();
|
||||
if (serverID != null)
|
||||
authMap.put("serverID", cc(serverID));
|
||||
|
||||
// Create and add auth block
|
||||
BlockConfigEntry authBlock = new BlockConfigEntry(authMap, true, 5);
|
||||
if (next)
|
||||
authBlock.setComment(0, "\n"); // Pre-name
|
||||
else
|
||||
next = true;
|
||||
authBlock.setComment(2, " "); // Pre-value
|
||||
authBlock.setComment(4, "\n"); // Post-comment
|
||||
map.put(uuid.toString(), authBlock);
|
||||
}
|
||||
|
||||
// Write auth handler file
|
||||
try (BufferedWriter writer = IOHelper.newWriter(fileTmp)) {
|
||||
BlockConfigEntry authFile = new BlockConfigEntry(map, true, 1);
|
||||
authFile.setComment(0, "\n");
|
||||
TextConfigWriter.write(authFile, writer, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package ru.gravit.launchserver.auth.hwid;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
|
||||
public class AcceptHWIDHandler extends HWIDHandler {
|
||||
|
||||
public AcceptHWIDHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ban(List<HWID> hwid) {
|
||||
//SKIP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check0(HWID hwid, String username) {
|
||||
//SKIP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
//SKIP
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HWID> getHwid(String username) {
|
||||
return Arrays.asList(nullHWID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unban(List<HWID> hwid) {
|
||||
//SKIP
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package ru.gravit.launchserver.auth.hwid;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
|
||||
public class HWID {
|
||||
public static HWID fromData(HInput in) throws IOException {
|
||||
return gen(in.readLong(), in.readLong(), in.readLong());
|
||||
}
|
||||
public static HWID gen(long hwid_hdd, long hwid_bios, long hwid_cpu) {
|
||||
return new HWID(hwid_hdd, hwid_bios, hwid_cpu);
|
||||
}
|
||||
private long hwid_bios;
|
||||
|
||||
private long hwid_hdd;
|
||||
|
||||
private long hwid_cpu;
|
||||
|
||||
private HWID(long hwid_hdd, long hwid_bios, long hwid_cpu) {
|
||||
this.hwid_hdd = hwid_hdd;
|
||||
this.hwid_bios = hwid_bios;
|
||||
this.hwid_cpu = hwid_cpu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (!(obj instanceof HWID))
|
||||
return false;
|
||||
HWID other = (HWID) obj;
|
||||
if (hwid_bios != other.hwid_bios)
|
||||
return false;
|
||||
if (hwid_cpu != other.hwid_cpu)
|
||||
return false;
|
||||
return hwid_hdd == other.hwid_hdd;
|
||||
}
|
||||
|
||||
public long getHwid_bios() {
|
||||
return hwid_bios;
|
||||
}
|
||||
|
||||
public long getHwid_cpu() {
|
||||
return hwid_cpu;
|
||||
}
|
||||
|
||||
public long getHwid_hdd() {
|
||||
return hwid_hdd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (int) (hwid_bios ^ hwid_bios >>> 32);
|
||||
result = prime * result + (int) (hwid_cpu ^ hwid_cpu >>> 32);
|
||||
result = prime * result + (int) (hwid_hdd ^ hwid_hdd >>> 32);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void toData(HOutput out) throws IOException {
|
||||
out.writeLong(hwid_hdd);
|
||||
out.writeLong(hwid_bios);
|
||||
out.writeLong(hwid_cpu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("HWID {hwid_bios=%s, hwid_hdd=%s, hwid_cpu=%s}", hwid_bios, hwid_hdd, hwid_cpu);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package ru.gravit.launchserver.auth.hwid;
|
||||
|
||||
public class HWIDException extends Exception {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -5307315891121889972L;
|
||||
|
||||
public HWIDException() {
|
||||
}
|
||||
|
||||
public HWIDException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public HWIDException(String s, Throwable throwable) {
|
||||
super(s, throwable);
|
||||
}
|
||||
|
||||
public HWIDException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package ru.gravit.launchserver.auth.hwid;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.ConfigObject;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
|
||||
public abstract class HWIDHandler extends ConfigObject implements AutoCloseable {
|
||||
private static final Map<String, Adapter<HWIDHandler>> HW_HANDLERS = new ConcurrentHashMap<>(4);
|
||||
public static final HWID nullHWID = HWID.gen(0, 0, 0);
|
||||
private static boolean registredHandl = false;
|
||||
|
||||
@LauncherAPI
|
||||
public static HWIDHandler newHandler(String name, BlockConfigEntry block) {
|
||||
Adapter<HWIDHandler> authHandlerAdapter = VerifyHelper.getMapValue(HW_HANDLERS, name,
|
||||
String.format("Unknown HWID handler: '%s'", name));
|
||||
return authHandlerAdapter.convert(block);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public static void registerHandler(String name, Adapter<HWIDHandler> adapter) {
|
||||
VerifyHelper.verifyIDName(name);
|
||||
VerifyHelper.putIfAbsent(HW_HANDLERS, name, Objects.requireNonNull(adapter, "adapter"),
|
||||
String.format("HWID handler has been already registered: '%s'", name));
|
||||
}
|
||||
public static void registerHandlers() {
|
||||
if (!registredHandl) {
|
||||
registerHandler("accept", AcceptHWIDHandler::new);
|
||||
registerHandler("mysql",MysqlHWIDHandler::new);
|
||||
registerHandler("json",JsonHWIDHandler::new);
|
||||
registredHandl = true;
|
||||
}
|
||||
}
|
||||
protected HWIDHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
public abstract void ban(List<HWID> hwid) throws HWIDException;
|
||||
|
||||
public void check(HWID hwid, String username) throws HWIDException {
|
||||
if (nullHWID.equals(hwid)) return;
|
||||
check0(hwid, username);
|
||||
}
|
||||
|
||||
public abstract void check0(HWID hwid, String username) throws HWIDException;
|
||||
|
||||
@Override
|
||||
public abstract void close() throws IOException;
|
||||
|
||||
public abstract List<HWID> getHwid(String username) throws HWIDException;
|
||||
|
||||
public abstract void unban(List<HWID> hwid) throws HWIDException;
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package ru.gravit.launchserver.auth.hwid;
|
||||
|
||||
import com.eclipsesource.json.Json;
|
||||
import com.eclipsesource.json.JsonArray;
|
||||
import com.eclipsesource.json.JsonObject;
|
||||
import com.eclipsesource.json.JsonValue;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class JsonHWIDHandler extends HWIDHandler {
|
||||
private static final int TIMEOUT = Integer.parseInt(
|
||||
System.getProperty("launcher.connection.timeout", Integer.toString(1500)));
|
||||
|
||||
private final URL url;
|
||||
private final URL urlBan;
|
||||
private final URL urlUnBan;
|
||||
private final URL urlGet;
|
||||
private final String loginKeyName;
|
||||
private final String hddKeyName;
|
||||
private final String cpuKeyName;
|
||||
private final String biosKeyName;
|
||||
private final String isBannedKeyName;
|
||||
|
||||
JsonHWIDHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
String configUrl = block.getEntryValue("url", StringConfigEntry.class);
|
||||
String configUrlBan = block.getEntryValue("urlBan", StringConfigEntry.class);
|
||||
String configUrlUnBan = block.getEntryValue("urlUnBan", StringConfigEntry.class);
|
||||
String configUrlGet = block.getEntryValue("urlGet", StringConfigEntry.class);
|
||||
loginKeyName = VerifyHelper.verify(block.getEntryValue("loginKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Login key name can't be empty");
|
||||
hddKeyName = VerifyHelper.verify(block.getEntryValue("hddKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "HDD key name can't be empty");
|
||||
cpuKeyName = VerifyHelper.verify(block.getEntryValue("cpuKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "CPU key can't be empty");
|
||||
biosKeyName = VerifyHelper.verify(block.getEntryValue("biosKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Bios key can't be empty");
|
||||
isBannedKeyName = VerifyHelper.verify(block.getEntryValue("isBannedKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Response username key can't be empty");
|
||||
url = IOHelper.convertToURL(configUrl);
|
||||
urlBan = IOHelper.convertToURL(configUrlBan);
|
||||
urlUnBan = IOHelper.convertToURL(configUrlUnBan);
|
||||
urlGet = IOHelper.convertToURL(configUrlGet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ban(List<HWID> l_hwid) throws HWIDException {
|
||||
for(HWID hwid : l_hwid) {
|
||||
JsonObject request = Json.object().add(hddKeyName, hwid.getHwid_hdd()).add(cpuKeyName, hwid.getHwid_cpu()).add(biosKeyName, hwid.getHwid_bios());
|
||||
try {
|
||||
request(request,urlBan);
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
throw new HWIDException("HWID service error");
|
||||
}
|
||||
}
|
||||
}
|
||||
public JsonObject request(JsonObject request, URL url) throws HWIDException, IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
if (TIMEOUT > 0)
|
||||
connection.setConnectTimeout(TIMEOUT);
|
||||
|
||||
OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), Charset.forName("UTF-8"));
|
||||
writer.write(request.toString());
|
||||
writer.flush();
|
||||
writer.close();
|
||||
|
||||
InputStreamReader reader;
|
||||
int statusCode = connection.getResponseCode();
|
||||
|
||||
if (200 <= statusCode && statusCode < 300)
|
||||
reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8);
|
||||
else
|
||||
reader = new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8);
|
||||
JsonValue content = Json.parse(reader);
|
||||
if (!content.isObject())
|
||||
throw new HWIDException("HWID server response is malformed");
|
||||
|
||||
JsonObject response = content.asObject();
|
||||
return response;
|
||||
}
|
||||
@Override
|
||||
public void check0(HWID hwid, String username) throws HWIDException {
|
||||
JsonObject request = Json.object().add(loginKeyName,username).add(hddKeyName,hwid.getHwid_hdd()).add(cpuKeyName,hwid.getHwid_cpu()).add(biosKeyName,hwid.getHwid_bios());
|
||||
JsonObject response = null;
|
||||
try {
|
||||
response = request(request,url);
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
throw new HWIDException("HWID service error");
|
||||
}
|
||||
boolean isBanned = response.getBoolean(isBannedKeyName,false);
|
||||
if(isBanned) throw new HWIDException("You will BANNED!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// pass
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HWID> getHwid(String username) throws HWIDException {
|
||||
JsonObject request = Json.object().add(loginKeyName,username);
|
||||
JsonObject responce;
|
||||
try {
|
||||
responce = request(request,urlGet);
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
throw new HWIDException("HWID service error");
|
||||
}
|
||||
JsonArray array = responce.get("hwids").asArray();
|
||||
ArrayList<HWID> hwids = new ArrayList<>();
|
||||
for(JsonValue i : array)
|
||||
{
|
||||
long hdd=0,cpu=0,bios=0;
|
||||
JsonObject object = i.asObject();
|
||||
hdd = object.getLong(hddKeyName,0);
|
||||
cpu = object.getLong(cpuKeyName,0);
|
||||
bios = object.getLong(biosKeyName,0);
|
||||
HWID hwid = HWID.gen(hdd,cpu,bios);
|
||||
hwids.add(hwid);
|
||||
}
|
||||
return hwids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unban(List<HWID> l_hwid) throws HWIDException {
|
||||
for(HWID hwid : l_hwid) {
|
||||
JsonObject request = Json.object().add(hddKeyName, hwid.getHwid_hdd()).add(cpuKeyName, hwid.getHwid_cpu()).add(biosKeyName, hwid.getHwid_bios());
|
||||
try {
|
||||
request(request,urlUnBan);
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
throw new HWIDException("HWID service error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
package ru.gravit.launchserver.auth.hwid;
|
||||
|
||||
import ru.gravit.launcher.helper.CommonHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.ListConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
import ru.gravit.launchserver.auth.MySQLSourceConfig;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
|
||||
public class MysqlHWIDHandler extends HWIDHandler {
|
||||
private final MySQLSourceConfig mySQLHolder;
|
||||
private final String query;
|
||||
private final String banMessage;
|
||||
private final String isBannedName;
|
||||
private final String loginName;
|
||||
private final String hddName,cpuName,biosName;
|
||||
private final String[] queryParams;
|
||||
private final String queryUpd;
|
||||
private final String[] queryParamsUpd;
|
||||
private final String queryBan;
|
||||
private final String[] queryParamsBan;
|
||||
private final String querySelect;
|
||||
private final String[] queryParamsSelect;
|
||||
|
||||
public MysqlHWIDHandler(BlockConfigEntry block) {
|
||||
super(block);
|
||||
mySQLHolder = new MySQLSourceConfig("hwidHandlerPool", block);
|
||||
|
||||
// Read query
|
||||
query = VerifyHelper.verify(block.getEntryValue("query", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "MySQL query can't be empty");
|
||||
queryParams = block.getEntry("queryParams", ListConfigEntry.class).
|
||||
stream(StringConfigEntry.class).toArray(String[]::new);
|
||||
isBannedName = VerifyHelper.verify(block.getEntryValue("isBannedName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "isBannedName can't be empty");
|
||||
loginName = VerifyHelper.verify(block.getEntryValue("loginName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "loginName can't be empty");
|
||||
banMessage = block.hasEntry("banMessage") ? block.getEntryValue("banMessage", StringConfigEntry.class) : "You HWID Banned";
|
||||
hddName = VerifyHelper.verify(block.getEntryValue("hddName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "hddName can't be empty");
|
||||
cpuName = VerifyHelper.verify(block.getEntryValue("cpuName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "cpuName can't be empty");
|
||||
biosName = VerifyHelper.verify(block.getEntryValue("biosName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "biosName can't be empty");
|
||||
|
||||
queryUpd = VerifyHelper.verify(block.getEntryValue("queryUpd", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "MySQL queryUpd can't be empty");
|
||||
queryParamsUpd = block.getEntry("queryParamsUpd", ListConfigEntry.class).
|
||||
stream(StringConfigEntry.class).toArray(String[]::new);
|
||||
queryBan = VerifyHelper.verify(block.getEntryValue("queryBan", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "MySQL queryBan can't be empty");
|
||||
queryParamsBan = block.getEntry("queryParamsBan", ListConfigEntry.class).
|
||||
stream(StringConfigEntry.class).toArray(String[]::new);
|
||||
querySelect = VerifyHelper.verify(block.getEntryValue("querySelect", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "MySQL queryUpd can't be empty");
|
||||
queryParamsSelect = block.getEntry("queryParamsSelect", ListConfigEntry.class).
|
||||
stream(StringConfigEntry.class).toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check0(HWID hwid, String username) throws HWIDException {
|
||||
try {
|
||||
Connection c = mySQLHolder.getConnection();
|
||||
|
||||
PreparedStatement s = c.prepareStatement(query);
|
||||
String[] replaceParams = {"hwid_hdd", String.valueOf(hwid.getHwid_hdd()), "hwid_cpu", String.valueOf(hwid.getHwid_cpu()), "hwid_bios", String.valueOf(hwid.getHwid_bios()),"login",username};
|
||||
for (int i = 0; i < queryParams.length; i++) {
|
||||
s.setString(i + 1, CommonHelper.replace(queryParams[i], replaceParams));
|
||||
}
|
||||
|
||||
// Execute SQL query
|
||||
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||
try (ResultSet set = s.executeQuery()) {
|
||||
boolean isOne = false;
|
||||
boolean needWrite = true;
|
||||
while(set.next()) {
|
||||
isOne = true;
|
||||
boolean isBanned = set.getBoolean(isBannedName);
|
||||
if (isBanned) throw new HWIDException(banMessage);
|
||||
String login = set.getString(loginName);
|
||||
if (username.equals(login)) {
|
||||
needWrite = false;
|
||||
}
|
||||
}
|
||||
if (!isOne) {
|
||||
writeHWID(hwid, username, c);
|
||||
return;
|
||||
}
|
||||
if(needWrite)
|
||||
{
|
||||
writeHWID(hwid, username, c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
public void writeHWID(HWID hwid, String username, Connection c)
|
||||
{
|
||||
LogHelper.debug("Write HWID %s from username %s",hwid.toString(),username);
|
||||
try (PreparedStatement a = c.prepareStatement(queryUpd)) {
|
||||
//IF
|
||||
String[] replaceParamsUpd = {"hwid_hdd", String.valueOf(hwid.getHwid_hdd()), "hwid_cpu", String.valueOf(hwid.getHwid_cpu()), "hwid_bios", String.valueOf(hwid.getHwid_bios()), "login", username};
|
||||
for (int i = 0; i < queryParamsUpd.length; i++) {
|
||||
a.setString(i + 1, CommonHelper.replace(queryParamsUpd[i], replaceParamsUpd));
|
||||
}
|
||||
a.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||
a.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
public void setIsBanned(HWID hwid,boolean isBanned)
|
||||
{
|
||||
LogHelper.debug("%s Request HWID: %s",isBanned ? "Ban" : "UnBan",hwid.toString());
|
||||
Connection c = null;
|
||||
try {
|
||||
c = mySQLHolder.getConnection();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try (PreparedStatement a = c.prepareStatement(queryBan)) {
|
||||
//IF
|
||||
String[] replaceParamsUpd = {"hwid_hdd", String.valueOf(hwid.getHwid_hdd()), "hwid_cpu", String.valueOf(hwid.getHwid_cpu()), "hwid_bios", String.valueOf(hwid.getHwid_bios()), "isBanned", isBanned ? "1" : "0"};
|
||||
for (int i = 0; i < queryParamsBan.length; i++) {
|
||||
a.setString(i + 1, CommonHelper.replace(queryParamsBan[i], replaceParamsUpd));
|
||||
}
|
||||
a.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||
a.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void ban(List<HWID> list) throws HWIDException {
|
||||
|
||||
for(HWID hwid : list)
|
||||
{
|
||||
setIsBanned(hwid,true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unban(List<HWID> list) throws HWIDException {
|
||||
for(HWID hwid : list)
|
||||
{
|
||||
setIsBanned(hwid,false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HWID> getHwid(String username) throws HWIDException {
|
||||
try {
|
||||
LogHelper.debug("Try find HWID from username %s",username);
|
||||
Connection c = mySQLHolder.getConnection();
|
||||
PreparedStatement s = c.prepareStatement(querySelect);
|
||||
String[] replaceParams = {"login", username};
|
||||
for (int i = 0; i < queryParamsSelect.length; i++) {
|
||||
s.setString(i + 1, CommonHelper.replace(queryParamsSelect[i], replaceParams));
|
||||
}
|
||||
long hdd,cpu,bios;
|
||||
try (ResultSet set = s.executeQuery()) {
|
||||
if(!set.next()) {
|
||||
LogHelper.error(new HWIDException("HWID not found"));
|
||||
return new ArrayList<HWID>();
|
||||
}
|
||||
hdd = set.getLong(hddName);
|
||||
cpu = set.getLong(cpuName);
|
||||
bios = set.getLong(biosName);
|
||||
}
|
||||
ArrayList<HWID> list = new ArrayList<>();
|
||||
HWID hwid = HWID.gen(hdd,bios,cpu);
|
||||
if(hdd == 0 && cpu == 0 && bios == 0) {LogHelper.warning("Null HWID");}
|
||||
else {
|
||||
list.add(hwid);
|
||||
LogHelper.debug("Username: %s HWID: %s",username,hwid.toString());
|
||||
}
|
||||
return list;
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
|
||||
public final class AcceptAuthProvider extends AuthProvider {
|
||||
public AcceptAuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthProviderResult auth(String login, String password, String ip) {
|
||||
return new AuthProviderResult(login, SecurityHelper.randomStringToken()); // Same as login
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.ConfigObject;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launchserver.auth.AuthException;
|
||||
|
||||
public abstract class AuthProvider extends ConfigObject implements AutoCloseable {
|
||||
private static final Map<String, Adapter<AuthProvider>> AUTH_PROVIDERS = new ConcurrentHashMap<>(8);
|
||||
private static boolean registredProv = false;
|
||||
|
||||
@LauncherAPI
|
||||
public static AuthProviderResult authError(String message) throws AuthException {
|
||||
throw new AuthException(message);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public static AuthProvider newProvider(String name, BlockConfigEntry block) {
|
||||
VerifyHelper.verifyIDName(name);
|
||||
Adapter<AuthProvider> authHandlerAdapter = VerifyHelper.getMapValue(AUTH_PROVIDERS, name,
|
||||
String.format("Unknown auth provider: '%s'", name));
|
||||
return authHandlerAdapter.convert(block);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public static void registerProvider(String name, Adapter<AuthProvider> adapter) {
|
||||
VerifyHelper.putIfAbsent(AUTH_PROVIDERS, name, Objects.requireNonNull(adapter, "adapter"),
|
||||
String.format("Auth provider has been already registered: '%s'", name));
|
||||
}
|
||||
|
||||
public static void registerProviders() {
|
||||
if (!registredProv) {
|
||||
registerProvider("null", NullAuthProvider::new);
|
||||
registerProvider("accept", AcceptAuthProvider::new);
|
||||
registerProvider("reject", RejectAuthProvider::new);
|
||||
|
||||
// Auth providers that doesn't do nothing :D
|
||||
registerProvider("mojang", MojangAuthProvider::new);
|
||||
registerProvider("mysql", MySQLAuthProvider::new);
|
||||
registerProvider("file", FileAuthProvider::new);
|
||||
registerProvider("request", RequestAuthProvider::new);
|
||||
registerProvider("json", JsonAuthProvider::new);
|
||||
registredProv = true;
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected AuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public abstract AuthProviderResult auth(String login, String password, String ip) throws Exception;
|
||||
|
||||
@Override
|
||||
public abstract void close() throws IOException;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
public class AuthProviderResult {
|
||||
public final String username;
|
||||
public final String accessToken;
|
||||
|
||||
public AuthProviderResult(String username, String accessToken) {
|
||||
this.username = username;
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper.DigestAlgorithm;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
import ru.gravit.launchserver.auth.AuthException;
|
||||
|
||||
public abstract class DigestAuthProvider extends AuthProvider {
|
||||
private final DigestAlgorithm digest;
|
||||
|
||||
@LauncherAPI
|
||||
protected DigestAuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
digest = DigestAlgorithm.byName(block.getEntryValue("digest", StringConfigEntry.class));
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected final void verifyDigest(String validDigest, String password) throws AuthException {
|
||||
boolean valid;
|
||||
if (digest == DigestAlgorithm.PLAIN)
|
||||
valid = password.equals(validDigest);
|
||||
else if (validDigest == null)
|
||||
valid = false;
|
||||
else {
|
||||
byte[] actualDigest = SecurityHelper.digest(digest, password);
|
||||
valid = SecurityHelper.toHex(actualDigest).equals(validDigest);
|
||||
}
|
||||
|
||||
// Verify is valid
|
||||
if (!valid)
|
||||
authError("Incorrect username or password");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import ru.gravit.launcher.helper.CommonHelper;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.ConfigObject;
|
||||
import ru.gravit.launcher.serialize.config.TextConfigReader;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.ConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.ConfigEntry.Type;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
|
||||
public final class FileAuthProvider extends DigestAuthProvider {
|
||||
private static final class Entry extends ConfigObject {
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final String ip;
|
||||
|
||||
private Entry(BlockConfigEntry block) {
|
||||
super(block);
|
||||
username = VerifyHelper.verifyUsername(block.getEntryValue("username", StringConfigEntry.class));
|
||||
password = VerifyHelper.verify(block.getEntryValue("password", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, String.format("Password can't be empty: '%s'", username));
|
||||
ip = block.hasEntry("ip") ? VerifyHelper.verify(block.getEntryValue("ip", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, String.format("IP can't be empty: '%s'", username)) : null;
|
||||
}
|
||||
}
|
||||
|
||||
private final Path file;
|
||||
// Cache
|
||||
private final Map<String, Entry> entries = new HashMap<>(256);
|
||||
private final Object cacheLock = new Object();
|
||||
|
||||
private FileTime cacheLastModified;
|
||||
|
||||
public FileAuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
file = IOHelper.toPath(block.getEntryValue("file", StringConfigEntry.class));
|
||||
|
||||
// Try to update cache
|
||||
try {
|
||||
updateCache();
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthProviderResult auth(String login, String password, String ip) throws IOException {
|
||||
Entry entry;
|
||||
synchronized (cacheLock) {
|
||||
updateCache();
|
||||
entry = entries.get(CommonHelper.low(login));
|
||||
}
|
||||
|
||||
// Verify digest and return true username
|
||||
verifyDigest(entry == null ? null : entry.password, password);
|
||||
if (entry == null || entry.ip != null && !entry.ip.equals(ip))
|
||||
authError("Authentication from this IP is not allowed");
|
||||
|
||||
// We're done
|
||||
return new AuthProviderResult(entry.username, SecurityHelper.randomStringToken());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
private void updateCache() throws IOException {
|
||||
FileTime lastModified = IOHelper.readAttributes(file).lastModifiedTime();
|
||||
if (lastModified.equals(cacheLastModified))
|
||||
return; // Not modified, so cache is up-to-date
|
||||
|
||||
// Read file
|
||||
LogHelper.info("Recaching auth provider file: '%s'", file);
|
||||
BlockConfigEntry authFile;
|
||||
try (BufferedReader reader = IOHelper.newReader(file)) {
|
||||
authFile = TextConfigReader.read(reader, false);
|
||||
}
|
||||
|
||||
// Read entries from config block
|
||||
entries.clear();
|
||||
Set<Map.Entry<String, ConfigEntry<?>>> entrySet = authFile.getValue().entrySet();
|
||||
for (Map.Entry<String, ConfigEntry<?>> entry : entrySet) {
|
||||
String login = entry.getKey();
|
||||
ConfigEntry<?> value = VerifyHelper.verify(entry.getValue(), v -> v.getType() == Type.BLOCK,
|
||||
String.format("Illegal config entry type: '%s'", login));
|
||||
|
||||
// Add auth entry
|
||||
Entry auth = new Entry((BlockConfigEntry) value);
|
||||
VerifyHelper.putIfAbsent(entries, CommonHelper.low(login), auth,
|
||||
String.format("Duplicate login: '%s'", login));
|
||||
}
|
||||
|
||||
// Update last modified time
|
||||
cacheLastModified = lastModified;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import com.eclipsesource.json.Json;
|
||||
import com.eclipsesource.json.JsonObject;
|
||||
import com.eclipsesource.json.JsonValue;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
|
||||
public final class JsonAuthProvider extends AuthProvider {
|
||||
private static final int TIMEOUT = Integer.parseInt(
|
||||
System.getProperty("launcher.connection.timeout", Integer.toString(1500)));
|
||||
|
||||
private final URL url;
|
||||
private final String userKeyName;
|
||||
private final String passKeyName;
|
||||
private final String ipKeyName;
|
||||
private final String responseUserKeyName;
|
||||
private final String responseErrorKeyName;
|
||||
|
||||
JsonAuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
String configUrl = block.getEntryValue("url", StringConfigEntry.class);
|
||||
userKeyName = VerifyHelper.verify(block.getEntryValue("userKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Username key name can't be empty");
|
||||
passKeyName = VerifyHelper.verify(block.getEntryValue("passKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Password key name can't be empty");
|
||||
ipKeyName = VerifyHelper.verify(block.getEntryValue("ipKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "IP key can't be empty");
|
||||
responseUserKeyName = VerifyHelper.verify(block.getEntryValue("responseUserKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Response username key can't be empty");
|
||||
responseErrorKeyName = VerifyHelper.verify(block.getEntryValue("responseErrorKeyName", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "Response error key can't be empty");
|
||||
url = IOHelper.convertToURL(configUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthProviderResult auth(String login, String password, String ip) throws IOException {
|
||||
JsonObject request = Json.object().add(userKeyName, login).add(passKeyName, password).add(ipKeyName, ip);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
if (TIMEOUT > 0)
|
||||
connection.setConnectTimeout(TIMEOUT);
|
||||
|
||||
OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), Charset.forName("UTF-8"));
|
||||
writer.write(request.toString());
|
||||
writer.flush();
|
||||
writer.close();
|
||||
|
||||
InputStreamReader reader;
|
||||
int statusCode = connection.getResponseCode();
|
||||
|
||||
if (200 <= statusCode && statusCode < 300)
|
||||
reader = new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8);
|
||||
else
|
||||
reader = new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8);
|
||||
JsonValue content = Json.parse(reader);
|
||||
if (!content.isObject())
|
||||
return authError("Authentication server response is malformed");
|
||||
|
||||
JsonObject response = content.asObject();
|
||||
String value;
|
||||
|
||||
if ((value = response.getString(responseUserKeyName, null)) != null)
|
||||
return new AuthProviderResult(value, SecurityHelper.randomStringToken());
|
||||
else if ((value = response.getString(responseErrorKeyName, null)) != null)
|
||||
return authError(value);
|
||||
else
|
||||
return authError("Authentication server response is malformed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// pass
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.eclipsesource.json.Json;
|
||||
import com.eclipsesource.json.JsonObject;
|
||||
import com.eclipsesource.json.JsonValue;
|
||||
import com.eclipsesource.json.WriterConfig;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
|
||||
public final class MojangAuthProvider extends AuthProvider {
|
||||
private static final Pattern UUID_REGEX = Pattern.compile("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})");
|
||||
private static final URL URL;
|
||||
|
||||
static {
|
||||
try {
|
||||
URL = new URL("https://authserver.mojang.com/authenticate");
|
||||
} catch (MalformedURLException e) {
|
||||
throw new InternalError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static JsonObject makeJSONRequest(URL url, JsonObject request) throws IOException {
|
||||
// Make authentication request
|
||||
HttpURLConnection connection = IOHelper.newConnectionPost(url);
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
try (OutputStream output = connection.getOutputStream()) {
|
||||
output.write(request.toString(WriterConfig.MINIMAL).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
connection.getResponseCode(); // Actually make request
|
||||
|
||||
// Read response
|
||||
InputStream errorInput = connection.getErrorStream();
|
||||
try (InputStream input = errorInput == null ? connection.getInputStream() : errorInput) {
|
||||
String charset = connection.getContentEncoding();
|
||||
Charset charsetObject = charset == null ?
|
||||
IOHelper.UNICODE_CHARSET : Charset.forName(charset);
|
||||
|
||||
// Parse response
|
||||
String json = new String(IOHelper.read(input), charsetObject);
|
||||
return json.isEmpty() ? null : Json.parse(json).asObject();
|
||||
}
|
||||
}
|
||||
|
||||
public MojangAuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthProviderResult auth(String login, String password, String ip) throws Exception {
|
||||
JsonObject request = Json.object().
|
||||
add("agent", Json.object().add("name", "Minecraft").add("version", 1)).
|
||||
add("username", login).add("password", password);
|
||||
|
||||
// Verify there's no error
|
||||
JsonObject response = makeJSONRequest(URL, request);
|
||||
if (response == null)
|
||||
authError("Empty mojang response");
|
||||
JsonValue errorMessage = response.get("errorMessage");
|
||||
if (errorMessage != null)
|
||||
authError(errorMessage.asString());
|
||||
|
||||
// Parse JSON data
|
||||
JsonObject selectedProfile = response.get("selectedProfile").asObject();
|
||||
String username = selectedProfile.get("name").asString();
|
||||
String accessToken = response.get("clientToken").asString();
|
||||
UUID uuid = UUID.fromString(UUID_REGEX.matcher(selectedProfile.get("id").asString()).replaceFirst("$1-$2-$3-$4-$5"));
|
||||
String launcherToken = response.get("accessToken").asString();
|
||||
|
||||
// We're done
|
||||
return new MojangAuthProviderResult(username, accessToken, uuid, launcherToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class MojangAuthProviderResult extends AuthProviderResult {
|
||||
public final UUID uuid;
|
||||
public final String launcherToken;
|
||||
|
||||
public MojangAuthProviderResult(String username, String accessToken, UUID uuid, String launcherToken) {
|
||||
super(username, accessToken);
|
||||
this.uuid = uuid;
|
||||
this.launcherToken = launcherToken;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import ru.gravit.launcher.helper.CommonHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.ListConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
import ru.gravit.launchserver.auth.AuthException;
|
||||
import ru.gravit.launchserver.auth.MySQLSourceConfig;
|
||||
|
||||
public final class MySQLAuthProvider extends AuthProvider {
|
||||
private final MySQLSourceConfig mySQLHolder;
|
||||
private final String query;
|
||||
private final String[] queryParams;
|
||||
|
||||
public MySQLAuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
mySQLHolder = new MySQLSourceConfig("authProviderPool", block);
|
||||
|
||||
// Read query
|
||||
query = VerifyHelper.verify(block.getEntryValue("query", StringConfigEntry.class),
|
||||
VerifyHelper.NOT_EMPTY, "MySQL query can't be empty");
|
||||
queryParams = block.getEntry("queryParams", ListConfigEntry.class).
|
||||
stream(StringConfigEntry.class).toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthProviderResult auth(String login, String password, String ip) throws SQLException, AuthException {
|
||||
Connection c = mySQLHolder.getConnection();
|
||||
PreparedStatement s = c.prepareStatement(query);
|
||||
String[] replaceParams = {"login", login, "password", password, "ip", ip};
|
||||
for (int i = 0; i < queryParams.length; i++)
|
||||
s.setString(i + 1, CommonHelper.replace(queryParams[i], replaceParams));
|
||||
|
||||
// Execute SQL query
|
||||
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||
try (ResultSet set = s.executeQuery()) {
|
||||
return set.next() ? new AuthProviderResult(set.getString(1), SecurityHelper.randomStringToken()) : authError("Incorrect username or password");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
|
||||
public final class NullAuthProvider extends AuthProvider {
|
||||
private volatile AuthProvider provider;
|
||||
|
||||
public NullAuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthProviderResult auth(String login, String password, String ip) throws Exception {
|
||||
return getProvider().auth(login, password, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
AuthProvider provider = this.provider;
|
||||
if (provider != null)
|
||||
provider.close();
|
||||
}
|
||||
|
||||
private AuthProvider getProvider() {
|
||||
return VerifyHelper.verify(provider, Objects::nonNull, "Backend auth provider wasn't set");
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void setBackend(AuthProvider provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
import ru.gravit.launchserver.auth.AuthException;
|
||||
|
||||
public final class RejectAuthProvider extends AuthProvider {
|
||||
private final String message;
|
||||
|
||||
public RejectAuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
message = VerifyHelper.verify(block.getEntryValue("message", StringConfigEntry.class), VerifyHelper.NOT_EMPTY,
|
||||
"Auth error message can't be empty");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthProviderResult auth(String login, String password, String ip) throws AuthException {
|
||||
return authError(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package ru.gravit.launchserver.auth.provider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import ru.gravit.launcher.helper.CommonHelper;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
|
||||
public final class RequestAuthProvider extends AuthProvider {
|
||||
private final String url;
|
||||
private final Pattern response;
|
||||
|
||||
public RequestAuthProvider(BlockConfigEntry block) {
|
||||
super(block);
|
||||
url = block.getEntryValue("url", StringConfigEntry.class);
|
||||
response = Pattern.compile(block.getEntryValue("response", StringConfigEntry.class));
|
||||
|
||||
// Verify is valid URL
|
||||
IOHelper.verifyURL(getFormattedURL("urlAuthLogin", "urlAuthPassword", "127.0.0.1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthProviderResult auth(String login, String password, String ip) throws IOException {
|
||||
String currentResponse = IOHelper.request(new URL(getFormattedURL(login, password, ip)));
|
||||
|
||||
// Match username
|
||||
Matcher matcher = response.matcher(currentResponse);
|
||||
return matcher.matches() && matcher.groupCount() >= 1 ?
|
||||
new AuthProviderResult(matcher.group("username"), SecurityHelper.randomStringToken()) :
|
||||
authError(currentResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
private String getFormattedURL(String login, String password, String ip) {
|
||||
return CommonHelper.replace(url, "login", IOHelper.urlEncode(login), "password", IOHelper.urlEncode(password), "ip", IOHelper.urlEncode(ip));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package ru.gravit.launchserver.binary;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.CommonHelper;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import net.sf.launch4j.Builder;
|
||||
import net.sf.launch4j.Log;
|
||||
import net.sf.launch4j.config.Config;
|
||||
import net.sf.launch4j.config.ConfigPersister;
|
||||
import net.sf.launch4j.config.Jre;
|
||||
import net.sf.launch4j.config.LanguageID;
|
||||
import net.sf.launch4j.config.VersionInfo;
|
||||
|
||||
public final class EXEL4JLauncherBinary extends LauncherBinary {
|
||||
private final static class Launch4JLog extends Log {
|
||||
private static final Launch4JLog INSTANCE = new Launch4JLog();
|
||||
|
||||
@Override
|
||||
public void append(String s) {
|
||||
LogHelper.subInfo(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// URL constants
|
||||
private static final String DOWNLOAD_URL = "http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html"; // Oracle
|
||||
// JRE
|
||||
// 8
|
||||
|
||||
// File constants
|
||||
private final Path faviconFile;
|
||||
|
||||
@LauncherAPI
|
||||
public EXEL4JLauncherBinary(LaunchServer server) {
|
||||
super(server, server.dir.resolve(server.config.binaryName + ".exe"));
|
||||
faviconFile = server.dir.resolve("favicon.ico");
|
||||
setConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void build() throws IOException {
|
||||
LogHelper.info("Building launcher EXE binary file (Using Launch4J)");
|
||||
|
||||
// Set favicon path
|
||||
Config config = ConfigPersister.getInstance().getConfig();
|
||||
if (IOHelper.isFile(faviconFile))
|
||||
config.setIcon(faviconFile.toFile());
|
||||
else {
|
||||
config.setIcon(null);
|
||||
LogHelper.warning("Missing favicon.ico file");
|
||||
}
|
||||
|
||||
// Start building
|
||||
Builder builder = new Builder(Launch4JLog.INSTANCE);
|
||||
try {
|
||||
builder.build();
|
||||
} catch (Throwable e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setConfig() {
|
||||
Config config = new Config();
|
||||
// Set string options
|
||||
config.setChdir(".");
|
||||
config.setErrTitle("JVM Error");
|
||||
config.setDownloadUrl(DOWNLOAD_URL);
|
||||
|
||||
// Set boolean options
|
||||
config.setPriorityIndex(0);
|
||||
config.setHeaderType(Config.GUI_HEADER);
|
||||
config.setStayAlive(false);
|
||||
config.setRestartOnCrash(false);
|
||||
|
||||
// Prepare JRE
|
||||
Jre jre = new Jre();
|
||||
jre.setMinVersion("1.8.0");
|
||||
jre.setRuntimeBits(Jre.RUNTIME_BITS_64_AND_32);
|
||||
jre.setJdkPreference(Jre.JDK_PREFERENCE_PREFER_JRE);
|
||||
config.setJre(jre);
|
||||
|
||||
// Prepare version info (product)
|
||||
VersionInfo info = new VersionInfo();
|
||||
info.setProductName(server.config.launch4j.productName);
|
||||
info.setProductVersion(CommonHelper.formatVars(server.config.launch4j.productVer));
|
||||
info.setFileDescription(server.config.launch4j.fileDesc);
|
||||
info.setFileVersion(CommonHelper.formatVars(server.config.launch4j.fileVer));
|
||||
info.setCopyright(server.config.launch4j.copyright);
|
||||
info.setTrademarks(server.config.launch4j.trademarks);
|
||||
info.setInternalName(CommonHelper.formatVars(server.config.launch4j.internalName));
|
||||
// Prepare version info (file)
|
||||
info.setTxtFileVersion(CommonHelper.formatVars(server.config.launch4j.txtFileVersion));
|
||||
info.setTxtProductVersion(CommonHelper.formatVars(server.config.launch4j.txtProductVersion));
|
||||
// Prepare version info (misc)
|
||||
info.setOriginalFilename(binaryFile.getFileName().toString());
|
||||
info.setLanguage(LanguageID.RUSSIAN);
|
||||
config.setVersionInfo(info);
|
||||
|
||||
// Set JAR wrapping options
|
||||
config.setDontWrapJar(false);
|
||||
config.setJar(server.launcherBinary.syncBinaryFile.toFile());
|
||||
config.setOutfile(binaryFile.toFile());
|
||||
|
||||
// Return prepared config
|
||||
ConfigPersister.getInstance().setAntConfig(config, null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package ru.gravit.launchserver.binary;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
|
||||
public class EXELauncherBinary extends LauncherBinary {
|
||||
|
||||
public EXELauncherBinary(LaunchServer server) {
|
||||
super(server, server.dir.resolve(server.config.binaryName + ".exe"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void build() throws IOException {
|
||||
if (IOHelper.isFile(binaryFile)) {
|
||||
LogHelper.subWarning("Deleting obsolete launcher EXE binary file");
|
||||
Files.delete(binaryFile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package ru.gravit.launchserver.binary;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javassist.CannotCompileException;
|
||||
import javassist.ClassPool;
|
||||
import javassist.CtClass;
|
||||
import javassist.CtConstructor;
|
||||
import javassist.NotFoundException;
|
||||
|
||||
public class JAConfigurator implements AutoCloseable {
|
||||
ClassPool pool = ClassPool.getDefault();
|
||||
CtClass ctClass;
|
||||
CtConstructor ctConstructor;
|
||||
String classname;
|
||||
StringBuilder body;
|
||||
int autoincrement;
|
||||
public JAConfigurator(Class<?> configclass) throws NotFoundException {
|
||||
classname = configclass.getName();
|
||||
ctClass = pool.get(classname);
|
||||
ctConstructor = ctClass.getDeclaredConstructor(null);
|
||||
body = new StringBuilder("{");
|
||||
autoincrement = 0;
|
||||
}
|
||||
public void addModuleClass(String fullName)
|
||||
{
|
||||
body.append("Module mod");
|
||||
body.append(autoincrement);
|
||||
body.append(" = new ");
|
||||
body.append(fullName);
|
||||
body.append("();");
|
||||
body.append("Launcher.modulesManager.registerModule( mod");
|
||||
body.append(autoincrement);
|
||||
body.append(" , true );");
|
||||
autoincrement++;
|
||||
}
|
||||
@Override
|
||||
public void close() {
|
||||
ctClass.defrost();
|
||||
}
|
||||
public byte[] getBytecode() throws IOException, CannotCompileException {
|
||||
body.append("}");
|
||||
ctConstructor.setBody(body.toString());
|
||||
return ctClass.toBytecode();
|
||||
}
|
||||
public String getZipEntryPath()
|
||||
{
|
||||
return classname.replace('.','/').concat(".class");
|
||||
}
|
||||
public void setAddress(String address)
|
||||
{
|
||||
body.append("this.address = \"");
|
||||
body.append(address);
|
||||
body.append("\";");
|
||||
}
|
||||
|
||||
public void setPort(int port)
|
||||
{
|
||||
body.append("this.port = ");
|
||||
body.append(port);
|
||||
body.append(";");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
package ru.gravit.launchserver.binary;
|
||||
|
||||
import static ru.gravit.launcher.helper.IOHelper.newZipEntry;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipException;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import javassist.CannotCompileException;
|
||||
import javassist.NotFoundException;
|
||||
import ru.gravit.launcher.AutogenConfig;
|
||||
import ru.gravit.launcher.Launcher;
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.LauncherConfig;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper.DigestAlgorithm;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import proguard.Configuration;
|
||||
import proguard.ConfigurationParser;
|
||||
import proguard.ParseException;
|
||||
import proguard.ProGuard;
|
||||
|
||||
public final class JARLauncherBinary extends LauncherBinary {
|
||||
private final class RuntimeDirVisitor extends SimpleFileVisitor<Path> {
|
||||
private final ZipOutputStream output;
|
||||
private final Map<String, byte[]> runtime;
|
||||
|
||||
private RuntimeDirVisitor(ZipOutputStream output, Map<String, byte[]> runtime) {
|
||||
this.output = output;
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
String dirName = IOHelper.toString(runtimeDir.relativize(dir));
|
||||
output.putNextEntry(newEntry(dirName + '/'));
|
||||
return super.preVisitDirectory(dir, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
String fileName = IOHelper.toString(runtimeDir.relativize(file));
|
||||
runtime.put(fileName, SecurityHelper.digest(DigestAlgorithm.MD5, file));
|
||||
|
||||
// Create zip entry and transfer contents
|
||||
output.putNextEntry(newEntry(fileName));
|
||||
IOHelper.transfer(file, output);
|
||||
|
||||
// Return result
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
}
|
||||
|
||||
private static ZipEntry newEntry(String fileName) {
|
||||
return newZipEntry(Launcher.RUNTIME_DIR + IOHelper.CROSS_SEPARATOR + fileName);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public final Path runtimeDir;
|
||||
|
||||
@LauncherAPI
|
||||
public final Path initScriptFile;
|
||||
|
||||
@LauncherAPI
|
||||
public final Path obfJar;
|
||||
|
||||
@LauncherAPI
|
||||
public JARLauncherBinary(LaunchServer server) throws IOException {
|
||||
super(server, server.dir.resolve(server.config.binaryName + ".jar"),
|
||||
server.dir.resolve(server.config.binaryName + (server.config.sign.enabled ? "-sign.jar" : "-obf.jar")));
|
||||
runtimeDir = server.dir.resolve(Launcher.RUNTIME_DIR);
|
||||
initScriptFile = runtimeDir.resolve(Launcher.INIT_SCRIPT_FILE);
|
||||
obfJar = server.config.sign.enabled ? server.dir.resolve(server.config.binaryName + "-obf.jar")
|
||||
: syncBinaryFile;
|
||||
tryUnpackRuntime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void build() throws IOException {
|
||||
tryUnpackRuntime();
|
||||
|
||||
// Build launcher binary
|
||||
LogHelper.info("Building launcher binary file");
|
||||
stdBuild();
|
||||
|
||||
// ProGuard
|
||||
Configuration proguard_cfg = new Configuration();
|
||||
ConfigurationParser parser = new ConfigurationParser(
|
||||
server.proguardConf.confStrs.toArray(new String[server.proguardConf.confStrs.size()]),
|
||||
server.proguardConf.proguard.toFile(), System.getProperties());
|
||||
try {
|
||||
parser.parse(proguard_cfg);
|
||||
ProGuard proGuard = new ProGuard(proguard_cfg);
|
||||
proGuard.execute();
|
||||
} catch (ParseException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
if (server.config.sign.enabled)
|
||||
signBuild();
|
||||
}
|
||||
|
||||
private void signBuild() throws IOException {
|
||||
try (SignerJar output = new SignerJar(IOHelper.newOutput(syncBinaryFile),
|
||||
SignerJar.getStore(server.config.sign.key, server.config.sign.storepass, server.config.sign.algo),
|
||||
server.config.sign.keyalias, server.config.sign.pass);
|
||||
ZipInputStream input = new ZipInputStream(IOHelper.newInput(obfJar))) {
|
||||
ZipEntry e = input.getNextEntry();
|
||||
while (e != null) {
|
||||
output.addFileContents(e, input);
|
||||
e = input.getNextEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stdBuild() throws IOException {
|
||||
try (ZipOutputStream output = new ZipOutputStream(IOHelper.newOutput(binaryFile));
|
||||
JAConfigurator jaConfigurator = new JAConfigurator(AutogenConfig.class)) {
|
||||
Map<String, byte[]> outputM1 = new HashMap<>();
|
||||
server.buildHookManager.preHook(outputM1);
|
||||
for (Entry<String, byte[]> e : outputM1.entrySet()) {
|
||||
output.putNextEntry(newZipEntry(e.getKey()));
|
||||
output.write(e.getValue());
|
||||
}
|
||||
outputM1.clear();
|
||||
jaConfigurator.setAddress(server.config.getAddress());
|
||||
jaConfigurator.setPort(server.config.port);
|
||||
server.buildHookManager.registerAllClientModuleClass(jaConfigurator);
|
||||
try (ZipInputStream input = new ZipInputStream(
|
||||
IOHelper.newInput(IOHelper.getResourceURL("Launcher.jar")))) {
|
||||
ZipEntry e = input.getNextEntry();
|
||||
while (e != null) {
|
||||
String filename = e.getName();
|
||||
if (server.buildHookManager.isContainsBlacklist(filename)) {
|
||||
e = input.getNextEntry();
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
output.putNextEntry(e);
|
||||
} catch (ZipException ex) {
|
||||
LogHelper.error(ex);
|
||||
e = input.getNextEntry();
|
||||
continue;
|
||||
}
|
||||
if (filename.endsWith(".class")) {
|
||||
CharSequence classname = filename.replace('/', '.').subSequence(0,
|
||||
filename.length() - ".class".length());
|
||||
byte[] bytes;
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(2048)) {
|
||||
IOHelper.transfer(input, outputStream);
|
||||
bytes = outputStream.toByteArray();
|
||||
}
|
||||
bytes = server.buildHookManager.classTransform(bytes, classname);
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) {
|
||||
IOHelper.transfer(inputStream, output);
|
||||
}
|
||||
} else
|
||||
IOHelper.transfer(input, output);
|
||||
// }
|
||||
e = input.getNextEntry();
|
||||
}
|
||||
}
|
||||
// write additional classes
|
||||
for (Entry<String, byte[]> ent : server.buildHookManager.getIncludeClass().entrySet()) {
|
||||
output.putNextEntry(newZipEntry(ent.getKey().replace('.', '/').concat(".class")));
|
||||
output.write(server.buildHookManager.classTransform(ent.getValue(), ent.getKey()));
|
||||
}
|
||||
// map for runtime
|
||||
Map<String, byte[]> runtime = new HashMap<>(256);
|
||||
if (server.buildHookManager.buildRuntime()) {
|
||||
// Verify has init script file
|
||||
if (!IOHelper.isFile(initScriptFile))
|
||||
throw new IOException(String.format("Missing init script file ('%s')", Launcher.INIT_SCRIPT_FILE));
|
||||
// Write launcher runtime dir
|
||||
IOHelper.walk(runtimeDir, new RuntimeDirVisitor(output, runtime), false);
|
||||
}
|
||||
// Create launcher config file
|
||||
byte[] launcherConfigBytes;
|
||||
try (ByteArrayOutputStream configArray = IOHelper.newByteArrayOutput()) {
|
||||
try (HOutput configOutput = new HOutput(configArray)) {
|
||||
new LauncherConfig(server.config.getAddress(), server.config.port, server.publicKey, runtime)
|
||||
.write(configOutput);
|
||||
}
|
||||
launcherConfigBytes = configArray.toByteArray();
|
||||
}
|
||||
|
||||
// Write launcher config file
|
||||
output.putNextEntry(newZipEntry(Launcher.CONFIG_FILE));
|
||||
output.write(launcherConfigBytes);
|
||||
ZipEntry e = newZipEntry(jaConfigurator.getZipEntryPath());
|
||||
output.putNextEntry(e);
|
||||
output.write(jaConfigurator.getBytecode());
|
||||
server.buildHookManager.postHook(outputM1);
|
||||
for (Entry<String, byte[]> e1 : outputM1.entrySet()) {
|
||||
output.putNextEntry(newZipEntry(e1.getKey()));
|
||||
output.write(e1.getValue());
|
||||
}
|
||||
outputM1.clear();
|
||||
} catch (CannotCompileException | NotFoundException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void tryUnpackRuntime() throws IOException {
|
||||
// Verify is runtime dir unpacked
|
||||
if (IOHelper.isDir(runtimeDir))
|
||||
return; // Already unpacked
|
||||
|
||||
// Unpack launcher runtime files
|
||||
Files.createDirectory(runtimeDir);
|
||||
LogHelper.info("Unpacking launcher runtime files");
|
||||
try (ZipInputStream input = IOHelper.newZipInput(IOHelper.getResourceURL("runtime.zip"))) {
|
||||
for (ZipEntry entry = input.getNextEntry(); entry != null; entry = input.getNextEntry()) {
|
||||
if (entry.isDirectory())
|
||||
continue; // Skip dirs
|
||||
|
||||
// Unpack runtime file
|
||||
IOHelper.transfer(input, runtimeDir.resolve(IOHelper.toPath(entry.getName())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package ru.gravit.launchserver.binary;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.serialize.signed.SignedBytesHolder;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
|
||||
public abstract class LauncherBinary {
|
||||
@LauncherAPI
|
||||
protected final LaunchServer server;
|
||||
@LauncherAPI
|
||||
protected final Path binaryFile;
|
||||
protected final Path syncBinaryFile;
|
||||
private volatile SignedBytesHolder binary;
|
||||
|
||||
@LauncherAPI
|
||||
protected LauncherBinary(LaunchServer server, Path binaryFile) {
|
||||
this.server = server;
|
||||
this.binaryFile = binaryFile;
|
||||
syncBinaryFile = binaryFile;
|
||||
}
|
||||
@LauncherAPI
|
||||
protected LauncherBinary(LaunchServer server, Path binaryFile, Path syncBinaryFile) {
|
||||
this.server = server;
|
||||
this.binaryFile = binaryFile;
|
||||
this.syncBinaryFile = syncBinaryFile;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public abstract void build() throws IOException;
|
||||
|
||||
@LauncherAPI
|
||||
public final boolean exists() {
|
||||
return IOHelper.isFile(syncBinaryFile);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public final SignedBytesHolder getBytes() {
|
||||
return binary;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public final boolean sync() throws IOException {
|
||||
boolean exists = exists();
|
||||
binary = exists ? new SignedBytesHolder(IOHelper.read(syncBinaryFile), server.privateKey) : null;
|
||||
return exists;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,423 @@
|
|||
package ru.gravit.launchserver.binary;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||
import org.bouncycastle.cms.CMSSignedData;
|
||||
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
||||
import org.bouncycastle.cms.CMSTypedData;
|
||||
import org.bouncycastle.cms.SignerInfoGenerator;
|
||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.DigestCalculatorProvider;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||
import org.bouncycastle.util.Store;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
|
||||
|
||||
/**
|
||||
* Generator of signed Jars. It stores some data in memory therefore it is not suited for creation of large files. The
|
||||
* usage:
|
||||
* <pre>
|
||||
* KeyStore keystore = KeyStore.getInstance("JKS");
|
||||
* keyStore.load(keystoreStream, "keystorePassword");
|
||||
* SignerJar jar = new SignerJar(out, keyStore, "keyAlias", "keyPassword");
|
||||
* signedJar.addManifestAttribute("Main-Class", "com.example.MainClass");
|
||||
* signedJar.addManifestAttribute("Application-Name", "Example");
|
||||
* signedJar.addManifestAttribute("Permissions", "all-permissions");
|
||||
* signedJar.addManifestAttribute("Codebase", "*");
|
||||
* signedJar.addFileContents("com/example/MainClass.class", clsData);
|
||||
* signedJar.addFileContents("JNLP-INF/APPLICATION.JNLP", generateJnlpContents());
|
||||
* signedJar.close();
|
||||
* </pre>
|
||||
*/
|
||||
public class SignerJar implements AutoCloseable {
|
||||
/** Helper output stream that also sends the data to the given {@link com.google.common.hash.Hasher}. */
|
||||
private static class HashingOutputStream extends OutputStream {
|
||||
private final OutputStream out;
|
||||
private final MessageDigest hasher;
|
||||
|
||||
public HashingOutputStream(OutputStream out, MessageDigest hasher) {
|
||||
this.out = out;
|
||||
this.hasher = hasher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
out.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
out.write(b);
|
||||
hasher.update(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
out.write(b, off, len);
|
||||
hasher.update(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
out.write(b);
|
||||
hasher.update((byte) b);
|
||||
}
|
||||
}
|
||||
private static final String MANIFEST_FN = "META-INF/MANIFEST.MF";
|
||||
private static final String SIG_FN = "META-INF/SIGNUMO.SF";
|
||||
|
||||
private static final String SIG_RSA_FN = "META-INF/SIGNUMO.RSA";
|
||||
|
||||
private static final String hashFunctionName = "SHA-256";
|
||||
|
||||
public static final KeyStore getStore(Path file, String storepass, String algo) throws IOException {
|
||||
try {
|
||||
KeyStore st = KeyStore.getInstance(algo);
|
||||
st.load(IOHelper.newInput(file), storepass != null ? storepass.toCharArray() : null);
|
||||
return st;
|
||||
} catch (NoSuchAlgorithmException | CertificateException| KeyStoreException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
private final static MessageDigest hasher() {
|
||||
try {
|
||||
return MessageDigest.getInstance(hashFunctionName);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private final ZipOutputStream zos;
|
||||
|
||||
private final KeyStore keyStore;
|
||||
|
||||
private final String keyAlias;
|
||||
|
||||
private final String password;
|
||||
private final Map<String, String> manifestAttributes;
|
||||
private String manifestHash;
|
||||
private String manifestMainHash;
|
||||
|
||||
private final Map<String, String> fileDigests;
|
||||
|
||||
private final Map<String, String> sectionDigests;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param out the output stream to write JAR data to
|
||||
* @param keyStore the key store to load given key from
|
||||
* @param keyAlias the name of the key in the store, this key is used to sign the JAR
|
||||
* @param keyPassword the password to access the key
|
||||
*/
|
||||
public SignerJar(OutputStream out, KeyStore keyStore, String keyAlias, String keyPassword) {
|
||||
zos = new ZipOutputStream(out);
|
||||
this.keyStore = keyStore;
|
||||
this.keyAlias = keyAlias;
|
||||
password = keyPassword;
|
||||
|
||||
manifestAttributes = new LinkedHashMap<>();
|
||||
fileDigests = new LinkedHashMap<>();
|
||||
sectionDigests = new LinkedHashMap<>();
|
||||
}
|
||||
/**
|
||||
* Adds a file to the JAR. The file is immediately added to the zipped output stream. This method cannot be called once
|
||||
* the stream is closed.
|
||||
*
|
||||
* @param filename name of the file to add (use forward slash as a path separator)
|
||||
* @param contents contents of the file
|
||||
* @throws java.io.IOException
|
||||
* @throws NullPointerException if any of the arguments is {@code null}
|
||||
*/
|
||||
public void addFileContents(String filename, byte[] contents) throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(filename));
|
||||
zos.write(contents);
|
||||
zos.closeEntry();
|
||||
|
||||
String hashCode64 = Base64.getEncoder().encodeToString(hasher().digest(contents));
|
||||
fileDigests.put(filename, hashCode64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the JAR. The file is immediately added to the zipped output stream. This method cannot be called once
|
||||
* the stream is closed.
|
||||
*
|
||||
* @param filename name of the file to add (use forward slash as a path separator)
|
||||
* @param contents contents of the file
|
||||
* @throws java.io.IOException
|
||||
* @throws NullPointerException if any of the arguments is {@code null}
|
||||
*/
|
||||
public void addFileContents(String filename, InputStream contents) throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(filename));
|
||||
byte[] arr = IOHelper.toByteArray(contents);
|
||||
zos.write(arr);
|
||||
zos.closeEntry();
|
||||
|
||||
String hashCode64 = Base64.getEncoder().encodeToString(hasher().digest(arr));
|
||||
fileDigests.put(filename, hashCode64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the JAR. The file is immediately added to the zipped output stream. This method cannot be called once
|
||||
* the stream is closed.
|
||||
*
|
||||
* @param entry name of the file to add (use forward slash as a path separator)
|
||||
* @param contents contents of the file
|
||||
* @throws java.io.IOException
|
||||
* @throws NullPointerException if any of the arguments is {@code null}
|
||||
*/
|
||||
public void addFileContents(ZipEntry entry, byte[] contents) throws IOException {
|
||||
zos.putNextEntry(entry);
|
||||
zos.write(contents);
|
||||
zos.closeEntry();
|
||||
|
||||
String hashCode64 = Base64.getEncoder().encodeToString(hasher().digest(contents));
|
||||
fileDigests.put(entry.getName(), hashCode64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the JAR. The file is immediately added to the zipped output stream. This method cannot be called once
|
||||
* the stream is closed.
|
||||
*
|
||||
* @param entry name of the file to add (use forward slash as a path separator)
|
||||
* @param contents contents of the file
|
||||
* @throws java.io.IOException
|
||||
* @throws NullPointerException if any of the arguments is {@code null}
|
||||
*/
|
||||
public void addFileContents(ZipEntry entry, InputStream contents) throws IOException {
|
||||
zos.putNextEntry(entry);
|
||||
byte[] arr = IOHelper.toByteArray(contents);
|
||||
zos.write(arr);
|
||||
zos.closeEntry();
|
||||
|
||||
String hashCode64 = Base64.getEncoder().encodeToString(hasher().digest(arr));
|
||||
fileDigests.put(entry.getName(), hashCode64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header to the manifest of the JAR.
|
||||
*
|
||||
* @param name name of the attribute, it is placed into the main section of the manifest file, it cannot be longer
|
||||
* than {@value #MANIFEST_ATTR_MAX_LEN} bytes (in utf-8 encoding)
|
||||
* @param value value of the attribute
|
||||
*/
|
||||
public void addManifestAttribute(String name, String value) {
|
||||
manifestAttributes.put(name, value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Closes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It closes the
|
||||
* underlying stream.
|
||||
*
|
||||
* @throws java.io.IOException
|
||||
* @throws RuntimeException if the signing goes wrong
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
finish();
|
||||
zos.close();
|
||||
}
|
||||
|
||||
/** Creates the beast that can actually sign the data. */
|
||||
private CMSSignedDataGenerator createSignedDataGenerator() throws Exception {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
|
||||
List<Certificate> certChain = new ArrayList<>(Arrays.asList(keyStore.getCertificateChain(keyAlias)));
|
||||
Store certStore = new JcaCertStore(certChain);
|
||||
Certificate cert = keyStore.getCertificate(keyAlias);
|
||||
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, password != null ? password.toCharArray() : null);
|
||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256WITHRSA").setProvider("BC").build(privateKey);
|
||||
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
|
||||
DigestCalculatorProvider dcp = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
|
||||
SignerInfoGenerator sig = new JcaSignerInfoGeneratorBuilder(dcp).build(signer, (X509Certificate) cert);
|
||||
generator.addSignerInfoGenerator(sig);
|
||||
generator.addCertificates(certStore);
|
||||
return generator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finishes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It leaves the
|
||||
* underlying stream open.
|
||||
*
|
||||
* @throws java.io.IOException
|
||||
* @throws RuntimeException if the signing goes wrong
|
||||
*/
|
||||
public void finish() throws IOException {
|
||||
writeManifest();
|
||||
byte sig[] = writeSigFile();
|
||||
writeSignature(sig);
|
||||
zos.finish();
|
||||
}
|
||||
|
||||
public ZipOutputStream getZos() {
|
||||
return zos;
|
||||
}
|
||||
|
||||
/** Helper for {@link #writeManifest()} that creates the digest of one entry. */
|
||||
private String hashEntrySection(String name, Attributes attributes) throws IOException {
|
||||
Manifest manifest = new Manifest();
|
||||
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
|
||||
ByteArrayOutputStream o = new ByteArrayOutputStream();
|
||||
manifest.write(o);
|
||||
int emptyLen = o.toByteArray().length;
|
||||
|
||||
manifest.getEntries().put(name, attributes);
|
||||
|
||||
manifest.write(o);
|
||||
byte[] ob = o.toByteArray();
|
||||
ob = Arrays.copyOfRange(ob, emptyLen, ob.length);
|
||||
return Base64.getEncoder().encodeToString(hasher().digest(ob));
|
||||
}
|
||||
|
||||
/** Helper for {@link #writeManifest()} that creates the digest of the main section. */
|
||||
private String hashMainSection(Attributes attributes) throws IOException {
|
||||
Manifest manifest = new Manifest();
|
||||
manifest.getMainAttributes().putAll(attributes);
|
||||
MessageDigest hasher = hasher();
|
||||
SignerJar.HashingOutputStream o = new SignerJar.HashingOutputStream(new OutputStream() {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NullOutputStream";
|
||||
}
|
||||
/** Discards the specified byte array. */
|
||||
@Override public void write(byte[] b) {
|
||||
}
|
||||
/** Discards the specified byte array. */
|
||||
@Override public void write(byte[] b, int off, int len) {
|
||||
}
|
||||
|
||||
/** Discards the specified byte. */
|
||||
@Override public void write(int b) {
|
||||
}
|
||||
}, hasher);
|
||||
manifest.write(o);
|
||||
return Base64.getEncoder().encodeToString(hasher.digest());
|
||||
}
|
||||
|
||||
/** Returns the CMS signed data. */
|
||||
private byte[] signSigFile(byte[] sigContents) throws Exception {
|
||||
CMSSignedDataGenerator gen = createSignedDataGenerator();
|
||||
CMSTypedData cmsData = new CMSProcessableByteArray(sigContents);
|
||||
CMSSignedData signedData = gen.generate(cmsData, true);
|
||||
return signedData.getEncoded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the manifest to the JAR. It also calculates the digests that are required to be placed in the the signature
|
||||
* file.
|
||||
*
|
||||
* @throws java.io.IOException
|
||||
*/
|
||||
private void writeManifest() throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(MANIFEST_FN));
|
||||
Manifest man = new Manifest();
|
||||
|
||||
// main section
|
||||
Attributes mainAttributes = man.getMainAttributes();
|
||||
mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
|
||||
|
||||
for (Map.Entry<String, String> entry : manifestAttributes.entrySet())
|
||||
mainAttributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
|
||||
|
||||
// individual files sections
|
||||
Attributes.Name digestAttr = new Attributes.Name(hashFunctionName + "-Digest");
|
||||
for (Map.Entry<String, String> entry : fileDigests.entrySet()) {
|
||||
Attributes attributes = new Attributes();
|
||||
man.getEntries().put(entry.getKey(), attributes);
|
||||
attributes.put(digestAttr, entry.getValue());
|
||||
sectionDigests.put(entry.getKey(), hashEntrySection(entry.getKey(), attributes));
|
||||
}
|
||||
|
||||
MessageDigest hasher = hasher();
|
||||
OutputStream out = new SignerJar.HashingOutputStream(zos, hasher);
|
||||
man.write(out);
|
||||
zos.closeEntry();
|
||||
|
||||
manifestHash = Base64.getEncoder().encodeToString(hasher.digest());
|
||||
manifestMainHash = hashMainSection(man.getMainAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the .SIG file to the JAR.
|
||||
*
|
||||
* @return the contents of the file as bytes
|
||||
*/
|
||||
private byte[] writeSigFile() throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(SIG_FN));
|
||||
Manifest man = new Manifest();
|
||||
// main section
|
||||
Attributes mainAttributes = man.getMainAttributes();
|
||||
mainAttributes.put(Attributes.Name.SIGNATURE_VERSION, "1.0");
|
||||
mainAttributes.put(new Attributes.Name(hashFunctionName + "-Digest-Manifest"), manifestHash);
|
||||
mainAttributes.put(new Attributes.Name(hashFunctionName + "-Digest-Manifest-Main-Attributes"), manifestMainHash);
|
||||
|
||||
// individual files sections
|
||||
Attributes.Name digestAttr = new Attributes.Name(hashFunctionName + "-Digest");
|
||||
for (Map.Entry<String, String> entry : sectionDigests.entrySet()) {
|
||||
Attributes attributes = new Attributes();
|
||||
man.getEntries().put(entry.getKey(), attributes);
|
||||
attributes.put(digestAttr, entry.getValue());
|
||||
}
|
||||
|
||||
man.write(zos);
|
||||
zos.closeEntry();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
man.write(baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the .SIG file and writes the signature (.RSA file) to the JAR.
|
||||
*
|
||||
* @throws java.io.IOException
|
||||
* @throws RuntimeException if the signing failed
|
||||
*/
|
||||
private void writeSignature(byte[] sigFile) throws IOException {
|
||||
zos.putNextEntry(new ZipEntry(SIG_RSA_FN));
|
||||
try {
|
||||
byte[] signature = signSigFile(sigFile);
|
||||
zos.write(signature);
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Signing failed.", e);
|
||||
}
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package ru.gravit.launchserver.command;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
|
||||
public abstract class Command {
|
||||
@LauncherAPI
|
||||
protected static String parseUsername(String username) throws CommandException {
|
||||
try {
|
||||
return VerifyHelper.verifyUsername(username);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new CommandException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected static UUID parseUUID(String s) throws CommandException {
|
||||
try {
|
||||
return UUID.fromString(s);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
throw new CommandException(String.format("Invalid UUID: '%s'", s));
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected final LaunchServer server;
|
||||
|
||||
@LauncherAPI
|
||||
protected Command(LaunchServer server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public abstract String getArgsDescription(); // "<required> [optional]"
|
||||
|
||||
@LauncherAPI
|
||||
public abstract String getUsageDescription();
|
||||
|
||||
@LauncherAPI
|
||||
public abstract void invoke(String... args) throws Exception;
|
||||
|
||||
@LauncherAPI
|
||||
protected final void verifyArgs(String[] args, int min) throws CommandException {
|
||||
if (args.length < min)
|
||||
throw new CommandException("Command usage: " + getArgsDescription());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package ru.gravit.launchserver.command;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
|
||||
public final class CommandException extends Exception {
|
||||
private static final long serialVersionUID = -6588814993972117772L;
|
||||
|
||||
@LauncherAPI
|
||||
public CommandException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public CommandException(Throwable exc) {
|
||||
super(exc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getMessage();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package ru.gravit.launchserver.command.auth;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.auth.provider.AuthProviderResult;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class AuthCommand extends Command {
|
||||
public AuthCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "<login> <password>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Try to auth with specified login and password";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
verifyArgs(args, 2);
|
||||
String login = args[0];
|
||||
String password = args[1];
|
||||
|
||||
// Authenticate
|
||||
AuthProviderResult result = server.config.authProvider.auth(login, password, "127.0.0.1");
|
||||
UUID uuid = server.config.authHandler.auth(result);
|
||||
|
||||
// Print auth successful message
|
||||
LogHelper.subInfo("UUID: %s, Username: '%s', Access Token: '%s'", uuid, result.username, result.accessToken);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package ru.gravit.launchserver.command.auth;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.auth.hwid.HWID;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public class BanCommand extends Command {
|
||||
public BanCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "[username]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Ban username for HWID";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
verifyArgs(args,1);
|
||||
List<HWID> target = server.config.hwidHandler.getHwid(args[0]);
|
||||
server.config.hwidHandler.ban(target);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package ru.gravit.launchserver.command.auth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
import ru.gravit.launchserver.command.CommandException;
|
||||
|
||||
public final class UUIDToUsernameCommand extends Command {
|
||||
public UUIDToUsernameCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "<uuid>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Convert player UUID to username";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws CommandException, IOException {
|
||||
verifyArgs(args, 1);
|
||||
UUID uuid = parseUUID(args[0]);
|
||||
|
||||
// Get UUID by username
|
||||
String username = server.config.authHandler.uuidToUsername(uuid);
|
||||
if (username == null)
|
||||
throw new CommandException("Unknown UUID: " + uuid);
|
||||
|
||||
// Print username
|
||||
LogHelper.subInfo("Username of player %s: '%s'", uuid, username);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package ru.gravit.launchserver.command.auth;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.auth.hwid.HWID;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public class UnbanCommand extends Command {
|
||||
public UnbanCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "[username]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Unban username for HWID";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
verifyArgs(args,1);
|
||||
List<HWID> target = server.config.hwidHandler.getHwid(args[0]);
|
||||
server.config.hwidHandler.unban(target);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package ru.gravit.launchserver.command.auth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
import ru.gravit.launchserver.command.CommandException;
|
||||
|
||||
public final class UsernameToUUIDCommand extends Command {
|
||||
public UsernameToUUIDCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "<username>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Convert player username to UUID";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws CommandException, IOException {
|
||||
verifyArgs(args, 1);
|
||||
String username = parseUsername(args[0]);
|
||||
|
||||
// Get UUID by username
|
||||
UUID uuid = server.config.authHandler.usernameToUUID(username);
|
||||
if (uuid == null)
|
||||
throw new CommandException(String.format("Unknown username: '%s'", username));
|
||||
|
||||
// Print UUID
|
||||
LogHelper.subInfo("UUID of player '%s': %s", username, uuid);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class BuildCommand extends Command {
|
||||
public BuildCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Build launcher binaries";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
server.buildLauncherBinaries();
|
||||
server.syncLauncherBinaries();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class ClearCommand extends Command {
|
||||
public ClearCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Clear terminal";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
server.commandHandler.clear();
|
||||
LogHelper.subInfo("Terminal cleared");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class DebugCommand extends Command {
|
||||
public DebugCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "[true/false]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Enable or disable debug logging at runtime";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) {
|
||||
boolean newValue;
|
||||
if (args.length >= 1) {
|
||||
newValue = Boolean.parseBoolean(args[0]);
|
||||
LogHelper.setDebugEnabled(newValue);
|
||||
} else
|
||||
newValue = LogHelper.isDebugEnabled();
|
||||
LogHelper.subInfo("Debug enabled: " + newValue);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import ru.gravit.launcher.helper.JVMHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.managers.GarbageManager;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class GCCommand extends Command {
|
||||
public GCCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Perform Garbage Collection and print memory usage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) {
|
||||
LogHelper.subInfo("Performing full GC");
|
||||
JVMHelper.fullGC();
|
||||
GarbageManager.gc();
|
||||
// Print memory usage
|
||||
long max = JVMHelper.RUNTIME.maxMemory() >> 20;
|
||||
long free = JVMHelper.RUNTIME.freeMemory() >> 20;
|
||||
long total = JVMHelper.RUNTIME.totalMemory() >> 20;
|
||||
long used = total - free;
|
||||
LogHelper.subInfo("Heap usage: %d / %d / %d MiB", used, total, max);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
import ru.gravit.launchserver.command.CommandException;
|
||||
|
||||
public final class HelpCommand extends Command {
|
||||
private static void printCommand(String name, Command command) {
|
||||
String args = command.getArgsDescription();
|
||||
LogHelper.subInfo("%s %s - %s", name, args == null ? "[nothing]" : args, command.getUsageDescription());
|
||||
}
|
||||
|
||||
public HelpCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "[command name]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Print command usage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws CommandException {
|
||||
if (args.length < 1) {
|
||||
printCommands();
|
||||
return;
|
||||
}
|
||||
|
||||
// Print command help
|
||||
printCommand(args[0]);
|
||||
}
|
||||
|
||||
private void printCommand(String name) throws CommandException {
|
||||
printCommand(name, server.commandHandler.lookup(name));
|
||||
}
|
||||
|
||||
private void printCommands() {
|
||||
for (Entry<String, Command> entry : server.commandHandler.commandsMap().entrySet())
|
||||
printCommand(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class LogConnectionsCommand extends Command {
|
||||
public LogConnectionsCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "[true/false]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Enable or disable logging connections";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) {
|
||||
boolean newValue;
|
||||
if (args.length >= 1) {
|
||||
newValue = Boolean.parseBoolean(args[0]);
|
||||
server.serverSocketHandler.logConnections = newValue;
|
||||
} else
|
||||
newValue = server.serverSocketHandler.logConnections;
|
||||
LogHelper.subInfo("Log connections: " + newValue);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public class ProguardCleanCommand extends Command {
|
||||
public ProguardCleanCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Resets proguard config";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) {
|
||||
server.proguardConf.prepare(true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class RebindCommand extends Command {
|
||||
public RebindCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Rebind server socket";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) {
|
||||
server.rebindServerSocket();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public class RegenProguardDictCommand extends Command {
|
||||
|
||||
public RegenProguardDictCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Regenerates proguard dictonary";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws IOException {
|
||||
server.proguardConf.genWords(true);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public class RemoveMappingsProguardCommand extends Command {
|
||||
|
||||
public RemoveMappingsProguardCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Removes proguard mappings (if you want to gen new mappings).";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws IOException {
|
||||
Files.deleteIfExists(server.proguardConf.mappings);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import ru.gravit.launcher.helper.JVMHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class StopCommand extends Command {
|
||||
public StopCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Stop LaunchServer";
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("CallToSystemExit")
|
||||
public void invoke(String... args) {
|
||||
JVMHelper.RUNTIME.exit(0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package ru.gravit.launchserver.command.basic;
|
||||
|
||||
import ru.gravit.launcher.LauncherVersion;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class VersionCommand extends Command {
|
||||
public VersionCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Print LaunchServer version";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) {
|
||||
LogHelper.subInfo("LaunchServer version: %d.%d.%d (build #%d)", LauncherVersion.MAJOR, LauncherVersion.MINOR, LauncherVersion.PATCH, LauncherVersion.BUILD);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
package ru.gravit.launchserver.command.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
import ru.gravit.launchserver.command.CommandException;
|
||||
import ru.gravit.launchserver.command.auth.AuthCommand;
|
||||
import ru.gravit.launchserver.command.auth.BanCommand;
|
||||
import ru.gravit.launchserver.command.auth.UUIDToUsernameCommand;
|
||||
import ru.gravit.launchserver.command.auth.UnbanCommand;
|
||||
import ru.gravit.launchserver.command.auth.UsernameToUUIDCommand;
|
||||
import ru.gravit.launchserver.command.basic.BuildCommand;
|
||||
import ru.gravit.launchserver.command.basic.ClearCommand;
|
||||
import ru.gravit.launchserver.command.basic.DebugCommand;
|
||||
import ru.gravit.launchserver.command.basic.GCCommand;
|
||||
import ru.gravit.launchserver.command.basic.HelpCommand;
|
||||
import ru.gravit.launchserver.command.basic.LogConnectionsCommand;
|
||||
import ru.gravit.launchserver.command.basic.ProguardCleanCommand;
|
||||
import ru.gravit.launchserver.command.basic.RebindCommand;
|
||||
import ru.gravit.launchserver.command.basic.RegenProguardDictCommand;
|
||||
import ru.gravit.launchserver.command.basic.RemoveMappingsProguardCommand;
|
||||
import ru.gravit.launchserver.command.basic.StopCommand;
|
||||
import ru.gravit.launchserver.command.basic.VersionCommand;
|
||||
import ru.gravit.launchserver.command.hash.DownloadAssetCommand;
|
||||
import ru.gravit.launchserver.command.hash.DownloadClientCommand;
|
||||
import ru.gravit.launchserver.command.hash.IndexAssetCommand;
|
||||
import ru.gravit.launchserver.command.hash.SyncBinariesCommand;
|
||||
import ru.gravit.launchserver.command.hash.SyncProfilesCommand;
|
||||
import ru.gravit.launchserver.command.hash.SyncUpdatesCommand;
|
||||
import ru.gravit.launchserver.command.hash.UnindexAssetCommand;
|
||||
import ru.gravit.launchserver.command.modules.LoadModuleCommand;
|
||||
import ru.gravit.launchserver.command.modules.ModulesCommand;
|
||||
|
||||
public abstract class CommandHandler implements Runnable {
|
||||
private static String[] parse(CharSequence line) throws CommandException {
|
||||
boolean quoted = false;
|
||||
boolean wasQuoted = false;
|
||||
|
||||
// Read line char by char
|
||||
Collection<String> result = new LinkedList<>();
|
||||
StringBuilder builder = new StringBuilder(100);
|
||||
for (int i = 0; i <= line.length(); i++) {
|
||||
boolean end = i >= line.length();
|
||||
char ch = end ? '\0' : line.charAt(i);
|
||||
|
||||
// Maybe we should read next argument?
|
||||
if (end || !quoted && Character.isWhitespace(ch)) {
|
||||
if (end && quoted)
|
||||
throw new CommandException("Quotes wasn't closed");
|
||||
|
||||
// Empty args are ignored (except if was quoted)
|
||||
if (wasQuoted || builder.length() > 0)
|
||||
result.add(builder.toString());
|
||||
|
||||
// Reset string builder
|
||||
wasQuoted = false;
|
||||
builder.setLength(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Append next char
|
||||
switch (ch) {
|
||||
case '"': // "abc"de, "abc""de" also allowed
|
||||
quoted = !quoted;
|
||||
wasQuoted = true;
|
||||
break;
|
||||
case '\\': // All escapes, including spaces etc
|
||||
if (i + 1 >= line.length())
|
||||
throw new CommandException("Escape character is not specified");
|
||||
char next = line.charAt(i + 1);
|
||||
builder.append(next);
|
||||
i++;
|
||||
break;
|
||||
default: // Default char, simply append
|
||||
builder.append(ch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return result as array
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private final Map<String, Command> commands = new ConcurrentHashMap<>(32);
|
||||
|
||||
protected CommandHandler(LaunchServer server) {
|
||||
// Register basic commands
|
||||
registerCommand("help", new HelpCommand(server));
|
||||
registerCommand("version", new VersionCommand(server));
|
||||
registerCommand("build", new BuildCommand(server));
|
||||
registerCommand("stop", new StopCommand(server));
|
||||
registerCommand("rebind", new RebindCommand(server));
|
||||
registerCommand("debug", new DebugCommand(server));
|
||||
registerCommand("clear", new ClearCommand(server));
|
||||
registerCommand("gc", new GCCommand(server));
|
||||
registerCommand("proguardClean", new ProguardCleanCommand(server));
|
||||
registerCommand("proguardDictRegen", new RegenProguardDictCommand(server));
|
||||
registerCommand("proguardMappingsRemove", new RemoveMappingsProguardCommand(server));
|
||||
registerCommand("logConnections", new LogConnectionsCommand(server));
|
||||
registerCommand("loadModule", new LoadModuleCommand(server));
|
||||
registerCommand("modules", new ModulesCommand(server));
|
||||
|
||||
// Register sync commands
|
||||
registerCommand("indexAsset", new IndexAssetCommand(server));
|
||||
registerCommand("unindexAsset", new UnindexAssetCommand(server));
|
||||
registerCommand("downloadAsset", new DownloadAssetCommand(server));
|
||||
registerCommand("downloadClient", new DownloadClientCommand(server));
|
||||
registerCommand("syncBinaries", new SyncBinariesCommand(server));
|
||||
registerCommand("syncUpdates", new SyncUpdatesCommand(server));
|
||||
registerCommand("syncProfiles", new SyncProfilesCommand(server));
|
||||
|
||||
// Register auth commands
|
||||
registerCommand("auth", new AuthCommand(server));
|
||||
registerCommand("usernameToUUID", new UsernameToUUIDCommand(server));
|
||||
registerCommand("uuidToUsername", new UUIDToUsernameCommand(server));
|
||||
registerCommand("ban", new BanCommand(server));
|
||||
registerCommand("unban", new UnbanCommand(server));
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public abstract void bell() throws IOException;
|
||||
|
||||
@LauncherAPI
|
||||
public abstract void clear() throws IOException;
|
||||
|
||||
@LauncherAPI
|
||||
public final Map<String, Command> commandsMap() {
|
||||
return Collections.unmodifiableMap(commands);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public final void eval(String line, boolean bell) {
|
||||
LogHelper.info("Command '%s'", line);
|
||||
|
||||
// Parse line to tokens
|
||||
String[] args;
|
||||
try {
|
||||
args = parse(line);
|
||||
} catch (Exception e) {
|
||||
LogHelper.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluate command
|
||||
eval(args, bell);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public final void eval(String[] args, boolean bell) {
|
||||
if (args.length == 0)
|
||||
return;
|
||||
|
||||
// Measure start time and invoke command
|
||||
Instant startTime = Instant.now();
|
||||
try {
|
||||
lookup(args[0]).invoke(Arrays.copyOfRange(args, 1, args.length));
|
||||
} catch (Exception e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
|
||||
// Bell if invocation took > 1s
|
||||
Instant endTime = Instant.now();
|
||||
if (bell && Duration.between(startTime, endTime).getSeconds() >= 5)
|
||||
try {
|
||||
bell();
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public final Command lookup(String name) throws CommandException {
|
||||
Command command = commands.get(name);
|
||||
if (command == null)
|
||||
throw new CommandException(String.format("Unknown command: '%s'", name));
|
||||
return command;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public abstract String readLine() throws IOException;
|
||||
|
||||
private void readLoop() throws IOException {
|
||||
for (String line = readLine(); line != null; line = readLine())
|
||||
eval(line, true);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public final void registerCommand(String name, Command command) {
|
||||
VerifyHelper.verifyIDName(name);
|
||||
VerifyHelper.putIfAbsent(commands, name, Objects.requireNonNull(command, "command"),
|
||||
String.format("Command has been already registered: '%s'", name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void run() {
|
||||
try {
|
||||
readLoop();
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package ru.gravit.launchserver.command.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jline.console.ConsoleReader;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper.Output;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
|
||||
public final class JLineCommandHandler extends CommandHandler {
|
||||
private final class JLineOutput implements Output {
|
||||
@Override
|
||||
public void println(String message) {
|
||||
try {
|
||||
reader.println(ConsoleReader.RESET_LINE + message);
|
||||
reader.drawLine();
|
||||
reader.flush();
|
||||
} catch (IOException ignored) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final ConsoleReader reader;
|
||||
|
||||
public JLineCommandHandler(LaunchServer server) throws IOException {
|
||||
super(server);
|
||||
|
||||
// Set reader
|
||||
reader = new ConsoleReader();
|
||||
reader.setExpandEvents(false);
|
||||
|
||||
// Replace writer
|
||||
LogHelper.removeStdOutput();
|
||||
LogHelper.addOutput(new JLineOutput());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bell() throws IOException {
|
||||
reader.beep();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() throws IOException {
|
||||
reader.clearScreen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readLine() throws IOException {
|
||||
return reader.readLine();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package ru.gravit.launchserver.command.handler;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
|
||||
public final class StdCommandHandler extends CommandHandler {
|
||||
private final BufferedReader reader;
|
||||
|
||||
public StdCommandHandler(LaunchServer server, boolean readCommands) {
|
||||
super(server);
|
||||
reader = readCommands ? IOHelper.newReader(System.in) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bell() {
|
||||
// Do nothing, unsupported
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
throw new UnsupportedOperationException("clear terminal");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readLine() throws IOException {
|
||||
return reader == null ? null : reader.readLine();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package ru.gravit.launchserver.command.hash;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.profiles.ClientProfile.Version;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class DownloadAssetCommand extends Command {
|
||||
private static final String ASSET_URL_MASK = "http://launcher.sashok724.net/download/assets/%s.zip";
|
||||
|
||||
public static void unpack(URL url, Path dir) throws IOException {
|
||||
try (ZipInputStream input = IOHelper.newZipInput(url)) {
|
||||
for (ZipEntry entry = input.getNextEntry(); entry != null; entry = input.getNextEntry()) {
|
||||
if (entry.isDirectory())
|
||||
continue; // Skip directories
|
||||
|
||||
// Unpack entry
|
||||
String name = entry.getName();
|
||||
LogHelper.subInfo("Downloading file: '%s'", name);
|
||||
IOHelper.transfer(input, dir.resolve(IOHelper.toPath(name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadAssetCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "<version> <dir>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Download asset dir";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
verifyArgs(args, 2);
|
||||
Version version = Version.byName(args[0]);
|
||||
String dirName = IOHelper.verifyFileName(args[1]);
|
||||
Path assetDir = server.updatesDir.resolve(dirName);
|
||||
|
||||
// Create asset dir
|
||||
LogHelper.subInfo("Creating asset dir: '%s'", dirName);
|
||||
Files.createDirectory(assetDir);
|
||||
|
||||
// Download required asset
|
||||
LogHelper.subInfo("Downloading asset, it may take some time");
|
||||
unpack(new URL(String.format(ASSET_URL_MASK, IOHelper.urlEncode(version.name))), assetDir);
|
||||
|
||||
// Finished
|
||||
server.syncUpdatesDir(Collections.singleton(dirName));
|
||||
LogHelper.subInfo("Asset successfully downloaded: '%s'", dirName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package ru.gravit.launchserver.command.hash;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.profiles.ClientProfile;
|
||||
import ru.gravit.launcher.profiles.ClientProfile.Version;
|
||||
import ru.gravit.launcher.serialize.config.TextConfigReader;
|
||||
import ru.gravit.launcher.serialize.config.TextConfigWriter;
|
||||
import ru.gravit.launcher.serialize.config.entry.StringConfigEntry;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
import ru.gravit.launchserver.command.CommandException;
|
||||
|
||||
public final class DownloadClientCommand extends Command {
|
||||
private static final String CLIENT_URL_MASK = "http://launcher.sashok724.net/download/clients/%s.zip";
|
||||
|
||||
public DownloadClientCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "<version> <dir>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Download client dir";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws IOException, CommandException {
|
||||
verifyArgs(args, 2);
|
||||
Version version = Version.byName(args[0]);
|
||||
String dirName = IOHelper.verifyFileName(args[1]);
|
||||
Path clientDir = server.updatesDir.resolve(args[1]);
|
||||
|
||||
// Create client dir
|
||||
LogHelper.subInfo("Creating client dir: '%s'", dirName);
|
||||
Files.createDirectory(clientDir);
|
||||
|
||||
// Download required client
|
||||
LogHelper.subInfo("Downloading client, it may take some time");
|
||||
DownloadAssetCommand.unpack(new URL(String.format(CLIENT_URL_MASK,
|
||||
IOHelper.urlEncode(version.name))), clientDir);
|
||||
|
||||
// Create profile file
|
||||
LogHelper.subInfo("Creaing profile file: '%s'", dirName);
|
||||
ClientProfile client;
|
||||
String profilePath = String.format("launchserver/defaults/profile%s.cfg", version.name);
|
||||
try (BufferedReader reader = IOHelper.newReader(IOHelper.getResourceURL(profilePath))) {
|
||||
client = new ClientProfile(TextConfigReader.read(reader, false));
|
||||
}
|
||||
client.setTitle(dirName);
|
||||
client.block.getEntry("dir", StringConfigEntry.class).setValue(dirName);
|
||||
try (BufferedWriter writer = IOHelper.newWriter(IOHelper.resolveIncremental(server.profilesDir,
|
||||
dirName, "cfg"))) {
|
||||
TextConfigWriter.write(client.block, writer, true);
|
||||
}
|
||||
|
||||
// Finished
|
||||
server.syncProfilesDir();
|
||||
server.syncUpdatesDir(Collections.singleton(dirName));
|
||||
LogHelper.subInfo("Client successfully downloaded: '%s'", dirName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package ru.gravit.launchserver.command.hash;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.eclipsesource.json.Json;
|
||||
import com.eclipsesource.json.JsonObject;
|
||||
import com.eclipsesource.json.WriterConfig;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper.DigestAlgorithm;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
import ru.gravit.launchserver.command.CommandException;
|
||||
|
||||
public final class IndexAssetCommand extends Command {
|
||||
private static final class IndexAssetVisitor extends SimpleFileVisitor<Path> {
|
||||
private final JsonObject objects;
|
||||
private final Path inputAssetDir;
|
||||
private final Path outputAssetDir;
|
||||
|
||||
private IndexAssetVisitor(JsonObject objects, Path inputAssetDir, Path outputAssetDir) {
|
||||
this.objects = objects;
|
||||
this.inputAssetDir = inputAssetDir;
|
||||
this.outputAssetDir = outputAssetDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
String name = IOHelper.toString(inputAssetDir.relativize(file));
|
||||
LogHelper.subInfo("Indexing: '%s'", name);
|
||||
|
||||
// Add to index and copy file
|
||||
String digest = SecurityHelper.toHex(SecurityHelper.digest(DigestAlgorithm.SHA1, file));
|
||||
objects.add(name, Json.object().add("size", attrs.size()).add("hash", digest));
|
||||
IOHelper.copy(file, resolveObjectFile(outputAssetDir, digest));
|
||||
|
||||
// Continue visiting
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
}
|
||||
public static final String INDEXES_DIR = "indexes";
|
||||
public static final String OBJECTS_DIR = "objects";
|
||||
|
||||
private static final String JSON_EXTENSION = ".json";
|
||||
|
||||
@LauncherAPI
|
||||
public static Path resolveIndexFile(Path assetDir, String name) {
|
||||
return assetDir.resolve(INDEXES_DIR).resolve(name + JSON_EXTENSION);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public static Path resolveObjectFile(Path assetDir, String hash) {
|
||||
return assetDir.resolve(OBJECTS_DIR).resolve(hash.substring(0, 2)).resolve(hash);
|
||||
}
|
||||
|
||||
public IndexAssetCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "<dir> <index> <output-dir>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Index asset dir (1.7.10+)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
verifyArgs(args, 3);
|
||||
String inputAssetDirName = IOHelper.verifyFileName(args[0]);
|
||||
String indexFileName = IOHelper.verifyFileName(args[1]);
|
||||
String outputAssetDirName = IOHelper.verifyFileName(args[2]);
|
||||
Path inputAssetDir = server.updatesDir.resolve(inputAssetDirName);
|
||||
Path outputAssetDir = server.updatesDir.resolve(outputAssetDirName);
|
||||
if (outputAssetDir.equals(inputAssetDir))
|
||||
throw new CommandException("Unindexed and indexed asset dirs can't be same");
|
||||
|
||||
// Create new asset dir
|
||||
LogHelper.subInfo("Creating indexed asset dir: '%s'", outputAssetDirName);
|
||||
Files.createDirectory(outputAssetDir);
|
||||
|
||||
// Index objects
|
||||
JsonObject objects = Json.object();
|
||||
LogHelper.subInfo("Indexing objects");
|
||||
IOHelper.walk(inputAssetDir, new IndexAssetVisitor(objects, inputAssetDir, outputAssetDir), false);
|
||||
|
||||
// Write index file
|
||||
LogHelper.subInfo("Writing asset index file: '%s'", indexFileName);
|
||||
try (BufferedWriter writer = IOHelper.newWriter(resolveIndexFile(outputAssetDir, indexFileName))) {
|
||||
Json.object().add(OBJECTS_DIR, objects).writeTo(writer, WriterConfig.MINIMAL);
|
||||
}
|
||||
|
||||
// Finished
|
||||
server.syncUpdatesDir(Collections.singleton(outputAssetDirName));
|
||||
LogHelper.subInfo("Asset successfully indexed: '%s'", inputAssetDirName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package ru.gravit.launchserver.command.hash;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class SyncBinariesCommand extends Command {
|
||||
public SyncBinariesCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Resync launcher binaries";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws IOException {
|
||||
server.syncLauncherBinaries();
|
||||
LogHelper.subInfo("Binaries successfully resynced");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package ru.gravit.launchserver.command.hash;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class SyncProfilesCommand extends Command {
|
||||
public SyncProfilesCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Resync profiles dir";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws IOException {
|
||||
server.syncProfilesDir();
|
||||
LogHelper.subInfo("Profiles successfully resynced");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package ru.gravit.launchserver.command.hash;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public final class SyncUpdatesCommand extends Command {
|
||||
public SyncUpdatesCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "[subdirs...]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Resync updates dir";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws IOException {
|
||||
Set<String> dirs = null;
|
||||
if (args.length > 0) { // Hash all updates dirs
|
||||
dirs = new HashSet<>(args.length);
|
||||
Collections.addAll(dirs, args);
|
||||
}
|
||||
|
||||
// Hash updates dir
|
||||
server.syncUpdatesDir(dirs);
|
||||
LogHelper.subInfo("Updates dir successfully resynced");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package ru.gravit.launchserver.command.hash;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.eclipsesource.json.Json;
|
||||
import com.eclipsesource.json.JsonObject;
|
||||
import com.eclipsesource.json.JsonObject.Member;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
import ru.gravit.launchserver.command.CommandException;
|
||||
|
||||
public final class UnindexAssetCommand extends Command {
|
||||
public UnindexAssetCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "<dir> <index> <output-dir>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Unindex asset dir (1.7.10+)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
verifyArgs(args, 3);
|
||||
String inputAssetDirName = IOHelper.verifyFileName(args[0]);
|
||||
String indexFileName = IOHelper.verifyFileName(args[1]);
|
||||
String outputAssetDirName = IOHelper.verifyFileName(args[2]);
|
||||
Path inputAssetDir = server.updatesDir.resolve(inputAssetDirName);
|
||||
Path outputAssetDir = server.updatesDir.resolve(outputAssetDirName);
|
||||
if (outputAssetDir.equals(inputAssetDir))
|
||||
throw new CommandException("Indexed and unindexed asset dirs can't be same");
|
||||
|
||||
// Create new asset dir
|
||||
LogHelper.subInfo("Creating unindexed asset dir: '%s'", outputAssetDirName);
|
||||
Files.createDirectory(outputAssetDir);
|
||||
|
||||
// Read JSON file
|
||||
JsonObject objects;
|
||||
LogHelper.subInfo("Reading asset index file: '%s'", indexFileName);
|
||||
try (BufferedReader reader = IOHelper.newReader(IndexAssetCommand.resolveIndexFile(inputAssetDir, indexFileName))) {
|
||||
objects = Json.parse(reader).asObject().get(IndexAssetCommand.OBJECTS_DIR).asObject();
|
||||
}
|
||||
|
||||
// Restore objects
|
||||
LogHelper.subInfo("Unindexing %d objects", objects.size());
|
||||
for (Member member : objects) {
|
||||
String name = member.getName();
|
||||
LogHelper.subInfo("Unindexing: '%s'", name);
|
||||
|
||||
// Copy hashed file to target
|
||||
String hash = member.getValue().asObject().get("hash").asString();
|
||||
Path source = IndexAssetCommand.resolveObjectFile(inputAssetDir, hash);
|
||||
IOHelper.copy(source, outputAssetDir.resolve(name));
|
||||
}
|
||||
|
||||
// Finished
|
||||
server.syncUpdatesDir(Collections.singleton(outputAssetDirName));
|
||||
LogHelper.subInfo("Asset successfully unindexed: '%s'", inputAssetDirName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package ru.gravit.launchserver.command.modules;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public class LoadModuleCommand extends Command {
|
||||
public LoadModuleCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return "[jar]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "Module jar file";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
verifyArgs(args, 1);
|
||||
URI uri = Paths.get(args[0]).toUri();
|
||||
server.modulesManager.loadModule(uri.toURL(), false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package ru.gravit.launchserver.command.modules;
|
||||
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.command.Command;
|
||||
|
||||
public class ModulesCommand extends Command {
|
||||
public ModulesCommand(LaunchServer server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArgsDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsageDescription() {
|
||||
return "get all modules";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(String... args) throws Exception {
|
||||
server.modulesManager.printModules();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package ru.gravit.launchserver.integration.plugin;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import ru.gravit.launcher.helper.JVMHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
|
||||
public final class LaunchServerPluginBridge implements Runnable, AutoCloseable {
|
||||
/**
|
||||
* Permission.
|
||||
*/
|
||||
public static final String perm = "launchserver.corecmdcall";
|
||||
/**
|
||||
* Err text.
|
||||
*/
|
||||
public static final String nonInitText = "Лаунчсервер не был полностью загружен";
|
||||
static {
|
||||
//SecurityHelper.verifyCertificates(LaunchServer.class);
|
||||
JVMHelper.verifySystemProperties(LaunchServer.class, false);
|
||||
}
|
||||
|
||||
private final LaunchServer server;
|
||||
|
||||
public LaunchServerPluginBridge(Path dir) throws Throwable {
|
||||
LogHelper.addOutput(dir.resolve("LaunchServer.log"));
|
||||
LogHelper.printVersion("LaunchServer");
|
||||
|
||||
// Create new LaunchServer
|
||||
Instant start = Instant.now();
|
||||
try {
|
||||
server = new LaunchServer(dir, true);
|
||||
} catch (Throwable exc) {
|
||||
LogHelper.error(exc);
|
||||
throw exc;
|
||||
}
|
||||
Instant end = Instant.now();
|
||||
LogHelper.debug("LaunchServer started in %dms", Duration.between(start, end).toMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
server.close();
|
||||
}
|
||||
|
||||
public void eval(String... command) {
|
||||
server.commandHandler.eval(command, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
server.run();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package ru.gravit.launchserver.integration.plugin.bukkit;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import ru.gravit.launchserver.integration.plugin.LaunchServerPluginBridge;
|
||||
|
||||
public final class LaunchServerCommandBukkit implements CommandExecutor {
|
||||
public final LaunchServerPluginBukkit plugin;
|
||||
|
||||
public LaunchServerCommandBukkit(LaunchServerPluginBukkit plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String alias, String[] args) {
|
||||
// Eval command
|
||||
LaunchServerPluginBridge bridge = plugin.bridge;
|
||||
if (bridge == null)
|
||||
sender.sendMessage(ChatColor.RED + LaunchServerPluginBridge.nonInitText);
|
||||
else
|
||||
bridge.eval(args);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package ru.gravit.launchserver.integration.plugin.bukkit;
|
||||
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import ru.gravit.launchserver.integration.plugin.LaunchServerPluginBridge;
|
||||
|
||||
public final class LaunchServerPluginBukkit extends JavaPlugin {
|
||||
public volatile LaunchServerPluginBridge bridge = null;
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
super.onDisable();
|
||||
if (bridge != null) {
|
||||
bridge.close();
|
||||
bridge = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
|
||||
// Initialize LaunchServer
|
||||
try {
|
||||
bridge = new LaunchServerPluginBridge(getDataFolder().toPath());
|
||||
} catch (Throwable exc) {
|
||||
exc.printStackTrace();
|
||||
}
|
||||
|
||||
// Register command
|
||||
PluginCommand com = getCommand("launchserver");
|
||||
com.setPermission(LaunchServerPluginBridge.perm);
|
||||
com.setExecutor(new LaunchServerCommandBukkit(this));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package ru.gravit.launchserver.integration.plugin.bungee;
|
||||
|
||||
import ru.gravit.launchserver.integration.plugin.LaunchServerPluginBridge;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
//import net.md_5.bungee.command.ConsoleCommandSender;
|
||||
|
||||
public final class LaunchServerCommandBungee extends Command {
|
||||
private static final BaseComponent[] NOT_INITIALIZED_MESSAGE = TextComponent.fromLegacyText(ChatColor.RED + LaunchServerPluginBridge.nonInitText);
|
||||
|
||||
// Instance
|
||||
public final LaunchServerPluginBungee plugin;
|
||||
|
||||
public LaunchServerCommandBungee(LaunchServerPluginBungee plugin) {
|
||||
super("launchserver", LaunchServerPluginBridge.perm, "ru/gravit/launcher", "ls", "l");
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
// Eval command
|
||||
LaunchServerPluginBridge bridge = plugin.bridge;
|
||||
if (bridge == null)
|
||||
sender.sendMessage(NOT_INITIALIZED_MESSAGE);
|
||||
else
|
||||
bridge.eval(args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ru.gravit.launchserver.integration.plugin.bungee;
|
||||
|
||||
import ru.gravit.launchserver.integration.plugin.LaunchServerPluginBridge;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
|
||||
public final class LaunchServerPluginBungee extends Plugin {
|
||||
public volatile LaunchServerPluginBridge bridge = null;
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
super.onDisable();
|
||||
if (bridge != null) {
|
||||
bridge.close();
|
||||
bridge = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
|
||||
// Initialize LaunchServer
|
||||
try {
|
||||
bridge = new LaunchServerPluginBridge(getDataFolder().toPath());
|
||||
} catch (Throwable exc) {
|
||||
exc.printStackTrace();
|
||||
}
|
||||
|
||||
// Register command
|
||||
getProxy().getPluginManager().registerCommand(this, new LaunchServerCommandBungee(this));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package ru.gravit.launchserver.manangers;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import ru.gravit.launcher.AutogenConfig;
|
||||
import ru.gravit.launcher.modules.TestClientModule;
|
||||
import ru.gravit.launchserver.binary.JAConfigurator;
|
||||
|
||||
public class BuildHookManager {
|
||||
@FunctionalInterface
|
||||
public interface PostBuildHook
|
||||
{
|
||||
void build(Map<String, byte[]> output);
|
||||
}
|
||||
@FunctionalInterface
|
||||
public interface PreBuildHook
|
||||
{
|
||||
void build(Map<String, byte[]> output);
|
||||
}
|
||||
@FunctionalInterface
|
||||
public interface Transformer
|
||||
{
|
||||
byte[] transform(byte[] input, CharSequence classname);
|
||||
}
|
||||
private boolean BUILDRUNTIME;
|
||||
private final Set<PostBuildHook> POST_HOOKS;
|
||||
private final Set<PreBuildHook> PRE_HOOKS;
|
||||
private final Set<Transformer> CLASS_TRANSFORMER;
|
||||
private final Set<String> CLASS_BLACKLIST;
|
||||
private final Set<String> MODULE_CLASS;
|
||||
private final Map<String, byte[]> INCLUDE_CLASS;
|
||||
public BuildHookManager() {
|
||||
POST_HOOKS = new HashSet<>(4);
|
||||
PRE_HOOKS = new HashSet<>(4);
|
||||
CLASS_BLACKLIST = new HashSet<>(4);
|
||||
MODULE_CLASS = new HashSet<>(4);
|
||||
INCLUDE_CLASS = new HashMap<>(4);
|
||||
CLASS_TRANSFORMER = new HashSet<>(4);
|
||||
BUILDRUNTIME = true;
|
||||
autoRegisterIgnoredClass(AutogenConfig.class.getName());
|
||||
registerIgnoredClass("META-INF/DEPENDENCIES");
|
||||
registerIgnoredClass("META-INF/LICENSE");
|
||||
registerIgnoredClass("META-INF/NOTICE");
|
||||
registerClientModuleClass(TestClientModule.class.getName());
|
||||
}
|
||||
public void autoRegisterIgnoredClass(String clazz)
|
||||
{
|
||||
CLASS_BLACKLIST.add(clazz.replace('.','/').concat(".class"));
|
||||
}
|
||||
public boolean buildRuntime() {
|
||||
return BUILDRUNTIME;
|
||||
}
|
||||
public byte[] classTransform(byte[] clazz, CharSequence classname)
|
||||
{
|
||||
byte[] result = clazz;
|
||||
for(Transformer transformer : CLASS_TRANSFORMER) result = transformer.transform(result,classname);
|
||||
return result;
|
||||
}
|
||||
public void registerIncludeClass(String classname, byte[] classdata) {
|
||||
INCLUDE_CLASS.put(classname, classdata);
|
||||
}
|
||||
public Map<String, byte[]> getIncludeClass() {
|
||||
return INCLUDE_CLASS;
|
||||
}
|
||||
public boolean isContainsBlacklist(String clazz)
|
||||
{
|
||||
return CLASS_BLACKLIST.contains(clazz);
|
||||
}
|
||||
public void postHook(Map<String, byte[]> output)
|
||||
{
|
||||
for(PostBuildHook hook : POST_HOOKS) hook.build(output);
|
||||
}
|
||||
public void preHook(Map<String, byte[]> output)
|
||||
{
|
||||
for(PreBuildHook hook : PRE_HOOKS) hook.build(output);
|
||||
}
|
||||
public void registerAllClientModuleClass(JAConfigurator cfg)
|
||||
{
|
||||
for(String clazz : MODULE_CLASS) cfg.addModuleClass(clazz);
|
||||
}
|
||||
public void registerClassTransformer(Transformer transformer)
|
||||
{
|
||||
CLASS_TRANSFORMER.add(transformer);
|
||||
}
|
||||
public void registerClientModuleClass(String clazz)
|
||||
{
|
||||
MODULE_CLASS.add(clazz);
|
||||
}
|
||||
public void registerIgnoredClass(String clazz)
|
||||
{
|
||||
CLASS_BLACKLIST.add(clazz);
|
||||
}
|
||||
public void registerPostHook(PostBuildHook hook)
|
||||
{
|
||||
POST_HOOKS.add(hook);
|
||||
}
|
||||
public void registerPreHook(PreBuildHook hook)
|
||||
{
|
||||
PRE_HOOKS.add(hook);
|
||||
}
|
||||
public void setBuildRuntime(boolean runtime) {
|
||||
BUILDRUNTIME = runtime;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
package ru.gravit.launchserver.manangers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.LauncherClassLoader;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.modules.Module;
|
||||
import ru.gravit.launcher.modules.ModulesManagerInterface;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.modules.CoreModule;
|
||||
import ru.gravit.launchserver.modules.LaunchServerModuleContext;
|
||||
|
||||
public class ModulesManager implements AutoCloseable, ModulesManagerInterface {
|
||||
private final class ModulesVisitor extends SimpleFileVisitor<Path> {
|
||||
private ModulesVisitor() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
try {
|
||||
JarFile f = new JarFile(file.toString());
|
||||
Manifest m = f.getManifest();
|
||||
String mainclass = m.getMainAttributes().getValue("Main-Class");
|
||||
loadModule(file.toUri().toURL(), mainclass, true);
|
||||
f.close();
|
||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// Return result
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<Module> modules;
|
||||
public LauncherClassLoader classloader;
|
||||
|
||||
private final LaunchServerModuleContext context;
|
||||
|
||||
public ModulesManager(LaunchServer lsrv) {
|
||||
modules = new ArrayList<>(1);
|
||||
classloader = new LauncherClassLoader(new URL[0], ClassLoader.getSystemClassLoader());
|
||||
context = new LaunchServerModuleContext(lsrv, classloader);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void autoload() throws IOException {
|
||||
LogHelper.info("Load modules");
|
||||
registerCoreModule();
|
||||
Path modules = context.launchServer.dir.resolve("modules");
|
||||
if (Files.notExists(modules))
|
||||
Files.createDirectory(modules);
|
||||
IOHelper.walk(modules, new ModulesVisitor(), true);
|
||||
LogHelper.info("Loaded %d modules", this.modules.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
for (Module m : modules)
|
||||
try {
|
||||
m.close();
|
||||
} catch (Throwable t) {
|
||||
if (m.getName() != null)
|
||||
LogHelper.error("Error in stopping module: %s", m.getName());
|
||||
else
|
||||
LogHelper.error("Error in stopping one of modules");
|
||||
LogHelper.error(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@LauncherAPI
|
||||
public void initModules() {
|
||||
for (Module m : modules) {
|
||||
m.init(context);
|
||||
LogHelper.info("Module %s version: %s init", m.getName(), m.getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@LauncherAPI
|
||||
public void load(Module module) {
|
||||
modules.add(module);
|
||||
}
|
||||
|
||||
@Override
|
||||
@LauncherAPI
|
||||
public void load(Module module, boolean preload) {
|
||||
load(module);
|
||||
if (!preload)
|
||||
module.init(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@LauncherAPI
|
||||
public void loadModule(URL jarpath, boolean preload) throws ClassNotFoundException, IllegalAccessException,
|
||||
InstantiationException, URISyntaxException, IOException {
|
||||
JarFile f = new JarFile(Paths.get(jarpath.toURI()).toString());
|
||||
Manifest m = f.getManifest();
|
||||
String mainclass = m.getMainAttributes().getValue("Main-Class");
|
||||
loadModule(jarpath, mainclass, preload);
|
||||
f.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
@LauncherAPI
|
||||
public void loadModule(URL jarpath, String classname, boolean preload)
|
||||
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
||||
classloader.addURL(jarpath);
|
||||
Class<?> moduleclass = Class.forName(classname, true, classloader);
|
||||
Module module = (Module) moduleclass.newInstance();
|
||||
modules.add(module);
|
||||
module.preInit(context);
|
||||
if (!preload)
|
||||
module.init(context);
|
||||
LogHelper.info("Module %s version: %s loaded", module.getName(), module.getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInitModules() {
|
||||
for (Module m : modules) {
|
||||
m.postInit(context);
|
||||
LogHelper.info("Module %s version: %s post-init", m.getName(), m.getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@LauncherAPI
|
||||
public void preInitModules() {
|
||||
for (Module m : modules) {
|
||||
m.preInit(context);
|
||||
LogHelper.info("Module %s version: %s pre-init", m.getName(), m.getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@LauncherAPI
|
||||
public void printModules() {
|
||||
for (Module m : modules)
|
||||
LogHelper.info("Module %s version: %s", m.getName(), m.getVersion());
|
||||
LogHelper.info("Loaded %d modules", modules.size());
|
||||
}
|
||||
|
||||
private void registerCoreModule() {
|
||||
load(new CoreModule());
|
||||
}
|
||||
|
||||
@Override
|
||||
@LauncherAPI
|
||||
public void registerModule(Module module, boolean preload) {
|
||||
load(module, preload);
|
||||
LogHelper.info("Module %s version: %s registered", module.getName(), module.getVersion());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package ru.gravit.launchserver.manangers;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.NeedGarbageCollection;
|
||||
import ru.gravit.launchserver.socket.Client;
|
||||
|
||||
public class SessionManager implements NeedGarbageCollection {
|
||||
@LauncherAPI
|
||||
public static final long SESSION_TIMEOUT = 10 * 60 * 1000; // 10 минут
|
||||
private Set<Client> clientSet = new HashSet<>(128);
|
||||
|
||||
@LauncherAPI
|
||||
public boolean addClient(Client client) {
|
||||
clientSet.add(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@LauncherAPI
|
||||
public void garbageCollection() {
|
||||
long time = System.currentTimeMillis();
|
||||
clientSet.removeIf(c -> c.timestamp + SESSION_TIMEOUT < time);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public Client getClient(long session) {
|
||||
for (Client c : clientSet)
|
||||
if (c.session == session) return c;
|
||||
return null;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public Client getOrNewClient(long session) {
|
||||
for (Client c : clientSet)
|
||||
if (c.session == session) return c;
|
||||
Client newClient = new Client(session);
|
||||
clientSet.add(newClient);
|
||||
return newClient;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public void updateClient(long session) {
|
||||
for (Client c : clientSet)
|
||||
if (c.session == session) {
|
||||
c.up();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package ru.gravit.launchserver.modules;
|
||||
|
||||
import ru.gravit.launcher.LauncherVersion;
|
||||
import ru.gravit.launcher.modules.Module;
|
||||
import ru.gravit.launcher.modules.ModuleContext;
|
||||
|
||||
public class CoreModule implements Module {
|
||||
@Override
|
||||
public void close() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "LaunchServer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public LauncherVersion getVersion() {
|
||||
return LauncherVersion.getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ModuleContext context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(ModuleContext context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void preInit(ModuleContext context) {
|
||||
// nothing to do
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package ru.gravit.launchserver.modules;
|
||||
|
||||
import ru.gravit.launcher.LauncherClassLoader;
|
||||
import ru.gravit.launcher.modules.ModuleContext;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
|
||||
public class LaunchServerModuleContext implements ModuleContext {
|
||||
public final LaunchServer launchServer;
|
||||
public final LauncherClassLoader classloader;
|
||||
public LaunchServerModuleContext(LaunchServer server, LauncherClassLoader classloader)
|
||||
{
|
||||
launchServer = server;
|
||||
this.classloader = classloader;
|
||||
}
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.LAUNCHSERVER;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package ru.gravit.launchserver.modules;
|
||||
|
||||
import ru.gravit.launcher.LauncherVersion;
|
||||
import ru.gravit.launcher.modules.Module;
|
||||
import ru.gravit.launcher.modules.ModuleContext;
|
||||
|
||||
public class SimpleModule implements Module {
|
||||
@Override
|
||||
public void close() {
|
||||
// on stop
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "SimpleModule";
|
||||
}
|
||||
|
||||
@Override
|
||||
public LauncherVersion getVersion() {
|
||||
return new LauncherVersion(1,0,0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ModuleContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(ModuleContext context) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void preInit(ModuleContext context) {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package ru.gravit.launchserver.response;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.SerializeLimits;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
|
||||
public final class PingResponse extends Response {
|
||||
public PingResponse(LaunchServer server, long id, HInput input, HOutput output, String ip) {
|
||||
super(server, id, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws IOException {
|
||||
output.writeUnsignedByte(SerializeLimits.EXPECTED_BYTE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package ru.gravit.launchserver.response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import ru.gravit.launcher.LauncherAPI;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.request.RequestException;
|
||||
import ru.gravit.launcher.request.RequestType;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.response.auth.AuthResponse;
|
||||
import ru.gravit.launchserver.response.auth.CheckServerResponse;
|
||||
import ru.gravit.launchserver.response.auth.JoinServerResponse;
|
||||
import ru.gravit.launchserver.response.profile.BatchProfileByUsernameResponse;
|
||||
import ru.gravit.launchserver.response.profile.ProfileByUUIDResponse;
|
||||
import ru.gravit.launchserver.response.profile.ProfileByUsernameResponse;
|
||||
import ru.gravit.launchserver.response.update.LauncherResponse;
|
||||
import ru.gravit.launchserver.response.update.ProfilesResponse;
|
||||
import ru.gravit.launchserver.response.update.UpdateListResponse;
|
||||
import ru.gravit.launchserver.response.update.UpdateResponse;
|
||||
|
||||
public abstract class Response {
|
||||
@FunctionalInterface
|
||||
public interface Factory<R> {
|
||||
@LauncherAPI
|
||||
Response newResponse(LaunchServer server, long id, HInput input, HOutput output, String ip);
|
||||
}
|
||||
private static final Map<Integer, Factory<?>> RESPONSES = new ConcurrentHashMap<>(8);
|
||||
public static Response getResponse(int type, LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
return RESPONSES.get(type).newResponse(server, session, input, output, ip);
|
||||
}
|
||||
public static void registerResponse(int type, Factory<?> factory) {
|
||||
RESPONSES.put(type, factory);
|
||||
}
|
||||
public static void registerResponses() {
|
||||
registerResponse(RequestType.PING.getNumber(), PingResponse::new);
|
||||
registerResponse(RequestType.AUTH.getNumber(), AuthResponse::new);
|
||||
registerResponse(RequestType.CHECK_SERVER.getNumber(), CheckServerResponse::new);
|
||||
registerResponse(RequestType.JOIN_SERVER.getNumber(), JoinServerResponse::new);
|
||||
|
||||
registerResponse(RequestType.BATCH_PROFILE_BY_USERNAME.getNumber(), BatchProfileByUsernameResponse::new);
|
||||
registerResponse(RequestType.PROFILE_BY_USERNAME.getNumber(), ProfileByUsernameResponse::new);
|
||||
registerResponse(RequestType.PROFILE_BY_UUID.getNumber(), ProfileByUUIDResponse::new);
|
||||
|
||||
registerResponse(RequestType.LAUNCHER.getNumber(), LauncherResponse::new);
|
||||
registerResponse(RequestType.UPDATE_LIST.getNumber(), UpdateListResponse::new);
|
||||
registerResponse(RequestType.UPDATE.getNumber(), UpdateResponse::new);
|
||||
registerResponse(RequestType.PROFILES.getNumber(), ProfilesResponse::new);
|
||||
}
|
||||
@LauncherAPI
|
||||
public static void requestError(String message) throws RequestException {
|
||||
throw new RequestException(message);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected final LaunchServer server;
|
||||
|
||||
@LauncherAPI
|
||||
protected final HInput input;
|
||||
|
||||
@LauncherAPI
|
||||
protected final HOutput output;
|
||||
|
||||
@LauncherAPI
|
||||
protected final String ip;
|
||||
|
||||
@LauncherAPI
|
||||
protected final long session;
|
||||
|
||||
protected Response(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
this.server = server;
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
this.ip = ip;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected final void debug(String message) {
|
||||
LogHelper.subDebug("#%d %s", session, message);
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
protected final void debug(String message, Object... args) {
|
||||
debug(String.format(message, args));
|
||||
}
|
||||
|
||||
@LauncherAPI
|
||||
public abstract void reply() throws Exception;
|
||||
|
||||
@LauncherAPI
|
||||
@SuppressWarnings("MethodMayBeStatic") // Intentionally not static
|
||||
protected final void writeNoError(HOutput output) throws IOException {
|
||||
output.writeString("", 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package ru.gravit.launchserver.response.auth;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.profiles.ClientProfile;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.SerializeLimits;
|
||||
import ru.gravit.launcher.serialize.signed.SignedObjectHolder;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.auth.AuthException;
|
||||
import ru.gravit.launchserver.auth.hwid.HWID;
|
||||
import ru.gravit.launchserver.auth.hwid.HWIDException;
|
||||
import ru.gravit.launchserver.auth.provider.AuthProvider;
|
||||
import ru.gravit.launchserver.auth.provider.AuthProviderResult;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
import ru.gravit.launchserver.response.profile.ProfileByUUIDResponse;
|
||||
|
||||
public final class AuthResponse extends Response {
|
||||
private static String echo(int length) {
|
||||
char[] chars = new char[length];
|
||||
Arrays.fill(chars, '*');
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
public AuthResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws Exception {
|
||||
String login = input.readString(SerializeLimits.MAX_LOGIN);
|
||||
String client = input.readString(SerializeLimits.MAX_CLIENT);
|
||||
long hwid_hdd = input.readLong();
|
||||
long hwid_cpu = input.readLong();
|
||||
long hwid_bios = input.readLong();
|
||||
byte[] encryptedPassword = input.readByteArray(SecurityHelper.CRYPTO_MAX_LENGTH);
|
||||
// Decrypt password
|
||||
String password;
|
||||
try {
|
||||
password = IOHelper.decode(SecurityHelper.newRSADecryptCipher(server.privateKey).
|
||||
doFinal(encryptedPassword));
|
||||
} catch (IllegalBlockSizeException | BadPaddingException ignored) {
|
||||
requestError("Password decryption error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
debug("Login: '%s', Password: '%s'", login, echo(password.length()));
|
||||
AuthProviderResult result;
|
||||
try {
|
||||
if (server.limiter.isLimit(ip)) {
|
||||
AuthProvider.authError(server.config.authRejectString);
|
||||
return;
|
||||
}
|
||||
result = server.config.authProvider.auth(login, password, ip);
|
||||
if (!VerifyHelper.isValidUsername(result.username)) {
|
||||
AuthProvider.authError(String.format("Illegal result: '%s'", result.username));
|
||||
return;
|
||||
}
|
||||
Collection<SignedObjectHolder<ClientProfile>> profiles = server.getProfiles();
|
||||
for(SignedObjectHolder<ClientProfile> p : profiles)
|
||||
if(p.object.getTitle().equals(client))
|
||||
if(!p.object.isWhitelistContains(login))
|
||||
throw new AuthException(server.config.whitelistRejectString);
|
||||
server.config.hwidHandler.check(HWID.gen(hwid_hdd, hwid_bios, hwid_cpu), result.username);
|
||||
} catch (AuthException e) {
|
||||
requestError(e.getMessage());
|
||||
return;
|
||||
} catch (HWIDException e) {
|
||||
requestError(e.getMessage());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
LogHelper.error(e);
|
||||
requestError("Internal auth provider error");
|
||||
return;
|
||||
}
|
||||
debug("Auth: '%s' -> '%s', '%s'", login, result.username, result.accessToken);
|
||||
// Authenticate on server (and get UUID)
|
||||
UUID uuid;
|
||||
try {
|
||||
uuid = server.config.authHandler.auth(result);
|
||||
} catch (AuthException e) {
|
||||
requestError(e.getMessage());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
LogHelper.error(e);
|
||||
requestError("Internal auth handler error");
|
||||
return;
|
||||
}
|
||||
writeNoError(output);
|
||||
// Write profile and UUID
|
||||
ProfileByUUIDResponse.getProfile(server, uuid, result.username, client).write(output);
|
||||
output.writeASCII(result.accessToken, -SecurityHelper.TOKEN_STRING_LENGTH);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package ru.gravit.launchserver.response.auth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.SerializeLimits;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.auth.AuthException;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
import ru.gravit.launchserver.response.profile.ProfileByUUIDResponse;
|
||||
|
||||
public final class CheckServerResponse extends Response {
|
||||
|
||||
public CheckServerResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws IOException {
|
||||
String username = VerifyHelper.verifyUsername(input.readString(SerializeLimits.MAX_LOGIN));
|
||||
String serverID = VerifyHelper.verifyServerID(input.readASCII(41)); // With minus sign
|
||||
String client = input.readString(SerializeLimits.MAX_CLIENT);
|
||||
debug("Username: %s, Server ID: %s", username, serverID);
|
||||
|
||||
// Try check server with auth handler
|
||||
UUID uuid;
|
||||
try {
|
||||
uuid = server.config.authHandler.checkServer(username, serverID);
|
||||
} catch (AuthException e) {
|
||||
requestError(e.getMessage());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
LogHelper.error(e);
|
||||
requestError("Internal auth handler error");
|
||||
return;
|
||||
}
|
||||
writeNoError(output);
|
||||
|
||||
// Write profile and UUID
|
||||
output.writeBoolean(uuid != null);
|
||||
if (uuid != null)
|
||||
ProfileByUUIDResponse.getProfile(server, uuid, username, client).write(output);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package ru.gravit.launchserver.response.auth;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.SerializeLimits;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.auth.AuthException;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
|
||||
public final class JoinServerResponse extends Response {
|
||||
|
||||
public JoinServerResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws IOException {
|
||||
String username = VerifyHelper.verifyUsername(input.readString(SerializeLimits.MAX_LOGIN));
|
||||
String accessToken = SecurityHelper.verifyToken(input.readASCII(-SecurityHelper.TOKEN_STRING_LENGTH));
|
||||
String serverID = VerifyHelper.verifyServerID(input.readASCII(SerializeLimits.MAX_SERVERID)); // With minus sign
|
||||
|
||||
// Try join server with auth handler
|
||||
debug("Username: '%s', Access token: %s, Server ID: %s", username, accessToken, serverID);
|
||||
boolean success;
|
||||
try {
|
||||
success = server.config.authHandler.joinServer(username, accessToken, serverID);
|
||||
} catch (AuthException e) {
|
||||
requestError(e.getMessage());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
LogHelper.error(e);
|
||||
requestError("Internal auth handler error");
|
||||
return;
|
||||
}
|
||||
writeNoError(output);
|
||||
|
||||
// Write response
|
||||
output.writeBoolean(success);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package ru.gravit.launchserver.response.profile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.SerializeLimits;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
|
||||
public final class BatchProfileByUsernameResponse extends Response {
|
||||
|
||||
public BatchProfileByUsernameResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws IOException {
|
||||
int length = input.readLength(SerializeLimits.MAX_BATCH_SIZE);
|
||||
String[] usernames = new String[length];
|
||||
String[] clients = new String[length];
|
||||
for (int i = 0; i < usernames.length; i++) {
|
||||
usernames[i] = VerifyHelper.verifyUsername(input.readString(64));
|
||||
clients[i] = input.readString(SerializeLimits.MAX_CLIENT);
|
||||
}
|
||||
debug("Usernames: " + Arrays.toString(usernames));
|
||||
|
||||
// Respond with profiles array
|
||||
for (int i = 0; i < usernames.length; i++)
|
||||
ProfileByUsernameResponse.writeProfile(server, output, usernames[i], clients[i]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package ru.gravit.launchserver.response.profile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.profiles.PlayerProfile;
|
||||
import ru.gravit.launcher.profiles.Texture;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.SerializeLimits;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
|
||||
public final class ProfileByUUIDResponse extends Response {
|
||||
|
||||
public static PlayerProfile getProfile(LaunchServer server, UUID uuid, String username, String client) {
|
||||
// Get skin texture
|
||||
Texture skin;
|
||||
try {
|
||||
skin = server.config.textureProvider.getSkinTexture(uuid, username, client);
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(new IOException(String.format("Can't get skin texture: '%s'", username), e));
|
||||
skin = null;
|
||||
}
|
||||
|
||||
// Get cloak texture
|
||||
Texture cloak;
|
||||
try {
|
||||
cloak = server.config.textureProvider.getCloakTexture(uuid, username, client);
|
||||
} catch (IOException e) {
|
||||
LogHelper.error(new IOException(String.format("Can't get cloak texture: '%s'", username), e));
|
||||
cloak = null;
|
||||
}
|
||||
|
||||
// Return combined profile
|
||||
return new PlayerProfile(uuid, username, skin, cloak);
|
||||
}
|
||||
|
||||
public ProfileByUUIDResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws IOException {
|
||||
UUID uuid = input.readUUID();
|
||||
debug("UUID: " + uuid);
|
||||
String client = input.readString(SerializeLimits.MAX_CLIENT);
|
||||
// Verify has such profile
|
||||
String username = server.config.authHandler.uuidToUsername(uuid);
|
||||
if (username == null) {
|
||||
output.writeBoolean(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write profile
|
||||
output.writeBoolean(true);
|
||||
getProfile(server, uuid, username, client).write(output);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package ru.gravit.launchserver.response.profile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import ru.gravit.launcher.helper.VerifyHelper;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.SerializeLimits;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
|
||||
public final class ProfileByUsernameResponse extends Response {
|
||||
|
||||
public static void writeProfile(LaunchServer server, HOutput output, String username, String client) throws IOException {
|
||||
UUID uuid = server.config.authHandler.usernameToUUID(username);
|
||||
if (uuid == null) {
|
||||
output.writeBoolean(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write profile
|
||||
output.writeBoolean(true);
|
||||
ProfileByUUIDResponse.getProfile(server, uuid, username, client).write(output);
|
||||
}
|
||||
|
||||
public ProfileByUsernameResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws IOException {
|
||||
String username = VerifyHelper.verifyUsername(input.readString(64));
|
||||
debug("Username: " + username);
|
||||
String client = input.readString(SerializeLimits.MAX_CLIENT);
|
||||
// Write response
|
||||
writeProfile(server, output, username, client);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package ru.gravit.launchserver.response.update;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.profiles.ClientProfile;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.signed.SignedBytesHolder;
|
||||
import ru.gravit.launcher.serialize.signed.SignedObjectHolder;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
|
||||
public final class LauncherResponse extends Response {
|
||||
|
||||
public LauncherResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws IOException {
|
||||
// Resolve launcher binary
|
||||
SignedBytesHolder bytes = (input.readBoolean() ? server.launcherEXEBinary : server.launcherBinary).getBytes();
|
||||
if (bytes == null) {
|
||||
requestError("Missing launcher binary");
|
||||
return;
|
||||
}
|
||||
writeNoError(output);
|
||||
|
||||
// Update launcher binary
|
||||
output.writeByteArray(bytes.getSign(), -SecurityHelper.RSA_KEY_LENGTH);
|
||||
output.flush();
|
||||
if (input.readBoolean()) {
|
||||
output.writeByteArray(bytes.getBytes(), 0);
|
||||
return; // Launcher will be restarted
|
||||
}
|
||||
|
||||
// Write clients profiles list
|
||||
Collection<SignedObjectHolder<ClientProfile>> profiles = server.getProfiles();
|
||||
output.writeLength(profiles.size(), 0);
|
||||
for (SignedObjectHolder<ClientProfile> profile : profiles)
|
||||
profile.write(output);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ru.gravit.launchserver.response.update;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.profiles.ClientProfile;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.signed.SignedObjectHolder;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
|
||||
public final class ProfilesResponse extends Response {
|
||||
|
||||
public ProfilesResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws IOException {
|
||||
// Resolve launcher binary
|
||||
input.readBoolean();
|
||||
writeNoError(output);
|
||||
Collection<SignedObjectHolder<ClientProfile>> profiles = server.getProfiles();
|
||||
output.writeLength(profiles.size(), 0);
|
||||
for (SignedObjectHolder<ClientProfile> profile : profiles) {
|
||||
LogHelper.debug("Writted profile: %s",profile.object.getTitle());
|
||||
profile.write(output);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package ru.gravit.launchserver.response.update;
|
||||
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import ru.gravit.launcher.hasher.HashedDir;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.signed.SignedObjectHolder;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
|
||||
public final class UpdateListResponse extends Response {
|
||||
|
||||
public UpdateListResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws Exception {
|
||||
Set<Entry<String, SignedObjectHolder<HashedDir>>> updateDirs = server.getUpdateDirs();
|
||||
|
||||
// Write all update dirs names
|
||||
output.writeLength(updateDirs.size(), 0);
|
||||
for (Entry<String, SignedObjectHolder<HashedDir>> entry : updateDirs)
|
||||
output.writeString(entry.getKey(), 255);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package ru.gravit.launchserver.response.update;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
import ru.gravit.launcher.hasher.HashedDir;
|
||||
import ru.gravit.launcher.hasher.HashedEntry;
|
||||
import ru.gravit.launcher.hasher.HashedEntry.Type;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.request.UpdateAction;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launcher.serialize.SerializeLimits;
|
||||
import ru.gravit.launcher.serialize.signed.SignedObjectHolder;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
|
||||
public final class UpdateResponse extends Response {
|
||||
|
||||
public UpdateResponse(LaunchServer server, long session, HInput input, HOutput output, String ip) {
|
||||
super(server, session, input, output, ip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reply() throws IOException {
|
||||
// Read update dir name
|
||||
String updateDirName = IOHelper.verifyFileName(input.readString(255));
|
||||
SignedObjectHolder<HashedDir> hdir = server.getUpdateDir(updateDirName);
|
||||
if (hdir == null) {
|
||||
requestError(String.format("Unknown update dir: %s", updateDirName));
|
||||
return;
|
||||
}
|
||||
writeNoError(output);
|
||||
|
||||
// Write update hdir
|
||||
debug("Update dir: '%s'", updateDirName);
|
||||
hdir.write(output);
|
||||
output.writeBoolean(server.config.compress);
|
||||
output.flush();
|
||||
|
||||
// Prepare variables for actions queue
|
||||
Path dir = server.updatesDir.resolve(updateDirName);
|
||||
Deque<HashedDir> dirStack = new LinkedList<>();
|
||||
dirStack.add(hdir.object);
|
||||
|
||||
// Perform update
|
||||
// noinspection IOResourceOpenedButNotSafelyClosed
|
||||
OutputStream fileOutput = server.config.compress ? new DeflaterOutputStream(output.stream, IOHelper.newDeflater(), IOHelper.BUFFER_SIZE, true) : output.stream;
|
||||
UpdateAction[] actionsSlice = new UpdateAction[SerializeLimits.MAX_QUEUE_SIZE];
|
||||
loop:
|
||||
while (true) {
|
||||
// Read actions slice
|
||||
int length = input.readLength(actionsSlice.length);
|
||||
for (int i = 0; i < length; i++)
|
||||
actionsSlice[i] = new UpdateAction(input);
|
||||
|
||||
// Perform actions
|
||||
for (int i = 0; i < length; i++) {
|
||||
UpdateAction action = actionsSlice[i];
|
||||
switch (action.type) {
|
||||
case CD:
|
||||
debug("CD '%s'", action.name);
|
||||
|
||||
// Get hashed dir (for validation)
|
||||
HashedEntry hSubdir = dirStack.getLast().getEntry(action.name);
|
||||
if (hSubdir == null || hSubdir.getType() != Type.DIR)
|
||||
throw new IOException("Unknown hashed dir: " + action.name);
|
||||
dirStack.add((HashedDir) hSubdir);
|
||||
|
||||
// Resolve dir
|
||||
dir = dir.resolve(action.name);
|
||||
break;
|
||||
case GET:
|
||||
debug("GET '%s'", action.name);
|
||||
|
||||
// Get hashed file (for validation)
|
||||
HashedEntry hFile = dirStack.getLast().getEntry(action.name);
|
||||
if (hFile == null || hFile.getType() != Type.FILE)
|
||||
throw new IOException("Unknown hashed file: " + action.name);
|
||||
|
||||
// Resolve and write file
|
||||
Path file = dir.resolve(action.name);
|
||||
if (IOHelper.readAttributes(file).size() != hFile.size()) {
|
||||
fileOutput.write(0x0);
|
||||
fileOutput.flush();
|
||||
throw new IOException("Unknown hashed file: " + action.name);
|
||||
}
|
||||
fileOutput.write(0xFF);
|
||||
try (InputStream fileInput = IOHelper.newInput(file)) {
|
||||
IOHelper.transfer(fileInput, fileOutput);
|
||||
}
|
||||
break;
|
||||
case CD_BACK:
|
||||
debug("CD ..");
|
||||
|
||||
// Remove from hashed dir stack
|
||||
dirStack.removeLast();
|
||||
if (dirStack.isEmpty())
|
||||
throw new IOException("Empty hDir stack");
|
||||
|
||||
// Get parent
|
||||
dir = dir.getParent();
|
||||
break;
|
||||
case FINISH:
|
||||
break loop;
|
||||
default:
|
||||
throw new AssertionError(String.format("Unsupported action type: '%s'", action.type.name()));
|
||||
}
|
||||
}
|
||||
|
||||
// Flush all actions
|
||||
fileOutput.flush();
|
||||
}
|
||||
|
||||
// So we've updated :)
|
||||
if (fileOutput instanceof DeflaterOutputStream)
|
||||
((DeflaterOutputStream) fileOutput).finish();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package ru.gravit.launchserver.socket;
|
||||
|
||||
public class Client {
|
||||
public long session;
|
||||
|
||||
public long timestamp;
|
||||
public Client(long session) {
|
||||
this.session = session;
|
||||
timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void up() {
|
||||
timestamp = System.currentTimeMillis();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package ru.gravit.launchserver.socket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
|
||||
import ru.gravit.launcher.Launcher;
|
||||
import ru.gravit.launcher.helper.IOHelper;
|
||||
import ru.gravit.launcher.helper.LogHelper;
|
||||
import ru.gravit.launcher.helper.SecurityHelper;
|
||||
import ru.gravit.launcher.request.RequestException;
|
||||
import ru.gravit.launcher.serialize.HInput;
|
||||
import ru.gravit.launcher.serialize.HOutput;
|
||||
import ru.gravit.launchserver.LaunchServer;
|
||||
import ru.gravit.launchserver.manangers.SessionManager;
|
||||
import ru.gravit.launchserver.response.Response;
|
||||
|
||||
public final class ResponseThread implements Runnable {
|
||||
class Handshake {
|
||||
int type;
|
||||
long session;
|
||||
|
||||
public Handshake(int type, long session) {
|
||||
this.type = type;
|
||||
this.session = session;
|
||||
}
|
||||
}
|
||||
private final LaunchServer server;
|
||||
private final Socket socket;
|
||||
|
||||
private final SessionManager sessions;
|
||||
|
||||
public ResponseThread(LaunchServer server, long id, Socket socket, SessionManager sessionManager) throws SocketException {
|
||||
this.server = server;
|
||||
this.socket = socket;
|
||||
sessions = sessionManager;
|
||||
// Fix socket flags
|
||||
IOHelper.setSocketFlags(socket);
|
||||
}
|
||||
|
||||
private Handshake readHandshake(HInput input, HOutput output) throws IOException {
|
||||
boolean legacy = false;
|
||||
long session = 0;
|
||||
// Verify magic number
|
||||
int magicNumber = input.readInt();
|
||||
if (magicNumber != Launcher.PROTOCOL_MAGIC)
|
||||
if (magicNumber == Launcher.PROTOCOL_MAGIC_LEGACY - 1) { // Previous launcher protocol
|
||||
session = 0;
|
||||
legacy = true;
|
||||
}
|
||||
else if (magicNumber == Launcher.PROTOCOL_MAGIC_LEGACY){
|
||||
|
||||
} else
|
||||
throw new IOException("Invalid Handshake");
|
||||
// Verify key modulus
|
||||
BigInteger keyModulus = input.readBigInteger(SecurityHelper.RSA_KEY_LENGTH + 1);
|
||||
if (!legacy) {
|
||||
session = input.readLong();
|
||||
sessions.updateClient(session);
|
||||
}
|
||||
if (!keyModulus.equals(server.privateKey.getModulus())) {
|
||||
output.writeBoolean(false);
|
||||
throw new IOException(String.format("#%d Key modulus mismatch", session));
|
||||
}
|
||||
// Read request type
|
||||
Integer type = input.readVarInt();
|
||||
if (!server.serverSocketHandler.onHandshake(session, type)) {
|
||||
output.writeBoolean(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Protocol successfully verified
|
||||
output.writeBoolean(true);
|
||||
output.flush();
|
||||
return new Handshake(type, session);
|
||||
}
|
||||
|
||||
private void respond(Integer type, HInput input, HOutput output, long session, String ip) throws Exception {
|
||||
if (server.serverSocketHandler.logConnections)
|
||||
LogHelper.info("Connection #%d from %s", session, ip);
|
||||
|
||||
// Choose response based on type
|
||||
Response response = Response.getResponse(type, server, session, input, output, ip);
|
||||
|
||||
// Reply
|
||||
response.reply();
|
||||
LogHelper.subDebug("#%d Replied", session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!server.serverSocketHandler.logConnections)
|
||||
LogHelper.debug("Connection from %s", IOHelper.getIP(socket.getRemoteSocketAddress()));
|
||||
|
||||
// Process connection
|
||||
boolean cancelled = false;
|
||||
Exception savedError = null;
|
||||
try (HInput input = new HInput(socket.getInputStream());
|
||||
HOutput output = new HOutput(socket.getOutputStream())) {
|
||||
Handshake handshake = readHandshake(input, output);
|
||||
if (handshake == null) { // Not accepted
|
||||
cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Start response
|
||||
try {
|
||||
respond(handshake.type, input, output, handshake.session, IOHelper.getIP(socket.getRemoteSocketAddress()));
|
||||
} catch (RequestException e) {
|
||||
LogHelper.subDebug(String.format("#%d Request error: %s", handshake.session, e.getMessage()));
|
||||
output.writeString(e.getMessage(), 0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
savedError = e;
|
||||
LogHelper.error(e);
|
||||
} finally {
|
||||
IOHelper.close(socket);
|
||||
if (!cancelled)
|
||||
server.serverSocketHandler.onDisconnect(savedError);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue