4.0.0 init commit

This commit is contained in:
Gravit 2018-09-17 14:07:32 +07:00
commit a04878f176
284 changed files with 21374 additions and 0 deletions

26
.gitattributes vendored Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<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
View 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

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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");
}
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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(";");
}
}

View File

@ -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())));
}
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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) {
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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]);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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