From 9c99465d7d76acc79e09257d15071a53aca3f3ab Mon Sep 17 00:00:00 2001 From: poison-flower Date: Sat, 12 Sep 2026 15:52:47 +0000 Subject: [PATCH] login server java 25 --- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 63093 bytes .../.mvn/wrapper/maven-wrapper.properties | 4 + l2j-server-login/LICENSE.md | 636 ++++++++++++++++++ l2j-server-login/README.md | 20 + l2j-server-login/bitbucket-pipelines.yml | 22 + l2j-server-login/mvnw | 338 ++++++++++ l2j-server-login/mvnw.cmd | 206 ++++++ l2j-server-login/pom.xml | 144 ++++ l2j-server-login/src/main/assembly/zip.xml | 47 ++ .../loginserver/FloodProtectedListener.java | 133 ++++ .../loginserver/GameServerListener.java | 48 ++ .../loginserver/GameServerTable.java | 552 +++++++++++++++ .../loginserver/GameServerThread.java | 320 +++++++++ .../loginserver/LoginController.java | 550 +++++++++++++++ .../l2jserver/loginserver/LoginServer.java | 221 ++++++ .../l2jserver/loginserver/SelectorHelper.java | 79 +++ .../com/l2jserver/loginserver/SessionKey.java | 70 ++ .../loginserver/config/Configuration.java | 63 ++ .../config/DatabaseConfiguration.java | 58 ++ .../config/EmailConfiguration.java | 82 +++ .../loginserver/config/MMOConfiguration.java | 58 ++ .../config/ServerConfiguration.java | 111 +++ .../config/TelnetConfiguration.java | 57 ++ .../l2jserver/loginserver/mail/BaseMail.java | 135 ++++ .../loginserver/mail/MailSystem.java | 134 ++++ .../loginserver/model/AccountInfo.java | 66 ++ .../loginserver/model/GameServerVersion.java | 107 +++ .../network/L2JGameServerPacketHandler.java | 121 ++++ .../loginserver/network/L2LoginClient.java | 252 +++++++ .../network/L2LoginPacketHandler.java | 79 +++ .../network/clientpackets/AuthGameGuard.java | 79 +++ .../clientpackets/L2LoginClientPacket.java | 47 ++ .../clientpackets/RequestAuthLogin.java | 153 +++++ .../clientpackets/RequestServerList.java | 69 ++ .../clientpackets/RequestServerLogin.java | 82 +++ .../gameserverpackets/BlowFishKey.java | 72 ++ .../gameserverpackets/ChangeAccessLevel.java | 45 ++ .../gameserverpackets/ChangePassword.java | 109 +++ .../gameserverpackets/GameServerAuth.java | 173 +++++ .../gameserverpackets/PlayerAuthRequest.java | 53 ++ .../gameserverpackets/PlayerInGame.java | 51 ++ .../gameserverpackets/PlayerLogout.java | 50 ++ .../gameserverpackets/PlayerTracert.java | 42 ++ .../gameserverpackets/ReplyCharacters.java | 41 ++ .../gameserverpackets/RequestSendMail.java | 41 ++ .../gameserverpackets/RequestTempBan.java | 76 +++ .../gameserverpackets/ServerStatus.java | 93 +++ .../loginserverpackets/AuthResponse.java | 40 ++ .../ChangePasswordResponse.java | 39 ++ .../network/loginserverpackets/InitLS.java | 48 ++ .../loginserverpackets/KickPlayer.java | 37 + .../loginserverpackets/LoginServerFail.java | 54 ++ .../PlayerAuthResponse.java | 38 ++ .../loginserverpackets/RequestCharacters.java | 37 + .../network/serverpackets/AccountKicked.java | 54 ++ .../network/serverpackets/GGAuth.java | 44 ++ .../network/serverpackets/Init.java | 73 ++ .../serverpackets/L2LoginServerPacket.java | 30 + .../network/serverpackets/LoginFail.java | 89 +++ .../network/serverpackets/LoginOk.java | 59 ++ .../network/serverpackets/PlayFail.java | 88 +++ .../network/serverpackets/PlayOk.java | 40 ++ .../network/serverpackets/ServerList.java | 162 +++++ .../security/ScrambledKeyPair.java | 72 ++ .../security/crypt/LoginCrypt.java | 117 ++++ .../loginserver/status/LoginStatusThread.java | 216 ++++++ .../l2jserver/loginserver/status/Status.java | 103 +++ .../l2jserver/loginserver/util/IPSubnet.java | 132 ++++ .../src/main/resources/LoginServer_loop.sh | 11 + .../src/main/resources/config/banned_ip.cfg | 7 + .../main/resources/config/database.properties | 21 + .../main/resources/config/email.properties | 65 ++ .../src/main/resources/config/mmo.properties | 24 + .../main/resources/config/server.properties | 116 ++++ .../main/resources/config/telnet.properties | 28 + .../src/main/resources/data/mail/MailList.xml | 4 + .../src/main/resources/data/mail/MailList.xsd | 20 + .../data/mail/html/SecAuthTempBan.htm | 15 + .../src/main/resources/data/servername.xml | 130 ++++ .../src/main/resources/data/servername.xsd | 22 + .../l2j-server-login-logo_256x256.png | Bin 0 -> 19918 bytes .../src/main/resources/log4j2.xml | 13 + .../src/main/resources/sql/account_data.sql | 6 + .../src/main/resources/sql/accounts.sql | 16 + .../main/resources/sql/accounts_ipauth.sql | 5 + .../main/resources/sql/cleanup/cleanup.sql | 10 + .../src/main/resources/sql/gameservers.sql | 6 + .../src/main/resources/startLoginServer.bat | 29 + .../src/main/resources/startLoginServer.sh | 3 + 89 files changed, 8112 insertions(+) create mode 100644 l2j-server-login/.mvn/wrapper/maven-wrapper.jar create mode 100644 l2j-server-login/.mvn/wrapper/maven-wrapper.properties create mode 100644 l2j-server-login/LICENSE.md create mode 100644 l2j-server-login/README.md create mode 100644 l2j-server-login/bitbucket-pipelines.yml create mode 100644 l2j-server-login/mvnw create mode 100644 l2j-server-login/mvnw.cmd create mode 100644 l2j-server-login/pom.xml create mode 100644 l2j-server-login/src/main/assembly/zip.xml create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/FloodProtectedListener.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerListener.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerTable.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerThread.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/LoginController.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/LoginServer.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/SelectorHelper.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/SessionKey.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/config/Configuration.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/config/DatabaseConfiguration.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/config/EmailConfiguration.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/config/MMOConfiguration.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/config/ServerConfiguration.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/config/TelnetConfiguration.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/mail/BaseMail.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/mail/MailSystem.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/model/AccountInfo.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/model/GameServerVersion.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2JGameServerPacketHandler.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2LoginClient.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2LoginPacketHandler.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/AuthGameGuard.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/L2LoginClientPacket.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestAuthLogin.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestServerList.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestServerLogin.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/BlowFishKey.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ChangeAccessLevel.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ChangePassword.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/GameServerAuth.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerAuthRequest.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerInGame.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerLogout.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerTracert.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ReplyCharacters.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/RequestSendMail.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/RequestTempBan.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ServerStatus.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/AuthResponse.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/ChangePasswordResponse.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/InitLS.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/KickPlayer.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/LoginServerFail.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/PlayerAuthResponse.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/RequestCharacters.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/AccountKicked.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/GGAuth.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/Init.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/L2LoginServerPacket.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/LoginFail.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/LoginOk.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/PlayFail.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/PlayOk.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/ServerList.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/security/ScrambledKeyPair.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/security/crypt/LoginCrypt.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/status/LoginStatusThread.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/status/Status.java create mode 100644 l2j-server-login/src/main/java/com/l2jserver/loginserver/util/IPSubnet.java create mode 100644 l2j-server-login/src/main/resources/LoginServer_loop.sh create mode 100644 l2j-server-login/src/main/resources/config/banned_ip.cfg create mode 100644 l2j-server-login/src/main/resources/config/database.properties create mode 100644 l2j-server-login/src/main/resources/config/email.properties create mode 100644 l2j-server-login/src/main/resources/config/mmo.properties create mode 100644 l2j-server-login/src/main/resources/config/server.properties create mode 100644 l2j-server-login/src/main/resources/config/telnet.properties create mode 100644 l2j-server-login/src/main/resources/data/mail/MailList.xml create mode 100644 l2j-server-login/src/main/resources/data/mail/MailList.xsd create mode 100644 l2j-server-login/src/main/resources/data/mail/html/SecAuthTempBan.htm create mode 100644 l2j-server-login/src/main/resources/data/servername.xml create mode 100644 l2j-server-login/src/main/resources/data/servername.xsd create mode 100644 l2j-server-login/src/main/resources/l2j-server-login-logo_256x256.png create mode 100644 l2j-server-login/src/main/resources/log4j2.xml create mode 100644 l2j-server-login/src/main/resources/sql/account_data.sql create mode 100644 l2j-server-login/src/main/resources/sql/accounts.sql create mode 100644 l2j-server-login/src/main/resources/sql/accounts_ipauth.sql create mode 100644 l2j-server-login/src/main/resources/sql/cleanup/cleanup.sql create mode 100644 l2j-server-login/src/main/resources/sql/gameservers.sql create mode 100644 l2j-server-login/src/main/resources/startLoginServer.bat create mode 100644 l2j-server-login/src/main/resources/startLoginServer.sh diff --git a/l2j-server-login/.mvn/wrapper/maven-wrapper.jar b/l2j-server-login/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..eebc3c18b4e0bce4a2a9e66be24c4a410f17a1fa GIT binary patch literal 63093 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 _ohw3Ut=#j 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 literal 0 HcmV?d00001 diff --git a/l2j-server-login/.mvn/wrapper/maven-wrapper.properties b/l2j-server-login/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..e364bfe9 --- /dev/null +++ b/l2j-server-login/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,4 @@ +wrapperVersion=3.3.4 +distributionType=script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar diff --git a/l2j-server-login/LICENSE.md b/l2j-server-login/LICENSE.md new file mode 100644 index 00000000..61b142f0 --- /dev/null +++ b/l2j-server-login/LICENSE.md @@ -0,0 +1,636 @@ +# 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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/](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](http://www.gnu.org/philosophy/why-not-lgpl.html). \ No newline at end of file diff --git a/l2j-server-login/README.md b/l2j-server-login/README.md new file mode 100644 index 00000000..71109590 --- /dev/null +++ b/l2j-server-login/README.md @@ -0,0 +1,20 @@ +L2J Server: Login +=== + +L2J Server: Login is the authentication server, users are redirected to a game server afterward. + +![](./src/main/resources/l2j-server-login-logo_256x256.png) + + +Links +--- + +- [Web Site](http://www.l2jserver.com) + +- [Forums](http://www.l2jserver.com/forum/) + +- [Discord](https://discord.gg/AzHh7e2Sej) + +- [Trello](https://trello.com/b/qjLoH966) + +- [@l2jserver](https://twitter.com/l2jserver) \ No newline at end of file diff --git a/l2j-server-login/bitbucket-pipelines.yml b/l2j-server-login/bitbucket-pipelines.yml new file mode 100644 index 00000000..7a726436 --- /dev/null +++ b/l2j-server-login/bitbucket-pipelines.yml @@ -0,0 +1,22 @@ +image: eclipse-temurin:25-jdk-ubi10-minimal + +pipelines: + default: + - step: + caches: + - maven + script: + - /bin/sh mvnw install + artifacts: + - target/*.zip + - step: + deployment: Production + script: + - pipe: atlassian/ftp-deploy:0.7.1 + variables: + USER: $FTP_USER + PASSWORD: $FTP_PASSWORD + SERVER: $FTP_HOST + REMOTE_PATH: $FTP_REMOTE_PATH + LOCAL_PATH: target/ + SET_ARGS: "ftp:ssl-allow yes ftp:ssl-force yes ftp:ssl-protect-data yes ssl:verify-certificate false" \ No newline at end of file diff --git a/l2j-server-login/mvnw b/l2j-server-login/mvnw new file mode 100644 index 00000000..1ddd97b9 --- /dev/null +++ b/l2j-server-login/mvnw @@ -0,0 +1,338 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ]; then + + if [ -f /usr/local/etc/mavenrc ]; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ]; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ]; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +mingw=false +case "$(uname)" in +CYGWIN*) cygwin=true ;; +MINGW*) mingw=true ;; +Darwin*) + darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)" + export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home" + export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ]; then + if [ -r /etc/gentoo-release ]; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \ + && JAVA_HOME="$( + cd "$JAVA_HOME" || ( + echo "cannot cd into $JAVA_HOME." >&2 + exit 1 + ) + pwd + )" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin; then + javaHome="$(dirname "$javaExecutable")" + javaExecutable="$(cd "$javaHome" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "$javaExecutable")" + fi + javaHome="$(dirname "$javaExecutable")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ]; then + if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$( + \unset -f command 2>/dev/null + \command -v java + )" + fi +fi + +if [ ! -x "$JAVACMD" ]; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ]; then + echo "Warning: JAVA_HOME environment variable is not set." >&2 +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ]; then + echo "Path not specified to find_maven_basedir" >&2 + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ]; do + if [ -d "$wdir"/.mvn ]; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$( + cd "$wdir/.." || exit 1 + pwd + ) + fi + # end of workaround + done + printf '%s' "$( + cd "$basedir" || exit 1 + pwd + )" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' <"$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1 +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + fi + while IFS="=" read -r key value; do + case "$key" in wrapperUrl) + wrapperUrl=$(trim "${value-}") + break + ;; + esac + done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget >/dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget ${QUIET:+"$QUIET"} "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget ${QUIET:+"$QUIET"} --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl >/dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl ${QUIET:+"$QUIET"} -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl ${QUIET:+"$QUIET"} --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in wrapperSha256Sum) + wrapperSha256Sum=$(trim "${value-}") + break + ;; + esac +done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c - >/dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] \ + && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/l2j-server-login/mvnw.cmd b/l2j-server-login/mvnw.cmd new file mode 100644 index 00000000..c453424c --- /dev/null +++ b/l2j-server-login/mvnw.cmd @@ -0,0 +1,206 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. >&2 +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. >&2 +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/l2j-server-login/pom.xml b/l2j-server-login/pom.xml new file mode 100644 index 00000000..53bbdc6a --- /dev/null +++ b/l2j-server-login/pom.xml @@ -0,0 +1,144 @@ + + 4.0.0 + com.l2jserver + l2j-server-login + 2.6.8.0 + L2J Login Server + L2J Login Server handles the game client authentication and login. + https://bitbucket.org/l2jserver/l2j-server-login/ + + UTF-8 + UTF-8 + 25 + 25 + + 2.25.3 + 2.1.5 + 1.0.12 + + 2.6.9.0 + + 8 + 12.2.0 + 3.8.0 + 3.15.0 + 3.5.0 + + + + GPL 3.0 + https://www.gnu.org/licenses/gpl-3.0.html + + + + L2J Server + https://l2jserver.com + + + + jitpack.io + https://jitpack.io + + + + + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.aeonbits.owner + owner-java8 + ${owner.version} + + + jakarta.mail + jakarta.mail-api + ${jakarta.mail-api.version} + + + + org.bitbucket.l2jserver + l2j-server-commons + ${l2j-server-commons.version} + + + + l2jlogin + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + full + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + true + libs/ + com.l2jserver.loginserver.LoginServer + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + package + + single + + + ${project.artifactId}-${project.version} + false + + src/main/assembly/zip.xml + + + + + + + + + + ci + + + + org.owasp + dependency-check-maven + ${dependency-check-maven.version} + + ${dependency-check-maven.level} + false + ${env.NVD_API_KEY} + + + + + check + + + + + + + + + \ No newline at end of file diff --git a/l2j-server-login/src/main/assembly/zip.xml b/l2j-server-login/src/main/assembly/zip.xml new file mode 100644 index 00000000..9b8aa146 --- /dev/null +++ b/l2j-server-login/src/main/assembly/zip.xml @@ -0,0 +1,47 @@ + + zip + false + + zip + + + + ${project.build.directory}/l2jlogin.jar + . + + + src/main/resources/LoginServer_loop.sh + . + + + src/main/resources/startLoginServer.bat + . + + + src/main/resources/startLoginServer.sh + . + + + + + src/main/resources/ + + config/** + data/** + sql/** + + . + + + + + libs + + ${project.groupId}:${project.artifactId}:jar:* + + + + \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/FloodProtectedListener.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/FloodProtectedListener.java new file mode 100644 index 00000000..f43daffa --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/FloodProtectedListener.java @@ -0,0 +1,133 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver; + +import static com.l2jserver.loginserver.config.Configuration.server; + +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Flood Protected listener. + * @author -Wooden- + * @version 2.6.1.0 + */ +public abstract class FloodProtectedListener extends Thread { + + private static final Logger LOG = LoggerFactory.getLogger(FloodProtectedListener.class); + + private final Map _floodProtection = new ConcurrentHashMap<>(); + + private final ServerSocket _serverSocket; + + public FloodProtectedListener(String listenIp, int port) throws Exception { + if (listenIp.equals("*")) { + _serverSocket = new ServerSocket(port); + } else { + _serverSocket = new ServerSocket(port, 50, InetAddress.getByName(listenIp)); + } + } + + @Override + public void run() { + Socket connection; + while (!isInterrupted()) { + try { + connection = _serverSocket.accept(); + if (server().isFloodProtectionEnabled()) { + ForeignConnection fConnection = _floodProtection.get(connection.getInetAddress().getHostAddress()); + if (fConnection != null) { + fConnection.connectionNumber += 1; + if (((fConnection.connectionNumber > server().getFastConnectionLimit()) && // + ((System.currentTimeMillis() - fConnection.lastConnection) < server().getNormalConnectionTime())) || // + ((System.currentTimeMillis() - fConnection.lastConnection) < server().getFastConnectionTime()) || // + (fConnection.connectionNumber > server().getMaxConnectionPerIP())) { + fConnection.lastConnection = System.currentTimeMillis(); + connection.close(); + fConnection.connectionNumber -= 1; + if (!fConnection.isFlooding) { + LOG.warn("Potential Flood from {}!", connection.getInetAddress().getHostAddress()); + } + fConnection.isFlooding = true; + continue; + } + if (fConnection.isFlooding) // if connection was flooding server but now passed the check + { + fConnection.isFlooding = false; + LOG.info("Connection {} is not considered as flooding anymore.", connection.getInetAddress().getHostAddress()); + } + fConnection.lastConnection = System.currentTimeMillis(); + } else { + fConnection = new ForeignConnection(System.currentTimeMillis()); + _floodProtection.put(connection.getInetAddress().getHostAddress(), fConnection); + } + } + + addClient(connection); + } catch (Exception e) { + if (isInterrupted()) { + close(); + break; + } + } + } + } + + protected static class ForeignConnection { + public int connectionNumber; + public long lastConnection; + public boolean isFlooding = false; + + public ForeignConnection(long time) { + lastConnection = time; + connectionNumber = 1; + } + } + + public abstract void addClient(Socket s); + + public void removeFloodProtection(String ip) { + if (!server().isFloodProtectionEnabled()) { + return; + } + ForeignConnection fConnection = _floodProtection.get(ip); + if (fConnection != null) { + fConnection.connectionNumber -= 1; + if (fConnection.connectionNumber == 0) { + _floodProtection.remove(ip); + } + } else { + LOG.warn("Removing a flood protection for a Game Server ({}) that was not in the connection map???", ip); + } + } + + public void close() { + try { + _serverSocket.close(); + } catch (Exception ex) { + LOG.warn("There has been an error closing the connection!", ex); + } + } +} \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerListener.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerListener.java new file mode 100644 index 00000000..707c7c53 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerListener.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver; + +import static com.l2jserver.loginserver.config.Configuration.server; + +import java.net.Socket; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Game Server listener. + * @author KenM + * @version 2.6.1.0 + */ +public class GameServerListener extends FloodProtectedListener { + private static final List _gameServers = new CopyOnWriteArrayList<>(); + + public GameServerListener() throws Exception { + super(server().getGameServerHost(), server().getGameServerPort()); + setName(getClass().getSimpleName()); + } + + @Override + public void addClient(Socket s) { + _gameServers.add(new GameServerThread(s)); + } + + public void removeGameServer(GameServerThread gst) { + _gameServers.remove(gst); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerTable.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerTable.java new file mode 100644 index 00000000..59a07ae0 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerTable.java @@ -0,0 +1,552 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.dao.ServerNameDAO; +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.loginserver.network.gameserverpackets.ServerStatus; +import com.l2jserver.loginserver.util.IPSubnet; + +/** + * Loads the game server names and initialize the game server tables. + * @author KenM + * @author Zoey76 + * @version 2.6.1.0 + */ +public final class GameServerTable { + + private static final Logger LOG = LoggerFactory.getLogger(GameServerTable.class); + + private static final Map GAME_SERVER_TABLE = new HashMap<>(); + + private static final int KEYS_SIZE = 10; + + private KeyPair[] _keyPairs; + + public GameServerTable() { + loadRegisteredGameServers(); + LOG.info("{}: Loaded {} registered Game Servers.", getClass().getSimpleName(), GAME_SERVER_TABLE.size()); + + initRSAKeys(); + LOG.info("{}: Cached {} RSA keys for Game Server communication.", getClass().getSimpleName(), _keyPairs.length); + } + + private void initRSAKeys() { + try { + final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(new RSAKeyGenParameterSpec(512, RSAKeyGenParameterSpec.F4)); + _keyPairs = new KeyPair[KEYS_SIZE]; + for (int i = 0; i < KEYS_SIZE; i++) { + _keyPairs[i] = keyGen.genKeyPair(); + } + } catch (Exception e) { + LOG.error("{}: Error loading RSA keys for Game Server communication!", getClass().getSimpleName(), e); + } + } + + private void loadRegisteredGameServers() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.createStatement(); + var rs = ps.executeQuery("SELECT * FROM gameservers")) { + int id; + while (rs.next()) { + id = rs.getInt("server_id"); + GAME_SERVER_TABLE.put(id, new GameServerInfo(id, stringToHex(rs.getString("hexid")))); + } + } catch (Exception e) { + LOG.error("{}: Error loading registered game servers!", getClass().getSimpleName(), e); + } + } + + /** + * Gets the registered game servers. + * @return the registered game servers + */ + public Map getRegisteredGameServers() { + return GAME_SERVER_TABLE; + } + + /** + * Gets the registered game server by id. + * @param id the game server ID + * @return the registered game server by id + */ + public GameServerInfo getRegisteredGameServerById(int id) { + return GAME_SERVER_TABLE.get(id); + } + + /** + * Checks for registered game server on id. + * @param id the id + * @return true, if successful + */ + public boolean hasRegisteredGameServerOnId(int id) { + return GAME_SERVER_TABLE.containsKey(id); + } + + /** + * Register with first available id. + * @param gsi the game server information DTO + * @return true, if successful + */ + public boolean registerWithFirstAvailableId(GameServerInfo gsi) { + // avoid two servers registering with the same "free" id + synchronized (GAME_SERVER_TABLE) { + for (Integer serverId : ServerNameDAO.getServers().keySet()) { + if (!GAME_SERVER_TABLE.containsKey(serverId)) { + GAME_SERVER_TABLE.put(serverId, gsi); + gsi.setId(serverId); + return true; + } + } + } + return false; + } + + /** + * Register a game server. + * @param id the id + * @param gsi the gsi + * @return true, if successful + */ + public boolean register(int id, GameServerInfo gsi) { + // avoid two servers registering with the same id + synchronized (GAME_SERVER_TABLE) { + if (!GAME_SERVER_TABLE.containsKey(id)) { + GAME_SERVER_TABLE.put(id, gsi); + return true; + } + } + return false; + } + + /** + * Wrapper method. + * @param gsi the game server info DTO. + */ + public void registerServerOnDB(GameServerInfo gsi) { + registerServerOnDB(gsi.getHexId(), gsi.getId(), gsi.getExternalHost()); + } + + /** + * Register server on db. + * @param hexId the hex id + * @param id the id + * @param externalHost the external host + */ + public void registerServerOnDB(byte[] hexId, int id, String externalHost) { + register(id, new GameServerInfo(id, hexId)); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO gameservers (hexid,server_id,host) values (?,?,?)")) { + ps.setString(1, hexToString(hexId)); + ps.setInt(2, id); + ps.setString(3, externalHost); + ps.executeUpdate(); + } catch (Exception e) { + LOG.error("{}: Error while saving game server!", getClass().getSimpleName(), e); + } + } + + /** + * Gets the key pair. + * @return a random key pair. + */ + public KeyPair getKeyPair() { + return _keyPairs[Rnd.nextInt(10)]; + } + + /** + * String to hex. + * @param string the string to convert. + * @return return the hex representation. + */ + private byte[] stringToHex(String string) { + return new BigInteger(string, 16).toByteArray(); + } + + /** + * Hex to string. + * @param hex the hex value to convert. + * @return the string representation. + */ + private String hexToString(byte[] hex) { + if (hex == null) { + return "null"; + } + return new BigInteger(hex).toString(16); + } + + /** + * The Class GameServerInfo. + */ + public static class GameServerInfo { + // auth + private int _id; + private final byte[] _hexId; + private boolean _isAuthed; + // status + private GameServerThread _gst; + private int _status; + // network + private final ArrayList _addresses = new ArrayList<>(5); + private int _port; + // config + private final boolean _isPvp = true; + private int _serverType; + private int _ageLimit; + private boolean _isShowingBrackets; + private int _maxPlayers; + + /** + * Instantiates a new game server info. + * @param id the id + * @param hexId the hex id + * @param gst the gst + */ + public GameServerInfo(int id, byte[] hexId, GameServerThread gst) { + _id = id; + _hexId = hexId; + _gst = gst; + _status = ServerStatus.STATUS_DOWN; + } + + /** + * Instantiates a new game server info. + * @param id the id + * @param hexId the hex id + */ + public GameServerInfo(int id, byte[] hexId) { + this(id, hexId, null); + } + + /** + * Sets the id. + * @param id the new id + */ + public void setId(int id) { + _id = id; + } + + /** + * Gets the id. + * @return the id + */ + public int getId() { + return _id; + } + + /** + * Gets the hex id. + * @return the hex id + */ + public byte[] getHexId() { + return _hexId; + } + + public String getName() { + return ServerNameDAO.getServer(_id); + } + + /** + * Sets the authed. + * @param isAuthed the new authed + */ + public void setAuthed(boolean isAuthed) { + _isAuthed = isAuthed; + } + + /** + * Checks if is authed. + * @return true, if is authed + */ + public boolean isAuthed() { + return _isAuthed; + } + + /** + * Sets the game server thread. + * @param gst the new game server thread + */ + public void setGameServerThread(GameServerThread gst) { + _gst = gst; + } + + /** + * Gets the game server thread. + * @return the game server thread + */ + public GameServerThread getGameServerThread() { + return _gst; + } + + /** + * Sets the status. + * @param status the new status + */ + public void setStatus(int status) { + _status = status; + } + + /** + * Gets the status. + * @return the status + */ + public int getStatus() { + return _status; + } + + public String getStatusName() { + return switch (_status) { + case 0 -> "Auto"; + case 1 -> "Good"; + case 2 -> "Normal"; + case 3 -> "Full"; + case 4 -> "Down"; + case 5 -> "GM Only"; + default -> "Unknown"; + }; + } + + /** + * Gets the current player count. + * @return the current player count + */ + public int getCurrentPlayerCount() { + if (_gst == null) { + return 0; + } + return _gst.getPlayerCount(); + } + + /** + * Gets the external host. + * @return the external host + */ + public String getExternalHost() { + try { + return getServerAddress(InetAddress.getByName("0.0.0.0")); + } catch (Exception e) { + + } + return null; + } + + /** + * Gets the port. + * @return the port + */ + public int getPort() { + return _port; + } + + /** + * Sets the port. + * @param port the new port + */ + public void setPort(int port) { + _port = port; + } + + /** + * Sets the max players. + * @param maxPlayers the new max players + */ + public void setMaxPlayers(int maxPlayers) { + _maxPlayers = maxPlayers; + } + + /** + * Gets the max players. + * @return the max players + */ + public int getMaxPlayers() { + return _maxPlayers; + } + + /** + * Checks if is pvp. + * @return true, if is pvp + */ + public boolean isPvp() { + return _isPvp; + } + + /** + * Sets the age limit. + * @param val the new age limit + */ + public void setAgeLimit(int val) { + _ageLimit = val; + } + + /** + * Gets the age limit. + * @return the age limit + */ + public int getAgeLimit() { + return _ageLimit; + } + + /** + * Sets the server type. + * @param val the new server type + */ + public void setServerType(int val) { + _serverType = val; + } + + /** + * Gets the server type. + * @return the server type + */ + public int getServerType() { + return _serverType; + } + + /** + * Sets the showing brackets. + * @param val the new showing brackets + */ + public void setShowingBrackets(boolean val) { + _isShowingBrackets = val; + } + + /** + * Checks if it is showing brackets. + * @return true, if it is showing brackets + */ + public boolean isShowingBrackets() { + return _isShowingBrackets; + } + + /** + * Sets the down. + */ + public void setDown() { + setAuthed(false); + setPort(0); + setGameServerThread(null); + setStatus(ServerStatus.STATUS_DOWN); + } + + /** + * Adds the server address. + * @param subnet the subnet + * @param addr the addr + * @throws UnknownHostException the unknown host exception + */ + public void addServerAddress(String subnet, String addr) throws UnknownHostException { + _addresses.add(new GameServerAddress(subnet, addr)); + } + + /** + * Gets the server address. + * @param addr the addr + * @return the server address + */ + public String getServerAddress(InetAddress addr) { + for (GameServerAddress a : _addresses) { + if (a.equals(addr)) { + return a.getServerAddress(); + } + } + return null; // should not happen + } + + /** + * Gets the server addresses. + * @return the server addresses + */ + public String[] getServerAddresses() { + String[] result = new String[_addresses.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = _addresses.get(i).toString(); + } + + return result; + } + + /** + * Clear server addresses. + */ + public void clearServerAddresses() { + _addresses.clear(); + } + + /** + * The Class GameServerAddress. + */ + private static class GameServerAddress extends IPSubnet { + private final String _serverAddress; + + /** + * Instantiates a new game server address. + * @param subnet the subnet + * @param address the address + * @throws UnknownHostException the unknown host exception + */ + public GameServerAddress(String subnet, String address) throws UnknownHostException { + super(subnet); + _serverAddress = address; + } + + /** + * Gets the server address. + * @return the server address + */ + public String getServerAddress() { + return _serverAddress; + } + + @Override + public String toString() { + return _serverAddress + super.toString(); + } + } + } + + /** + * Gets the single instance of GameServerTable. + * @return single instance of GameServerTable + */ + public static GameServerTable getInstance() { + return SingletonHolder._instance; + } + + /** + * The Class SingletonHolder. + */ + private static class SingletonHolder { + protected static final GameServerTable _instance = new GameServerTable(); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerThread.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerThread.java new file mode 100644 index 00000000..9bbd3a5d --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/GameServerThread.java @@ -0,0 +1,320 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver; + +import static com.l2jserver.loginserver.config.Configuration.server; +import static com.l2jserver.loginserver.network.loginserverpackets.LoginServerFail.REASON_IP_BANNED; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.security.KeyPair; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.dao.ServerNameDAO; +import com.l2jserver.commons.network.BaseSendablePacket; +import com.l2jserver.commons.security.crypt.NewCrypt; +import com.l2jserver.commons.util.Util; +import com.l2jserver.loginserver.GameServerTable.GameServerInfo; +import com.l2jserver.loginserver.network.L2JGameServerPacketHandler; +import com.l2jserver.loginserver.network.L2JGameServerPacketHandler.GameServerState; +import com.l2jserver.loginserver.network.loginserverpackets.ChangePasswordResponse; +import com.l2jserver.loginserver.network.loginserverpackets.InitLS; +import com.l2jserver.loginserver.network.loginserverpackets.KickPlayer; +import com.l2jserver.loginserver.network.loginserverpackets.LoginServerFail; +import com.l2jserver.loginserver.network.loginserverpackets.RequestCharacters; + +/** + * Game Server thread. + * @author -Wooden- + * @author KenM + * @version 2.6.1.0 + */ +public class GameServerThread extends Thread { + + private static final Logger LOG = LoggerFactory.getLogger(GameServerThread.class); + + private final Socket _connection; + + private InputStream _in; + + private OutputStream _out; + + private final RSAPublicKey _publicKey; + + private final RSAPrivateKey _privateKey; + + private NewCrypt _blowfish; + + private GameServerState _loginConnectionState = GameServerState.CONNECTED; + + private final String _connectionIp; + + private GameServerInfo _gsi; + + /** Authed Clients on a GameServer */ + private final Set _accountsOnGameServer = ConcurrentHashMap.newKeySet(); + + private String _connectionIPAddress; + + @Override + public void run() { + _connectionIPAddress = _connection.getInetAddress().getHostAddress(); + if (GameServerThread.isBannedGameServerIP(_connectionIPAddress)) { + LOG.warn("IP Address {} is on banned IP list.", _connectionIPAddress); + forceClose(REASON_IP_BANNED); + return; + } + + try { + sendPacket(new InitLS(_publicKey.getModulus().toByteArray())); + + int lengthHi; + int lengthLo; + int length; + boolean checksumOk; + for (;;) { + lengthLo = _in.read(); + lengthHi = _in.read(); + length = (lengthHi * 256) + lengthLo; + + if ((lengthHi < 0) || _connection.isClosed()) { + LOG.warn("Login terminated the connection!"); + break; + } + + byte[] data = new byte[length - 2]; + + int receivedBytes = 0; + int newBytes = 0; + int left = length - 2; + while ((newBytes != -1) && (receivedBytes < (length - 2))) { + newBytes = _in.read(data, receivedBytes, left); + receivedBytes = receivedBytes + newBytes; + left -= newBytes; + } + + if (receivedBytes != (length - 2)) { + LOG.warn("Incomplete Packet is sent to the server, closing connection. (LS)"); + break; + } + + // decrypt if we have a key + _blowfish.decrypt(data, 0, data.length); + checksumOk = NewCrypt.verifyChecksum(data); + if (!checksumOk) { + LOG.warn("Incorrect packet checksum, closing connection. (LS)"); + return; + } + + if (server().isDebug()) { + LOG.warn("[C]" + System.lineSeparator() + Util.printData(data)); + } + + L2JGameServerPacketHandler.handlePacket(data, this); + } + } catch (IOException ex) { + final var serverName = (getServerId() != -1 ? "[" + getServerId() + "] " + ServerNameDAO.getServer(getServerId()) : "(" + _connectionIPAddress + ")"); + LOG.warn("Game Server {} lost connection!", serverName); + broadcastToTelnet("Game Server " + serverName + " lost connection!"); + } finally { + if (isAuthed()) { + _gsi.setDown(); + + LOG.info("Server {}[{}] is now disconnected.", ServerNameDAO.getServer(getServerId()), getServerId()); + } + LoginServer.getInstance().getGameServerListener().removeGameServer(this); + LoginServer.getInstance().getGameServerListener().removeFloodProtection(_connectionIp); + } + } + + public boolean hasAccountOnGameServer(String account) { + return _accountsOnGameServer.contains(account); + } + + public int getPlayerCount() { + return _accountsOnGameServer.size(); + } + + /** + * Attaches a GameServerInfo to this Thread
+ *
    + *
  • Updates the GameServerInfo values based on GameServerAuth packet
  • + *
  • Sets the GameServerInfo as Authed
  • + *
+ * @param gsi The GameServerInfo to be attached. + * @param port the port + * @param hosts the hosts + * @param maxPlayers the maximum amount of players + */ + public void attachGameServerInfo(GameServerInfo gsi, int port, String[] hosts, int maxPlayers) { + setGameServerInfo(gsi); + gsi.setGameServerThread(this); + gsi.setPort(port); + setGameHosts(hosts); + gsi.setMaxPlayers(maxPlayers); + gsi.setAuthed(true); + } + + public void forceClose(int reason) { + sendPacket(new LoginServerFail(reason)); + + try { + _connection.close(); + } catch (IOException ex) { + LOG.debug("Failed disconnecting banned server, server already disconnected."); + } + } + + public static boolean isBannedGameServerIP(String ipAddress) { + return false; + } + + public GameServerThread(Socket con) { + _connection = con; + _connectionIp = con.getInetAddress().getHostAddress(); + try { + _in = _connection.getInputStream(); + _out = new BufferedOutputStream(_connection.getOutputStream()); + } catch (IOException ex) { + LOG.warn("There has been an error creating a connection!", ex); + } + + KeyPair pair = GameServerTable.getInstance().getKeyPair(); + _privateKey = (RSAPrivateKey) pair.getPrivate(); + _publicKey = (RSAPublicKey) pair.getPublic(); + _blowfish = new NewCrypt("_;v.]05-31!|+-%xT!^[$\00"); + setName(getClass().getSimpleName() + "-" + threadId() + "@" + _connectionIp); + start(); + } + + public void sendPacket(BaseSendablePacket sl) { + try { + byte[] data = sl.getContent(); + NewCrypt.appendChecksum(data); + if (server().isDebug()) { + LOG.info("[S] {}:{}{}", sl.getClass().getSimpleName(), System.lineSeparator(), Util.printData(data)); + } + _blowfish.crypt(data, 0, data.length); + + int len = data.length + 2; + synchronized (_out) { + _out.write(len & 0xff); + _out.write((len >> 8) & 0xff); + _out.write(data); + _out.flush(); + } + } catch (IOException ex) { + LOG.error("There has been an error while sending packet {}!", sl.getClass().getSimpleName(), ex); + } + } + + public void broadcastToTelnet(String msg) { + if (LoginServer.getInstance().getStatusServer() != null) { + LoginServer.getInstance().getStatusServer().sendMessageToTelnets(msg); + } + } + + public void kickPlayer(String account) { + sendPacket(new KickPlayer(account)); + } + + public void requestCharacters(String account) { + sendPacket(new RequestCharacters(account)); + } + + public void ChangePasswordResponse(byte successful, String characterName, String msgToSend) { + sendPacket(new ChangePasswordResponse(successful, characterName, msgToSend)); + } + + public void setGameHosts(String[] hosts) { + LOG.info("Updated game server {}[{}] IPs.", ServerNameDAO.getServer(getServerId()), getServerId()); + + _gsi.clearServerAddresses(); + for (int i = 0; i < hosts.length; i += 2) { + try { + _gsi.addServerAddress(hosts[i], hosts[i + 1]); + } catch (Exception ex) { + LOG.warn("There has been an error resolving host name {}!", hosts[i], ex); + } + } + + for (String s : _gsi.getServerAddresses()) { + LOG.info(s); + } + } + + public boolean isAuthed() { + if (getGameServerInfo() == null) { + return false; + } + return getGameServerInfo().isAuthed(); + } + + public void setGameServerInfo(GameServerInfo gsi) { + _gsi = gsi; + } + + public GameServerInfo getGameServerInfo() { + return _gsi; + } + + public String getConnectionIpAddress() { + return _connectionIPAddress; + } + + public int getServerId() { + if (getGameServerInfo() != null) { + return getGameServerInfo().getId(); + } + return -1; + } + + public RSAPrivateKey getPrivateKey() { + return _privateKey; + } + + public void SetBlowFish(NewCrypt blowfish) { + _blowfish = blowfish; + } + + public void addAccountOnGameServer(String account) { + _accountsOnGameServer.add(account); + } + + public void removeAccountOnGameServer(String account) { + _accountsOnGameServer.remove(account); + } + + public GameServerState getLoginConnectionState() { + return _loginConnectionState; + } + + public void setLoginConnectionState(GameServerState state) { + _loginConnectionState = state; + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/LoginController.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/LoginController.java new file mode 100644 index 00000000..94783ff0 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/LoginController.java @@ -0,0 +1,550 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver; + +import static com.l2jserver.loginserver.config.Configuration.server; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.security.spec.RSAKeyGenParameterSpec.F4; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.crypto.Cipher; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.loginserver.GameServerTable.GameServerInfo; +import com.l2jserver.loginserver.model.AccountInfo; +import com.l2jserver.loginserver.network.L2LoginClient; +import com.l2jserver.loginserver.network.gameserverpackets.ServerStatus; +import com.l2jserver.loginserver.network.serverpackets.LoginFail.LoginFailReason; +import com.l2jserver.loginserver.security.ScrambledKeyPair; + +/** + * Login Controller. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class LoginController { + + private static final Logger LOG = LoggerFactory.getLogger(LoginController.class); + + /** Time before kicking the client if he didn't log yet */ + public static final int LOGIN_TIMEOUT = 60 * 1000; + + /** Authed Clients on LoginServer */ + protected final Map _loginServerClients = new ConcurrentHashMap<>(); + + private final Map _failedLoginAttempts = new ConcurrentHashMap<>(); + private final Map _bannedIps = new ConcurrentHashMap<>(); + + protected final ScrambledKeyPair[] _keyPairs; + + protected byte[][] _blowfishKeys; + private static final int BLOWFISH_KEYS = 20; + + // SQL Queries + private static final String USER_INFO_SELECT = "SELECT login, password, IF(? > value OR value IS NULL, accessLevel, -1) AS accessLevel, lastServer FROM accounts LEFT JOIN (account_data) ON (account_data.account_name=accounts.login AND account_data.var=\"ban_temp\") WHERE login=?"; + private static final String AUTO_CREATE_ACCOUNTS_INSERT = "INSERT INTO accounts (login, password, lastactive, accessLevel, lastIP) values (?, ?, ?, ?, ?)"; + private static final String ACCOUNT_INFO_UPDATE = "UPDATE accounts SET lastactive = ?, lastIP = ? WHERE login = ?"; + private static final String ACCOUNT_LAST_SERVER_UPDATE = "UPDATE accounts SET lastServer = ? WHERE login = ?"; + private static final String ACCOUNT_ACCESS_LEVEL_UPDATE = "UPDATE accounts SET accessLevel = ? WHERE login = ?"; + private static final String ACCOUNT_IPS_UPDATE = "UPDATE accounts SET pcIp = ?, hop1 = ?, hop2 = ?, hop3 = ?, hop4 = ? WHERE login = ?"; + private static final String ACCOUNT_IPAUTH_SELECT = "SELECT * FROM accounts_ipauth WHERE login = ?"; + + private LoginController() { + LOG.info("Loading Login Controller..."); + + _keyPairs = new ScrambledKeyPair[10]; + + try { + final var keygen = KeyPairGenerator.getInstance("RSA"); + final var spec = new RSAKeyGenParameterSpec(1024, F4); + keygen.initialize(spec); + for (int i = 0; i < 10; i++) { + _keyPairs[i] = new ScrambledKeyPair(keygen.generateKeyPair()); + } + + testCipher((RSAPrivateKey) _keyPairs[0].getPair().getPrivate()); + + LOG.info("Cached 10 KeyPairs for RSA communication."); + } catch (Exception ex) { + LOG.error("There has been an error loading the key pairs!", ex); + } + + // Store keys for blowfish communication + generateBlowFishKeys(); + + final var purge = new PurgeThread(); + purge.setDaemon(true); + purge.start(); + } + + /** + * This is mostly to force the initialization of the Crypto Implementation, avoiding it being done on runtime when its first needed.
+ * In short, it avoids the worst-case execution time on runtime by doing it on loading. + * @param key Any private RSA Key just for testing purposes. + */ + private void testCipher(RSAPrivateKey key) throws Exception { + // avoid worst-case execution, KenM + Cipher rsaCipher = Cipher.getInstance("RSA/ECB/nopadding"); + rsaCipher.init(Cipher.DECRYPT_MODE, key); + } + + private void generateBlowFishKeys() { + _blowfishKeys = new byte[BLOWFISH_KEYS][16]; + + for (int i = 0; i < BLOWFISH_KEYS; i++) { + for (int j = 0; j < _blowfishKeys[i].length; j++) { + _blowfishKeys[i][j] = (byte) (Rnd.nextInt(255) + 1); + } + } + LOG.info("Stored {} keys for Blowfish communication.", _blowfishKeys.length); + } + + public byte[] getBlowfishKey() { + return _blowfishKeys[(int) (Math.random() * BLOWFISH_KEYS)]; + } + + public SessionKey assignSessionKeyToClient(String account, L2LoginClient client) { + SessionKey key; + + key = new SessionKey(Rnd.nextInt(), Rnd.nextInt(), Rnd.nextInt(), Rnd.nextInt()); + _loginServerClients.put(account, client); + return key; + } + + public void removeAuthedLoginClient(String account) { + if (account == null) { + return; + } + _loginServerClients.remove(account); + } + + public L2LoginClient getAuthedClient(String account) { + return _loginServerClients.get(account); + } + + public AccountInfo retrieveAccountInfo(InetAddress clientAddr, String login, String password) { + return retrieveAccountInfo(clientAddr, login, password, true); + } + + private void recordFailedLoginAttempt(InetAddress addr) { + final var failedLoginAttempts = _failedLoginAttempts.getOrDefault(addr, 0) + 1; + _failedLoginAttempts.put(addr, failedLoginAttempts); + + if (failedLoginAttempts >= server().getLoginTryBeforeBan()) { + addBanForAddress(addr, server().getLoginBlockAfterBan() * 1000); + // we need to clear the failed login attempts here, so after the ip ban is over the client has another 5 attempts + clearFailedLoginAttempts(addr); + LOG.warn("Added banned address {}, too many login attempts!", addr.getHostAddress()); + } + } + + private void clearFailedLoginAttempts(InetAddress addr) { + _failedLoginAttempts.remove(addr); + } + + private AccountInfo retrieveAccountInfo(InetAddress addr, String login, String password, boolean autoCreateIfEnabled) { + try { + final var md = MessageDigest.getInstance("SHA"); + final var raw = password.getBytes(UTF_8); + final var hashBase64 = Base64.getEncoder().encodeToString(md.digest(raw)); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(USER_INFO_SELECT)) { + ps.setString(1, Long.toString(System.currentTimeMillis())); + ps.setString(2, login); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + if (server().isDebug()) { + LOG.info("Account {} exists.", login); + } + + final var info = new AccountInfo(rs.getString("login"), rs.getString("password"), rs.getInt("accessLevel"), rs.getInt("lastServer")); + if (!info.checkPassHash(hashBase64)) { + recordFailedLoginAttempt(addr); + return null; + } + + clearFailedLoginAttempts(addr); + return info; + } + } + } + + if (!autoCreateIfEnabled || !server().autoCreateAccounts()) { + // account does not exist and auto create account is not desired + recordFailedLoginAttempt(addr); + return null; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(AUTO_CREATE_ACCOUNTS_INSERT)) { + ps.setString(1, login); + ps.setString(2, hashBase64); + ps.setLong(3, System.currentTimeMillis()); + ps.setInt(4, server().autoCreateAccountsAccessLevel()); + ps.setString(5, addr.getHostAddress()); + ps.execute(); + } catch (Exception ex) { + LOG.warn("There has been an error auto-creating the account {}!", login, ex); + return null; + } + + LOG.info("Auto-created account {}.", login); + return retrieveAccountInfo(addr, login, password, false); + } catch (Exception ex) { + LOG.warn("There has been an error getting account info for {}!", login, ex); + return null; + } + } + + public AuthLoginResult tryCheckinAccount(L2LoginClient client, InetAddress address, AccountInfo info) { + if (info.getAccessLevel() < 0) { + if (info.getAccessLevel() == server().autoCreateAccountsAccessLevel()) { + return AuthLoginResult.ACCOUNT_INACTIVE; + } + return AuthLoginResult.ACCOUNT_BANNED; + } + + AuthLoginResult ret = AuthLoginResult.INVALID_PASSWORD; + // check auth + if (canCheckIn(client, address, info)) { + // login was successful, verify presence on game servers + ret = AuthLoginResult.ALREADY_ON_GS; + if (!isAccountInAnyGameServer(info.getLogin())) { + // account isn't on any GS verify LS itself + ret = AuthLoginResult.ALREADY_ON_LS; + + if (_loginServerClients.putIfAbsent(info.getLogin(), client) == null) { + ret = AuthLoginResult.AUTH_SUCCESS; + } + } + } + return ret; + } + + /** + * Adds the address to the ban list of the login server, with the given end time in milliseconds. + * @param address The Address to be banned. + * @param expiration Timestamp in milliseconds when this ban expires + * @throws UnknownHostException if the address is invalid. + */ + public void addBanForAddress(String address, long expiration) throws Exception { + _bannedIps.putIfAbsent(InetAddress.getByName(address), expiration); + } + + /** + * Adds the address to the ban list of the login server, with the given duration. + * @param address The Address to be banned. + * @param duration is milliseconds + */ + public void addBanForAddress(InetAddress address, long duration) { + _bannedIps.putIfAbsent(address, System.currentTimeMillis() + duration); + } + + public boolean isBannedAddress(InetAddress address) throws Exception { + String[] parts = address.getHostAddress().split("\\."); + Long bi = _bannedIps.get(address); + if (bi == null) { + bi = _bannedIps.get(InetAddress.getByName(parts[0] + "." + parts[1] + "." + parts[2] + ".0")); + } + if (bi == null) { + bi = _bannedIps.get(InetAddress.getByName(parts[0] + "." + parts[1] + ".0.0")); + } + if (bi == null) { + bi = _bannedIps.get(InetAddress.getByName(parts[0] + ".0.0.0")); + } + if (bi != null) { + if ((bi > 0) && (bi < System.currentTimeMillis())) { + _bannedIps.remove(address); + LOG.info("Removed expired IP address ban {}.", address.getHostAddress()); + return false; + } + return true; + } + return false; + } + + public Map getBannedIps() { + return _bannedIps; + } + + /** + * Remove the specified address from the ban list + * @param address The address to be removed from the ban list + * @return true if the ban was removed, false if there was no ban for this ip + */ + public boolean removeBanForAddress(InetAddress address) { + return _bannedIps.remove(address) != null; + } + + /** + * Remove the specified address from the ban list + * @param address The address to be removed from the ban list + * @return true if the ban was removed, false if there was no ban for this ip or the address was invalid. + */ + public boolean removeBanForAddress(String address) { + try { + return this.removeBanForAddress(InetAddress.getByName(address)); + } catch (Exception e) { + return false; + } + } + + public SessionKey getKeyForAccount(String account) { + L2LoginClient client = _loginServerClients.get(account); + if (client != null) { + return client.getSessionKey(); + } + return null; + } + + public boolean isAccountInAnyGameServer(String account) { + Collection serverList = GameServerTable.getInstance().getRegisteredGameServers().values(); + for (GameServerInfo gsi : serverList) { + GameServerThread gst = gsi.getGameServerThread(); + if ((gst != null) && gst.hasAccountOnGameServer(account)) { + return true; + } + } + return false; + } + + public GameServerInfo getAccountOnGameServer(String account) { + Collection serverList = GameServerTable.getInstance().getRegisteredGameServers().values(); + for (GameServerInfo gsi : serverList) { + GameServerThread gst = gsi.getGameServerThread(); + if ((gst != null) && gst.hasAccountOnGameServer(account)) { + return gsi; + } + } + return null; + } + + public void getCharactersOnAccount(String account) { + Collection serverList = GameServerTable.getInstance().getRegisteredGameServers().values(); + for (GameServerInfo gsi : serverList) { + if (gsi.isAuthed()) { + gsi.getGameServerThread().requestCharacters(account); + } + } + } + + public boolean isLoginPossible(L2LoginClient client, int serverId) { + GameServerInfo gsi = GameServerTable.getInstance().getRegisteredGameServerById(serverId); + int access = client.getAccessLevel(); + if ((gsi != null) && gsi.isAuthed()) { + boolean loginOk = ((gsi.getCurrentPlayerCount() < gsi.getMaxPlayers()) && (gsi.getStatus() != ServerStatus.STATUS_GM_ONLY)) || (access > 0); + + if (loginOk && (client.getLastServer() != serverId)) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(ACCOUNT_LAST_SERVER_UPDATE)) { + ps.setInt(1, serverId); + ps.setString(2, client.getAccount()); + ps.executeUpdate(); + } catch (Exception ex) { + LOG.warn("There has been an error setting last server for account {}!", client.getAccount(), ex); + } + } + return loginOk; + } + return false; + } + + public void setAccountAccessLevel(String account, int banLevel) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(ACCOUNT_ACCESS_LEVEL_UPDATE)) { + ps.setInt(1, banLevel); + ps.setString(2, account); + ps.executeUpdate(); + } catch (Exception ex) { + LOG.warn("There has been an error setting account level for account {}!", account, ex); + } + } + + public void setAccountLastTracert(String account, String pcIp, String hop1, String hop2, String hop3, String hop4) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(ACCOUNT_IPS_UPDATE)) { + ps.setString(1, pcIp); + ps.setString(2, hop1); + ps.setString(3, hop2); + ps.setString(4, hop3); + ps.setString(5, hop4); + ps.setString(6, account); + ps.executeUpdate(); + } catch (Exception ex) { + LOG.warn("There has been an error setting last tracert for account {}!", account, ex); + } + } + + public void setCharactersOnServer(String account, int charsNum, long[] timeToDel, int serverId) { + final var client = _loginServerClients.get(account); + if (client == null) { + return; + } + + if (charsNum > 0) { + client.setCharsOnServ(serverId, charsNum); + } + + if (timeToDel.length > 0) { + client.serCharsWaitingDelOnServ(serverId, timeToDel); + } + } + + /** + * This method returns one of the cached {@link ScrambledKeyPair ScrambledKeyPairs} for communication with Login Clients. + * @return a scrambled keypair + */ + public ScrambledKeyPair getScrambledRSAKeyPair() { + return _keyPairs[Rnd.nextInt(10)]; + } + + /** + * @param client the client + * @param address client host address + * @param info the account info to check in + * @return true when ok to check in, false otherwise + */ + public boolean canCheckIn(L2LoginClient client, InetAddress address, AccountInfo info) { + try { + List ipWhiteList = new ArrayList<>(); + List ipBlackList = new ArrayList<>(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(ACCOUNT_IPAUTH_SELECT)) { + ps.setString(1, info.getLogin()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final var ip = rs.getString("ip"); + if (!isValidIPAddress(ip)) { + continue; + } + + final var type = rs.getString("type"); + if (type.equals("allow")) { + ipWhiteList.add(InetAddress.getByName(ip)); + } else if (type.equals("deny")) { + ipBlackList.add(InetAddress.getByName(ip)); + } + } + } + } + + // Check IP + if (!ipWhiteList.isEmpty() || !ipBlackList.isEmpty()) { + if (!ipWhiteList.isEmpty() && !ipWhiteList.contains(address)) { + LOG.warn("Account checkin attempt from address {} not present on whitelist for account {}!", address.getHostAddress(), info.getLogin()); + return false; + } + + if (!ipBlackList.isEmpty() && ipBlackList.contains(address)) { + LOG.warn("Account checkin attempt from address {} on blacklist for account {}!", address.getHostAddress(), info.getLogin()); + return false; + } + } + + client.setAccessLevel(info.getAccessLevel()); + client.setLastServer(info.getLastServer()); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(ACCOUNT_INFO_UPDATE)) { + ps.setLong(1, System.currentTimeMillis()); + ps.setString(2, address.getHostAddress()); + ps.setString(3, info.getLogin()); + ps.execute(); + } + return true; + } catch (Exception ex) { + LOG.warn("There has been an error logging in!", ex); + return false; + } + } + + public boolean isValidIPAddress(String ipAddress) { + String[] parts = ipAddress.split("\\."); + if (parts.length != 4) { + return false; + } + + for (String s : parts) { + int i = Integer.parseInt(s); + if ((i < 0) || (i > 255)) { + return false; + } + } + return true; + } + + class PurgeThread extends Thread { + public PurgeThread() { + setName("PurgeThread"); + } + + @Override + public void run() { + while (!isInterrupted()) { + for (L2LoginClient client : _loginServerClients.values()) { + if (client == null) { + continue; + } + if ((client.getConnectionStartTime() + LOGIN_TIMEOUT) < System.currentTimeMillis()) { + client.close(LoginFailReason.REASON_ACCESS_FAILED); + } + } + + try { + Thread.sleep(LOGIN_TIMEOUT / 2); + } catch (InterruptedException e) { + return; + } + } + } + } + + public enum AuthLoginResult { + INVALID_PASSWORD, + ACCOUNT_INACTIVE, + ACCOUNT_BANNED, + ALREADY_ON_LS, + ALREADY_ON_GS, + AUTH_SUCCESS + } + + public static LoginController getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final LoginController INSTANCE = new LoginController(); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/LoginServer.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/LoginServer.java new file mode 100644 index 00000000..fa2b03c3 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/LoginServer.java @@ -0,0 +1,221 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver; + +import static com.l2jserver.loginserver.config.Configuration.database; +import static com.l2jserver.loginserver.config.Configuration.email; +import static com.l2jserver.loginserver.config.Configuration.mmo; +import static com.l2jserver.loginserver.config.Configuration.server; +import static com.l2jserver.loginserver.config.Configuration.telnet; + +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.net.InetAddress; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.UPnPService; +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.loginserver.mail.MailSystem; +import com.l2jserver.loginserver.network.L2LoginClient; +import com.l2jserver.loginserver.network.L2LoginPacketHandler; +import com.l2jserver.loginserver.status.Status; +import com.l2jserver.mmocore.SelectorConfig; +import com.l2jserver.mmocore.SelectorThread; + +/** + * Login Server. + * @author KenM + * @author Zoey76 + * @version 2.6.1.0 + */ +public final class LoginServer { + + private static final Logger LOG = LoggerFactory.getLogger(LoginServer.class); + + private static final String BANNED_IPS = "/config/banned_ip.cfg"; + + private static LoginServer _instance; + + private GameServerListener _gameServerListener; + + private SelectorThread _selectorThread; + + private Status _statusServer; + + public static void main(String[] args) { + new LoginServer(); + } + + public static LoginServer getInstance() { + return _instance; + } + + private LoginServer() { + _instance = this; + + // Prepare Database + ConnectionFactory.builder() // + .withUrl(database().getURL()) // + .withUser(database().getUser()) // + .withPassword(database().getPassword()) // + .withMaxIdleTime(database().getMaxIdleTime()) // + .withMaxPoolSize(database().getMaxConnections()) // + .build(); + + LoginController.getInstance(); + + GameServerTable.getInstance(); + + loadBanFile(); + + if (email().isEnabled()) { + MailSystem.getInstance(); + } + + final SelectorConfig sc = new SelectorConfig(); + sc.MAX_READ_PER_PASS = mmo().getMaxReadPerPass(); + sc.MAX_SEND_PER_PASS = mmo().getMaxSendPerPass(); + sc.SLEEP_TIME = mmo().getSleepTime(); + sc.HELPER_BUFFER_COUNT = mmo().getHelperBufferCount(); + + final L2LoginPacketHandler loginPacketHandler = new L2LoginPacketHandler(); + final SelectorHelper selectorHelper = new SelectorHelper(); + try { + _selectorThread = new SelectorThread<>(sc, selectorHelper, loginPacketHandler, selectorHelper, selectorHelper); + } catch (Exception ex) { + LOG.error("Failed to open Selector!", ex); + System.exit(1); + } + + try { + _gameServerListener = new GameServerListener(); + _gameServerListener.start(); + LOG.info("Listening for game servers on {}:{}.", server().getGameServerHost(), server().getGameServerPort()); + } catch (Exception ex) { + LOG.error("Failed to start the Game Server Listener!", ex); + System.exit(1); + } + + if (telnet().isEnabled()) { + try { + _statusServer = new Status(); + _statusServer.start(); + } catch (Exception ex) { + LOG.warn("Failed to start the Telnet Server!", ex); + } + } else { + LOG.info("Telnet server is currently disabled."); + } + + InetAddress bindAddress = null; + if (!server().getHost().equals("*")) { + try { + bindAddress = InetAddress.getByName(server().getGameServerHost()); + } catch (Exception ex) { + LOG.warn("The Login Server bind address is invalid, using all available IPs!", ex); + } + } + try { + _selectorThread.openServerSocket(bindAddress, server().getPort()); + _selectorThread.start(); + LOG.info("Login Server is now listening on {}:{}.", server().getHost(), server().getPort()); + } catch (Exception ex) { + LOG.error("Failed to open server socket!", ex); + System.exit(1); + } + + if (server().isUPnPEnabled()) { + UPnPService.getInstance().load(server().getPort(), "L2J Login Server"); + } + } + + public Status getStatusServer() { + return _statusServer; + } + + public GameServerListener getGameServerListener() { + return _gameServerListener; + } + + private void loadBanFile() { + try (var fis = getClass().getResourceAsStream(BANNED_IPS); + var is = new InputStreamReader(fis); + var lnr = new LineNumberReader(is)) { + lnr.lines() // + .map(String::trim) // + .filter(l -> !l.isEmpty() && (l.charAt(0) != '#')) // + .forEach(line -> { + String[] parts = line.split("#", 2); // address[ duration][ # comments] + line = parts[0]; + parts = line.split("\\s+"); // durations might be aligned via multiple spaces + String address = parts[0]; + long duration = 0; + + if (parts.length > 1) { + try { + duration = Long.parseLong(parts[1]); + } catch (Exception ex) { + LOG.warn("Incorrect ban duration {} on line {} on file {}!", parts[1], lnr.getLineNumber(), BANNED_IPS, ex); + return; + } + } + + try { + LoginController.getInstance().addBanForAddress(address, duration); + } catch (Exception ex) { + LOG.warn("Invalid address {} on line {} on file {}!", address, lnr.getLineNumber(), BANNED_IPS, ex); + } + }); + } catch (Exception ex) { + LOG.warn("Error while reading the bans file {}!", BANNED_IPS, ex); + } + LOG.info("Loaded {} banned IPs.", LoginController.getInstance().getBannedIps().size()); + + if (server().isLoginRestartEnabled()) { + final var restartLoginServer = new LoginServerRestart(); + restartLoginServer.setDaemon(true); + restartLoginServer.start(); + LOG.info("Scheduled restart after {} hours.", server().getLoginRestartTime()); + } + } + + class LoginServerRestart extends Thread { + public LoginServerRestart() { + setName("LoginServerRestart"); + } + + @Override + public void run() { + while (!isInterrupted()) { + try { + Thread.sleep(server().getLoginRestartTime() * 3600000); + } catch (InterruptedException e) { + return; + } + shutdown(true); + } + } + } + + public void shutdown(boolean restart) { + Runtime.getRuntime().exit(restart ? 2 : 0); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/SelectorHelper.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/SelectorHelper.java new file mode 100644 index 00000000..8b0426fd --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/SelectorHelper.java @@ -0,0 +1,79 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.nio.channels.SocketChannel; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.IPv4Filter; +import com.l2jserver.loginserver.network.L2LoginClient; +import com.l2jserver.loginserver.network.serverpackets.Init; +import com.l2jserver.mmocore.AcceptFilter; +import com.l2jserver.mmocore.ClientFactory; +import com.l2jserver.mmocore.MMOConnection; +import com.l2jserver.mmocore.MMOExecutor; +import com.l2jserver.mmocore.ReceivablePacket; + +/** + * Selector Helper. + * @author KenM + * @author Zoey76 + * @version 2.6.1.0 + */ +public class SelectorHelper implements MMOExecutor, ClientFactory, AcceptFilter { + + private static final Logger LOG = LoggerFactory.getLogger(SelectorHelper.class); + + private final ThreadPoolExecutor _generalPacketsThreadPool; + + private final IPv4Filter _ipv4filter; + + public SelectorHelper() { + _generalPacketsThreadPool = new ThreadPoolExecutor(4, 6, 15L, SECONDS, new LinkedBlockingQueue<>()); + _ipv4filter = new IPv4Filter(); + } + + @Override + public void execute(ReceivablePacket packet) { + _generalPacketsThreadPool.execute(packet); + } + + @Override + public L2LoginClient create(MMOConnection con) { + L2LoginClient client = new L2LoginClient(con); + client.sendPacket(new Init(client)); + return client; + } + + @Override + public boolean accept(SocketChannel sc) { + try { + return _ipv4filter.accept(sc) && !LoginController.getInstance().isBannedAddress(sc.socket().getInetAddress()); + } catch (Exception ex) { + LOG.error("Invalid address {}!", sc.socket().getInetAddress(), ex); + } + return false; + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/SessionKey.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/SessionKey.java new file mode 100644 index 00000000..378c7591 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/SessionKey.java @@ -0,0 +1,70 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver; + +import static com.l2jserver.loginserver.config.Configuration.server; + +/** + * This class is used to represent session keys used by the client to authenticate in the game server.
+ * A SessionKey is made up of two 8 bytes keys. One is sent in the {@link com.l2jserver.loginserver.network.serverpackets.LoginOk#LoginOk} packet and the other is sent in {@link com.l2jserver.loginserver.network.serverpackets.PlayOk#PlayOk} + * @author -Wooden- + * @version 2.6.1.0 + */ +public class SessionKey { + public final int playOkID1; + public final int playOkID2; + public final int loginOkID1; + public final int loginOkID2; + + public SessionKey(int loginOK1, int loginOK2, int playOK1, int playOK2) { + playOkID1 = playOK1; + playOkID2 = playOK2; + loginOkID1 = loginOK1; + loginOkID2 = loginOK2; + } + + @Override + public String toString() { + return "PlayOk: " + playOkID1 + " " + playOkID2 + " LoginOk:" + loginOkID1 + " " + loginOkID2; + } + + public boolean checkLoginPair(int loginOk1, int loginOk2) { + return (loginOkID1 == loginOk1) && (loginOkID2 == loginOk2); + } + + /** + * Only checks the PlayOk part of the session key if server doesn't show the license when player logs in. + * @param other the other session key to validate + * @return true if keys are equal. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SessionKey key)) { + return false; + } + // when server doesn't show license it doesn't send the LoginOk packet, client doesn't have this part of the key then. + if (server().showLicense()) { + return ((playOkID1 == key.playOkID1) && (loginOkID1 == key.loginOkID1) && (playOkID2 == key.playOkID2) && (loginOkID2 == key.loginOkID2)); + } + return ((playOkID1 == key.playOkID1) && (playOkID2 == key.playOkID2)); + } +} \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/Configuration.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/Configuration.java new file mode 100644 index 00000000..dfa814c2 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/Configuration.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.config; + +import org.aeonbits.owner.ConfigFactory; + +/** + * Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class Configuration { + + private static final EmailConfiguration email = ConfigFactory.create(EmailConfiguration.class); + + private static final ServerConfiguration server = ConfigFactory.create(ServerConfiguration.class); + + private static final DatabaseConfiguration database = ConfigFactory.create(DatabaseConfiguration.class); + + private static final MMOConfiguration mmo = ConfigFactory.create(MMOConfiguration.class); + + private static final TelnetConfiguration telnet = ConfigFactory.create(TelnetConfiguration.class); + + private Configuration() { + // Do nothing. + } + + public static EmailConfiguration email() { + return email; + } + + public static ServerConfiguration server() { + return server; + } + + public static DatabaseConfiguration database() { + return database; + } + + public static MMOConfiguration mmo() { + return mmo; + } + + public static TelnetConfiguration telnet() { + return telnet; + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/DatabaseConfiguration.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/DatabaseConfiguration.java new file mode 100644 index 00000000..0f8e4a3d --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/DatabaseConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config; +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; + +/** + * Database Configuration. + * @author Zoey76 + * @version 2.6.1.5 + */ +@Sources({ + "file:${L2J_HOME}/custom/login/config/database.properties", + "file:./config/database.properties", + "classpath:config/database.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface DatabaseConfiguration extends Config { + + @Key("URL") + String getURL(); + + @Key("User") + String getUser(); + + @Key("Password") + String getPassword(); + + @Key("MaxConnections") + Integer getMaxConnections(); + + @Key("MaxIdleTime") + Integer getMaxIdleTime(); +} \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/EmailConfiguration.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/EmailConfiguration.java new file mode 100644 index 00000000..891f41ab --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/EmailConfiguration.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config; +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; + +/** + * Email Configuration. + * @author Zoey76 + * @version 2.6.1.1 + */ +@Sources({ + "file:${L2J_HOME}/custom/login/config/email.properties", + "file:./config/email.properties", + "classpath:config/email.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface EmailConfiguration extends Config { + + @Key("EnableEmail") + boolean isEnabled(); + + @Key("ServerName") + String getServerName(); + + @Key("ServerEmail") + String getServerEmail(); + + @Key("SelectQuery") + String getSelectQuery(); + + @Key("DatabaseField") + String getDatabaseField(); + + @Key("Host") + String getHost(); + + @Key("Port") + int getPort(); + + @Key("SmtpAuthRequired") + boolean isSmtpAuthRequired(); + + @Key("SmtpFactory") + String getSmtpFactory(); + + @Key("SmtpFactoryCallback") + boolean smtpFactoryCallback(); + + @Key("SmtpUsername") + String getSmtpUsername(); + + @Key("SmtpPassword") + String getSmtpPassword(); + + @Key("SystemAddress") + String getSystemAddress(); +} \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/MMOConfiguration.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/MMOConfiguration.java new file mode 100644 index 00000000..348fcaf3 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/MMOConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config; +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; + +/** + * MMOConfiguration. + * @author Zoey76 + * @version 2.6.1.1 + */ +@Sources({ + "file:${L2J_HOME}/custom/login/config/mmo.properties", + "file:./config/mmo.properties", + "classpath:config/mmo.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface MMOConfiguration extends Config { + + @Key("SleepTime") + int getSleepTime(); + + @Key("MaxSendPerPass") + int getMaxSendPerPass(); + + @Key("MaxReadPerPass") + int getMaxReadPerPass(); + + @Key("HelperBufferCount") + int getHelperBufferCount(); + + @Key("TcpNoDelay") + boolean isTcpNoDelay(); +} \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/ServerConfiguration.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/ServerConfiguration.java new file mode 100644 index 00000000..7dde5d17 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/ServerConfiguration.java @@ -0,0 +1,111 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.Set; + +import org.aeonbits.owner.Config; +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; + +/** + * Server Configuration. + * @author Zoey76 + * @version 2.6.1.1 + */ +@Sources({ + "file:${L2J_HOME}/custom/login/config/server.properties", + "file:./config/server.properties", + "classpath:config/server.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface ServerConfiguration extends Config { + + @Key("EnableUPnP") + boolean isUPnPEnabled(); + + @Key("Host") + String getHost(); + + @Key("Port") + int getPort(); + + @Key("GameServerHost") + String getGameServerHost(); + + @Key("GameServerPort") + int getGameServerPort(); + + @Key("ServerVersions") + Set getServerVersions(); + + @Key("LoginTryBeforeBan") + int getLoginTryBeforeBan(); + + @Key("LoginBlockAfterBan") + int getLoginBlockAfterBan(); + + @Key("AcceptNewGameServer") + boolean isAcceptNetGameServer(); + + @Key("EnableFloodProtection") + boolean isFloodProtectionEnabled(); + + @Key("FastConnectionLimit") + int getFastConnectionLimit(); + + @Key("NormalConnectionTime") + int getNormalConnectionTime(); + + @Key("FastConnectionTime") + int getFastConnectionTime(); + + @Key("MaxConnectionPerIP") + int getMaxConnectionPerIP(); + + @Key("AccountInactiveAccessLevel") + int getAccountInactiveAccessLevel(); + + @Key("ShowLicence") + boolean showLicense(); + + @Key("AutoCreateAccounts") + boolean autoCreateAccounts(); + + @Key("AutoCreateAccountsAccessLevel") + int autoCreateAccountsAccessLevel(); + + @Key("DatapackRoot") + String getDatapackRoot(); + + @Key("Debug") + boolean isDebug(); + + @Key("LoginRestartSchedule") + boolean isLoginRestartEnabled(); + + @Key("LoginRestartTime") + int getLoginRestartTime(); +} \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/TelnetConfiguration.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/TelnetConfiguration.java new file mode 100644 index 00000000..6d16a438 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/config/TelnetConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.List; + +import org.aeonbits.owner.Config; +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; + +/** + * Telnet Configuration. + * @author Zoey76 + * @version 2.6.1.1 + */ +@Sources({ + "file:${L2J_HOME}/custom/login/config/telnet.properties", + "file:./config/telnet.properties", + "classpath:config/telnet.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface TelnetConfiguration extends Config { + + @Key("EnableTelnet") + boolean isEnabled(); + + @Key("Port") + int getPort(); + + @Key("Password") + String getPassword(); + + @Key("Hosts") + List getHosts(); +} \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/mail/BaseMail.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/mail/BaseMail.java new file mode 100644 index 00000000..ee8e4b53 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/mail/BaseMail.java @@ -0,0 +1,135 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.mail; + +import static com.l2jserver.loginserver.config.Configuration.email; + +import java.io.UnsupportedEncodingException; +import java.util.Properties; + +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; + +/** + * Base Mail. + * @author mrTJO + * @version 2.6.1.0 + */ +public class BaseMail implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(BaseMail.class); + + private MimeMessage _messageMime = null; + + private static class SmtpAuthenticator extends Authenticator { + private final PasswordAuthentication _auth; + + public SmtpAuthenticator() { + _auth = new PasswordAuthentication(email().getSmtpUsername(), email().getSmtpPassword()); + } + + @Override + public PasswordAuthentication getPasswordAuthentication() { + return _auth; + } + } + + public BaseMail(String account, String mailId, String... args) { + final var mailAddr = getUserMail(account); + if (mailAddr == null) { + return; + } + + final var content = MailSystem.getInstance().getMailContent(mailId); + if (content == null) { + return; + } + + final var message = compileHtml(account, content.getText(), args); + final var mailProp = new Properties(); + mailProp.put("mail.smtp.host", email().getHost()); + mailProp.put("mail.smtp.auth", email().isSmtpAuthRequired()); + mailProp.put("mail.smtp.port", email().getPort()); + mailProp.put("mail.smtp.socketFactory.port", email().getPort()); + mailProp.put("mail.smtp.socketFactory.class", email().getSmtpFactory()); + mailProp.put("mail.smtp.socketFactory.fallback", email().smtpFactoryCallback()); + final var authenticator = (email().isSmtpAuthRequired() ? new SmtpAuthenticator() : null); + final var mailSession = Session.getDefaultInstance(mailProp, authenticator); + + try { + _messageMime = new MimeMessage(mailSession); + _messageMime.setSubject(content.getSubject()); + try { + _messageMime.setFrom(new InternetAddress(email().getServerEmail(), email().getServerName())); + } catch (UnsupportedEncodingException ex) { + LOG.warn("Sender address {} is not Valid!", email().getServerEmail()); + } + _messageMime.setContent(message, "text/html"); + _messageMime.setRecipient(Message.RecipientType.TO, new InternetAddress(mailAddr)); + } catch (MessagingException ex) { + LOG.warn("There has been an error sending the email!", ex); + } + } + + private String compileHtml(String account, String html, String[] args) { + if (args != null) { + for (int i = 0; i < args.length; i++) { + html = html.replace("%var" + i + "%", args[i]); + } + } + return html.replace("%accountname%", account); + } + + private String getUserMail(String username) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.prepareStatement(email().getSelectQuery())) { + statement.setString(1, username); + try (var rs = statement.executeQuery()) { + if (rs.next()) { + return rs.getString(email().getDatabaseField()); + } + } + } catch (Exception ex) { + LOG.warn("Cannot select user mail!", ex); + } + return null; + } + + @Override + public void run() { + try { + if (_messageMime != null) { + Transport.send(_messageMime); + } + } catch (MessagingException ex) { + LOG.warn("There has been an error while sending email!", ex); + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/mail/MailSystem.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/mail/MailSystem.java new file mode 100644 index 00000000..200e3980 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/mail/MailSystem.java @@ -0,0 +1,134 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.mail; + +import static com.l2jserver.loginserver.config.Configuration.email; +import static com.l2jserver.loginserver.config.Configuration.server; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +/** + * Mail System. + * @author mrTJO + * @version 2.6.1.0 + */ +public class MailSystem { + + private static final Logger LOG = LoggerFactory.getLogger(MailSystem.class); + + private final Map _mailData = new HashMap<>(); + + public MailSystem() { + loadMails(); + } + + public void sendMail(String account, String messageId, String... args) { + BaseMail mail = new BaseMail(account, messageId, args); + mail.run(); + } + + private void loadMails() { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setIgnoringComments(true); + File file = new File(server().getDatapackRoot() + "data/mail/MailList.xml"); + if (!file.exists()) { + LOG.warn("Cannot load email system - Missing file MailList.xml"); + return; + } + + Document doc; + try { + doc = factory.newDocumentBuilder().parse(file); + } catch (Exception ex) { + LOG.warn("Could not parse MailList.xml file!", ex); + return; + } + + Node n = doc.getFirstChild(); + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if (d.getNodeName().equals("mail")) { + String mailId = d.getAttributes().getNamedItem("id").getNodeValue(); + String subject = d.getAttributes().getNamedItem("subject").getNodeValue(); + String maFile = d.getAttributes().getNamedItem("file").getNodeValue(); + + File mailFile = new File(server().getDatapackRoot() + "data/mail/" + maFile); + try (FileInputStream fis = new FileInputStream(mailFile); + BufferedInputStream bis = new BufferedInputStream(fis)) { + int bytes = bis.available(); + byte[] raw = new byte[bytes]; + + bis.read(raw); + String html = new String(raw, UTF_8); + html = html.replaceAll(System.lineSeparator(), "\n"); + html = html.replace("%servermail%", email().getServerEmail()); + html = html.replace("%servername%", email().getServerName()); + + _mailData.put(mailId, new MailContent(subject, html)); + } catch (IOException ex) { + LOG.warn("There has been an error while reading {}!", maFile, ex); + } + } + } + LOG.info("Email system loaded."); + } + + public static class MailContent { + private final String _subject; + private final String _text; + + public MailContent(String subject, String text) { + _subject = subject; + _text = text; + } + + public String getSubject() { + return _subject; + } + + public String getText() { + return _text; + } + } + + public MailContent getMailContent(String mailId) { + return _mailData.get(mailId); + } + + public static MailSystem getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final MailSystem INSTANCE = new MailSystem(); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/model/AccountInfo.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/model/AccountInfo.java new file mode 100644 index 00000000..a287fa71 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/model/AccountInfo.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.model; + +import java.util.Objects; + +/** + * Account Info. + * @author HorridoJoho + * @version 2.6.1.0 + */ +public final class AccountInfo { + private final String _login; + private final String _passHash; + private final int _accessLevel; + private final int _lastServer; + + public AccountInfo(final String login, final String passHash, final int accessLevel, final int lastServer) { + Objects.requireNonNull(login, "login parameter is null"); + Objects.requireNonNull(passHash, "passHash parameter is null"); + + if (login.isEmpty()) { + throw new IllegalArgumentException("login string is empty"); + } + if (passHash.isEmpty()) { + throw new IllegalArgumentException("passHash string is empty"); + } + + _login = login.toLowerCase(); + _passHash = passHash; + _accessLevel = accessLevel; + _lastServer = lastServer; + } + + public boolean checkPassHash(final String passHash) { + return _passHash.equals(passHash); + } + + public String getLogin() { + return _login; + } + + public int getAccessLevel() { + return _accessLevel; + } + + public int getLastServer() { + return _lastServer; + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/model/GameServerVersion.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/model/GameServerVersion.java new file mode 100644 index 00000000..4fabfc42 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/model/GameServerVersion.java @@ -0,0 +1,107 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.model; + +import java.util.HashMap; +import java.util.Map; + +/** + * Game Server version. + * @author Zoey76 + * @version 2.6.1.8 + */ +public enum GameServerVersion { + // Saga I: The Chaotic Chronicle + PRELUDE_CLOSED_BETA(-1, "Prelude Closed Beta"), + PRELUDE(0, "Prelude"), + HARBINGERS_OF_WAR(1, "Chronicle I: Harbingers of War"), + AGE_OF_SPLENDOR(2, "Chronicle II: Age of Splendor"), + RISE_OF_DARKNESS(3, "Chronicle III: Rise of Darkness"), + SCIONS_OF_DESTINY(4, "Chronicle IV: Scions of Destiny"), + OATH_OF_BLOOD(5, "Chronicle V: Oath of Blood"), + // Saga II: The Chaotic Throne + INTERLUDE(6, "Interlude"), + // Throne I + THE_KAMAEL(7, "The Kamael"), + HELLBOUND(8, "Hellbound"), + // Throne II + GRACIA_1(9, "Gracia"), + GRACIA_2(10, "Gracia Part 2"), + GRACIA_FINAL(11, "Gracia Final"), + EPILOGUE(12, "Gracia Epilogue"), + FREYA(13, "Freya"), + HIGH_FIVE(14, "High Five"), + // Saga III: Goddess of Destruction + // Chapter 1 + AWAKENING(15, "Awakening"), + HARMONY(16, "Harmony"), + // Chapter 2 + TAUTI(17, "Tauti"), + GLORY_DAYS(18, "Glory Days"), + // Chapter 3 + LIVINDOR(19, "Lindvior"), + // Saga IV: Epic Tale of Aden + // Episode 1 + ERTHEIA(20, "Ertheia"), + // Episode 2 + VALIANCE(21, "Valiance"), + // Episode 2.5 + INFINITE_ODYSSEY(22, "Infinite Odyssey"), + UNDERGROUND(23, "Underground"), + HELIOS(24, "Helios"), + GRAND_CRUSADE(25, "Grand Crusade"), + SALVATION(26, "Salvation"), + ETINAS_FATE(27, "Etina's Fate"), + FAFURION(28, "Fafurion"), + PREULDE_OF_WAR(29, "Prelude of War"), + DAWN_OF_HEROES(30, "Dawn of Heroes"); + + static final Map versions = new HashMap<>(); + + int id; + + String description; + + static { + for (var p : GameServerVersion.values()) { + versions.put(p.getId(), p.getDescription()); + } + } + + GameServerVersion(int id, String description) { + this.id = id; + this.description = description; + } + + public int getId() { + return id; + } + + public String getDescription() { + return description; + } + + public static Map versions() { + return versions; + } + + public static String valueOf(int id) { + return versions.get(id); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2JGameServerPacketHandler.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2JGameServerPacketHandler.java new file mode 100644 index 00000000..1daf5f18 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2JGameServerPacketHandler.java @@ -0,0 +1,121 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network; + +import static com.l2jserver.loginserver.config.Configuration.email; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.GameServerThread; +import com.l2jserver.loginserver.network.gameserverpackets.BlowFishKey; +import com.l2jserver.loginserver.network.gameserverpackets.ChangeAccessLevel; +import com.l2jserver.loginserver.network.gameserverpackets.ChangePassword; +import com.l2jserver.loginserver.network.gameserverpackets.GameServerAuth; +import com.l2jserver.loginserver.network.gameserverpackets.PlayerAuthRequest; +import com.l2jserver.loginserver.network.gameserverpackets.PlayerInGame; +import com.l2jserver.loginserver.network.gameserverpackets.PlayerLogout; +import com.l2jserver.loginserver.network.gameserverpackets.PlayerTracert; +import com.l2jserver.loginserver.network.gameserverpackets.ReplyCharacters; +import com.l2jserver.loginserver.network.gameserverpackets.RequestSendMail; +import com.l2jserver.loginserver.network.gameserverpackets.RequestTempBan; +import com.l2jserver.loginserver.network.gameserverpackets.ServerStatus; +import com.l2jserver.loginserver.network.loginserverpackets.LoginServerFail; + +/** + * Game Server packet handler. + * @author mrTJO + * @version 2.6.1.0 + */ +public class L2JGameServerPacketHandler { + + private static final Logger LOG = LoggerFactory.getLogger(L2JGameServerPacketHandler.class); + + public enum GameServerState { + CONNECTED, + BF_CONNECTED, + AUTHED + } + + public static BaseRecievePacket handlePacket(byte[] data, GameServerThread server) { + BaseRecievePacket msg = null; + int opcode = data[0] & 0xff; + GameServerState state = server.getLoginConnectionState(); + switch (state) { + case CONNECTED: + if (opcode == 0x00) { + msg = new BlowFishKey(data, server); + } else { + LOG.warn("Unknown Opcode {} in state {} from game server, closing connection!", Integer.toHexString(opcode).toUpperCase(), state); + server.forceClose(LoginServerFail.NOT_AUTHED); + } + break; + case BF_CONNECTED: + if (opcode == 0x01) { + msg = new GameServerAuth(data, server); + } else { + LOG.warn("Unknown Opcode {} in state {} from game server, closing connection!", Integer.toHexString(opcode).toUpperCase(), state); + server.forceClose(LoginServerFail.NOT_AUTHED); + } + break; + case AUTHED: + switch (opcode) { + case 0x02: + msg = new PlayerInGame(data, server); + break; + case 0x03: + msg = new PlayerLogout(data, server); + break; + case 0x04: + msg = new ChangeAccessLevel(data, server); + break; + case 0x05: + msg = new PlayerAuthRequest(data, server); + break; + case 0x06: + msg = new ServerStatus(data, server); + break; + case 0x07: + msg = new PlayerTracert(data); + break; + case 0x08: + msg = new ReplyCharacters(data, server); + break; + case 0x09: + if (email().isEnabled()) { + msg = new RequestSendMail(data); + } + break; + case 0x0A: + msg = new RequestTempBan(data); + break; + case 0x0B: + new ChangePassword(data); + break; + default: + LOG.warn("Unknown Opcode {} in state {} from GameServer, closing connection!", Integer.toHexString(opcode).toUpperCase(), state); + server.forceClose(LoginServerFail.NOT_AUTHED); + break; + } + break; + } + return msg; + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2LoginClient.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2LoginClient.java new file mode 100644 index 00000000..fbcbe0a2 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2LoginClient.java @@ -0,0 +1,252 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.security.interfaces.RSAPrivateKey; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.loginserver.LoginController; +import com.l2jserver.loginserver.SessionKey; +import com.l2jserver.loginserver.network.serverpackets.L2LoginServerPacket; +import com.l2jserver.loginserver.network.serverpackets.LoginFail; +import com.l2jserver.loginserver.network.serverpackets.LoginFail.LoginFailReason; +import com.l2jserver.loginserver.network.serverpackets.PlayFail; +import com.l2jserver.loginserver.network.serverpackets.PlayFail.PlayFailReason; +import com.l2jserver.loginserver.security.ScrambledKeyPair; +import com.l2jserver.loginserver.security.crypt.LoginCrypt; +import com.l2jserver.mmocore.MMOClient; +import com.l2jserver.mmocore.MMOConnection; +import com.l2jserver.mmocore.SendablePacket; + +/** + * Represents a client connected into the LoginServer + * @author KenM + * @version 2.6.1.0 + */ +public final class L2LoginClient extends MMOClient> { + + private static final Logger LOG = LoggerFactory.getLogger(L2LoginClient.class); + + public enum LoginClientState { + CONNECTED, + AUTHED_GG, + AUTHED_LOGIN + } + + private LoginClientState _state; + + private final LoginCrypt _loginCrypt; + + private final ScrambledKeyPair _scrambledPair; + + private final byte[] _blowfishKey; + + private String _account; + + private int _accessLevel; + + private int _lastServer; + + private SessionKey _sessionKey; + + private final int _sessionId; + + private boolean _joinedGS; + + private Map _charsOnServers; + + private Map _charsToDelete; + + private final long _connectionStartTime; + + public L2LoginClient(MMOConnection con) { + super(con); + _state = LoginClientState.CONNECTED; + _scrambledPair = LoginController.getInstance().getScrambledRSAKeyPair(); + _blowfishKey = LoginController.getInstance().getBlowfishKey(); + _sessionId = Rnd.nextInt(); + _connectionStartTime = System.currentTimeMillis(); + _loginCrypt = new LoginCrypt(); + _loginCrypt.setKey(_blowfishKey); + } + + @Override + public boolean decrypt(ByteBuffer buf, int size) { + try { + if (!_loginCrypt.decrypt(buf.array(), buf.position(), size)) { + LOG.warn("Wrong checksum from client {}!", this); + super.getConnection().close((SendablePacket) null); + return false; + } + return true; + } catch (IOException ex) { + LOG.warn("There has been an error decrypting message!", ex); + super.getConnection().close((SendablePacket) null); + return false; + } + } + + @Override + public boolean encrypt(ByteBuffer buf, int size) { + final int offset = buf.position(); + try { + size = _loginCrypt.encrypt(buf.array(), offset, size); + } catch (IOException ex) { + LOG.warn("There has been an error encrypting message!", ex); + return false; + } + buf.position(offset + size); + return true; + } + + public LoginClientState getState() { + return _state; + } + + public void setState(LoginClientState state) { + _state = state; + } + + public byte[] getBlowfishKey() { + return _blowfishKey; + } + + public byte[] getScrambledModulus() { + return _scrambledPair.getScrambledModulus(); + } + + public RSAPrivateKey getRSAPrivateKey() { + return (RSAPrivateKey) _scrambledPair.getPair().getPrivate(); + } + + public String getAccount() { + return _account; + } + + public void setAccount(String account) { + _account = account; + } + + public void setAccessLevel(int accessLevel) { + _accessLevel = accessLevel; + } + + public int getAccessLevel() { + return _accessLevel; + } + + public void setLastServer(int lastServer) { + _lastServer = lastServer; + } + + public int getLastServer() { + return _lastServer; + } + + public int getSessionId() { + return _sessionId; + } + + public boolean hasJoinedGS() { + return _joinedGS; + } + + public void setJoinedGS(boolean val) { + _joinedGS = val; + } + + public void setSessionKey(SessionKey sessionKey) { + _sessionKey = sessionKey; + } + + public SessionKey getSessionKey() { + return _sessionKey; + } + + public long getConnectionStartTime() { + return _connectionStartTime; + } + + public void sendPacket(L2LoginServerPacket lsp) { + getConnection().sendPacket(lsp); + } + + public void close(LoginFailReason reason) { + getConnection().close(new LoginFail(reason)); + } + + public void close(PlayFailReason reason) { + getConnection().close(new PlayFail(reason)); + } + + public void close(L2LoginServerPacket lsp) { + getConnection().close(lsp); + } + + public void setCharsOnServ(int servId, int chars) { + if (_charsOnServers == null) { + _charsOnServers = new HashMap<>(); + } + _charsOnServers.put(servId, chars); + } + + public Map getCharsOnServ() { + return _charsOnServers; + } + + public void serCharsWaitingDelOnServ(int servId, long[] charsToDel) { + if (_charsToDelete == null) { + _charsToDelete = new HashMap<>(); + } + _charsToDelete.put(servId, charsToDel); + } + + public Map getCharsWaitingDelOnServ() { + return _charsToDelete; + } + + @Override + public void onDisconnection() { + if (!hasJoinedGS() || ((getConnectionStartTime() + LoginController.LOGIN_TIMEOUT) < System.currentTimeMillis())) { + LoginController.getInstance().removeAuthedLoginClient(getAccount()); + } + } + + @Override + public String toString() { + InetAddress address = getConnection().getInetAddress(); + if (getState() == LoginClientState.AUTHED_LOGIN) { + return "[" + getAccount() + " (" + (address == null ? "disconnected" : address.getHostAddress()) + ")]"; + } + return "[" + (address == null ? "disconnected" : address.getHostAddress()) + "]"; + } + + @Override + protected void onForcedDisconnection() { + // Empty + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2LoginPacketHandler.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2LoginPacketHandler.java new file mode 100644 index 00000000..fd5e97d1 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/L2LoginPacketHandler.java @@ -0,0 +1,79 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network; + +import java.nio.ByteBuffer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.loginserver.network.L2LoginClient.LoginClientState; +import com.l2jserver.loginserver.network.clientpackets.AuthGameGuard; +import com.l2jserver.loginserver.network.clientpackets.RequestAuthLogin; +import com.l2jserver.loginserver.network.clientpackets.RequestServerList; +import com.l2jserver.loginserver.network.clientpackets.RequestServerLogin; +import com.l2jserver.mmocore.PacketHandler; +import com.l2jserver.mmocore.ReceivablePacket; + +/** + * Handler for packets received by Login Server. + * @author KenM + * @version 2.6.1.0 + */ +public final class L2LoginPacketHandler implements PacketHandler { + + private static final Logger LOG = LoggerFactory.getLogger(L2LoginPacketHandler.class); + + @Override + public ReceivablePacket handlePacket(ByteBuffer buf, L2LoginClient client) { + int opcode = buf.get() & 0xFF; + + ReceivablePacket packet = null; + LoginClientState state = client.getState(); + + switch (state) { + case CONNECTED: + if (opcode == 0x07) { + packet = new AuthGameGuard(); + } else { + debugOpcode(opcode, state); + } + break; + case AUTHED_GG: + if (opcode == 0x00) { + packet = new RequestAuthLogin(); + } else { + debugOpcode(opcode, state); + } + break; + case AUTHED_LOGIN: + switch (opcode) { + case 0x02 -> packet = new RequestServerLogin(); + case 0x05 -> packet = new RequestServerList(); + default -> debugOpcode(opcode, state); + } + break; + } + return packet; + } + + private void debugOpcode(int opcode, LoginClientState state) { + LOG.warn("Unknown Opcode {} for state {}!", opcode, state.name()); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/AuthGameGuard.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/AuthGameGuard.java new file mode 100644 index 00000000..ad4e6d0f --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/AuthGameGuard.java @@ -0,0 +1,79 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.clientpackets; + +import com.l2jserver.loginserver.network.L2LoginClient.LoginClientState; +import com.l2jserver.loginserver.network.serverpackets.GGAuth; +import com.l2jserver.loginserver.network.serverpackets.LoginFail.LoginFailReason; + +/** + * Format: ddddd + * @author -Wooden- + * @version 2.6.1.0 + */ +public class AuthGameGuard extends L2LoginClientPacket { + private int _sessionId; + private int _data1; + private int _data2; + private int _data3; + private int _data4; + + public int getSessionId() { + return _sessionId; + } + + public int getData1() { + return _data1; + } + + public int getData2() { + return _data2; + } + + public int getData3() { + return _data3; + } + + public int getData4() { + return _data4; + } + + @Override + protected boolean readImpl() { + if (super._buf.remaining() >= 20) { + _sessionId = readD(); + _data1 = readD(); + _data2 = readD(); + _data3 = readD(); + _data4 = readD(); + return true; + } + return false; + } + + @Override + public void run() { + if (_sessionId == getClient().getSessionId()) { + getClient().setState(LoginClientState.AUTHED_GG); + getClient().sendPacket(new GGAuth(getClient().getSessionId())); + } else { + getClient().close(LoginFailReason.REASON_ACCESS_FAILED); + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/L2LoginClientPacket.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/L2LoginClientPacket.java new file mode 100644 index 00000000..ea46ed82 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/L2LoginClientPacket.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.clientpackets; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.loginserver.network.L2LoginClient; +import com.l2jserver.mmocore.ReceivablePacket; + +/** + * Login Client abstract packet. + * @author KenM + * @version 2.6.1.0 + */ +public abstract class L2LoginClientPacket extends ReceivablePacket { + + private static final Logger LOG = LoggerFactory.getLogger(L2LoginClientPacket.class); + + @Override + protected final boolean read() { + try { + return readImpl(); + } catch (Exception ex) { + LOG.error("Error reading {}!", getClass().getSimpleName(), ex); + return false; + } + } + + protected abstract boolean readImpl(); +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestAuthLogin.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestAuthLogin.java new file mode 100644 index 00000000..8c5ee797 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestAuthLogin.java @@ -0,0 +1,153 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.clientpackets; + +import static com.l2jserver.loginserver.config.Configuration.server; + +import java.net.InetAddress; + +import javax.crypto.Cipher; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.loginserver.GameServerTable.GameServerInfo; +import com.l2jserver.loginserver.LoginController; +import com.l2jserver.loginserver.LoginController.AuthLoginResult; +import com.l2jserver.loginserver.model.AccountInfo; +import com.l2jserver.loginserver.network.L2LoginClient; +import com.l2jserver.loginserver.network.L2LoginClient.LoginClientState; +import com.l2jserver.loginserver.network.serverpackets.AccountKicked; +import com.l2jserver.loginserver.network.serverpackets.AccountKicked.AccountKickedReason; +import com.l2jserver.loginserver.network.serverpackets.LoginFail.LoginFailReason; +import com.l2jserver.loginserver.network.serverpackets.LoginOk; +import com.l2jserver.loginserver.network.serverpackets.ServerList; + +/** + * Format: x 0 (a leading null) x: the rsa encrypted block with the login and password. + * @version 2.6.1.0 + */ +public class RequestAuthLogin extends L2LoginClientPacket { + + private static final Logger LOG = LoggerFactory.getLogger(RequestAuthLogin.class); + + private final byte[] _raw = new byte[128]; + + private String _user; + + private String _password; + + private int _ncotp; + + public String getPassword() { + return _password; + } + + public String getUser() { + return _user; + } + + public int getOneTimePassword() { + return _ncotp; + } + + @Override + public boolean readImpl() { + if (super._buf.remaining() >= 128) { + readB(_raw); + return true; + } + return false; + } + + @Override + public void run() { + byte[] decrypted; + final L2LoginClient client = getClient(); + try { + final Cipher rsaCipher = Cipher.getInstance("RSA/ECB/nopadding"); + rsaCipher.init(Cipher.DECRYPT_MODE, client.getRSAPrivateKey()); + decrypted = rsaCipher.doFinal(_raw, 0x00, 0x80); + } catch (Exception ex) { + LOG.warn("There has been an error trying to login!", ex); + return; + } + + try { + _user = new String(decrypted, 0x5E, 14).trim().toLowerCase(); + _password = new String(decrypted, 0x6C, 16).trim(); + _ncotp = decrypted[0x7c]; + _ncotp |= decrypted[0x7d] << 8; + _ncotp |= decrypted[0x7e] << 16; + _ncotp |= decrypted[0x7f] << 24; + } catch (Exception ex) { + LOG.warn("There has been an error parsing credentials!", ex); + return; + } + + InetAddress clientAddr = getClient().getConnection().getInetAddress(); + + final LoginController lc = LoginController.getInstance(); + AccountInfo info = lc.retrieveAccountInfo(clientAddr, _user, _password); + if (info == null) { + // user or pass wrong + client.close(LoginFailReason.REASON_USER_OR_PASS_WRONG); + return; + } + + AuthLoginResult result = lc.tryCheckinAccount(client, clientAddr, info); + switch (result) { + case AUTH_SUCCESS -> { + client.setAccount(info.getLogin()); + client.setState(LoginClientState.AUTHED_LOGIN); + client.setSessionKey(lc.assignSessionKeyToClient(info.getLogin(), client)); + lc.getCharactersOnAccount(info.getLogin()); + if (server().showLicense()) { + client.sendPacket(new LoginOk(getClient().getSessionKey())); + } else { + getClient().sendPacket(new ServerList(getClient())); + } + } + case INVALID_PASSWORD -> client.close(LoginFailReason.REASON_USER_OR_PASS_WRONG); + case ACCOUNT_INACTIVE -> client.close(LoginFailReason.REASON_INACTIVE); + case ACCOUNT_BANNED -> client.close(new AccountKicked(AccountKickedReason.REASON_PERMANENTLY_BANNED)); + case ALREADY_ON_LS -> { + L2LoginClient oldClient = lc.getAuthedClient(info.getLogin()); + if (oldClient != null) { + // kick the other client + oldClient.close(LoginFailReason.REASON_ACCOUNT_IN_USE); + lc.removeAuthedLoginClient(info.getLogin()); + } + // kick also current client + client.close(LoginFailReason.REASON_ACCOUNT_IN_USE); + } + case ALREADY_ON_GS -> { + GameServerInfo gsi = lc.getAccountOnGameServer(info.getLogin()); + if (gsi != null) { + client.close(LoginFailReason.REASON_ACCOUNT_IN_USE); + + // kick from there + if (gsi.isAuthed()) { + gsi.getGameServerThread().kickPlayer(info.getLogin()); + } + } + } + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestServerList.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestServerList.java new file mode 100644 index 00000000..375e43f7 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestServerList.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.clientpackets; + +import com.l2jserver.loginserver.network.serverpackets.LoginFail.LoginFailReason; +import com.l2jserver.loginserver.network.serverpackets.ServerList; + +/** + *
+ * Format: ddc
+ * d: fist part of session id
+ * d: second part of session id
+ * c: ?
+ * 
+ * + * @version 2.6.1.0 + */ +public class RequestServerList extends L2LoginClientPacket { + private int _skey1; + private int _skey2; + private int _data3; + + public int getSessionKey1() { + return _skey1; + } + + public int getSessionKey2() { + return _skey2; + } + + public int getData3() { + return _data3; + } + + @Override + public boolean readImpl() { + if (super._buf.remaining() >= 8) { + _skey1 = readD(); // loginOk 1 + _skey2 = readD(); // loginOk 2 + return true; + } + return false; + } + + @Override + public void run() { + if (getClient().getSessionKey().checkLoginPair(_skey1, _skey2)) { + getClient().sendPacket(new ServerList(getClient())); + } else { + getClient().close(LoginFailReason.REASON_ACCESS_FAILED); + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestServerLogin.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestServerLogin.java new file mode 100644 index 00000000..c590fa9a --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/clientpackets/RequestServerLogin.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.clientpackets; + +import static com.l2jserver.loginserver.config.Configuration.server; + +import com.l2jserver.loginserver.LoginController; +import com.l2jserver.loginserver.SessionKey; +import com.l2jserver.loginserver.network.serverpackets.LoginFail.LoginFailReason; +import com.l2jserver.loginserver.network.serverpackets.PlayFail.PlayFailReason; +import com.l2jserver.loginserver.network.serverpackets.PlayOk; + +/** + *
+ * Format is ddc
+ * d: first part of session id
+ * d: second part of session id
+ * c: server ID
+ * 
+ * + * @version 2.6.1.0 + */ +public class RequestServerLogin extends L2LoginClientPacket { + private int _skey1; + private int _skey2; + private int _serverId; + + public int getSessionKey1() { + return _skey1; + } + + public int getSessionKey2() { + return _skey2; + } + + public int getServerID() { + return _serverId; + } + + @Override + public boolean readImpl() { + if (super._buf.remaining() >= 9) { + _skey1 = readD(); + _skey2 = readD(); + _serverId = readC(); + return true; + } + return false; + } + + @Override + public void run() { + SessionKey sk = getClient().getSessionKey(); + // if we didn't show the license we can't check these values + if (!server().showLicense() || sk.checkLoginPair(_skey1, _skey2)) { + if (LoginController.getInstance().isLoginPossible(getClient(), _serverId)) { + getClient().setJoinedGS(true); + getClient().sendPacket(new PlayOk(sk)); + } else { + getClient().close(PlayFailReason.REASON_SERVER_OVERLOADED); + } + } else { + getClient().close(LoginFailReason.REASON_ACCESS_FAILED); + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/BlowFishKey.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/BlowFishKey.java new file mode 100644 index 00000000..96686318 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/BlowFishKey.java @@ -0,0 +1,72 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import static com.l2jserver.loginserver.config.Configuration.server; +import static com.l2jserver.loginserver.network.L2JGameServerPacketHandler.GameServerState.BF_CONNECTED; +import static javax.crypto.Cipher.DECRYPT_MODE; + +import javax.crypto.Cipher; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.commons.security.crypt.NewCrypt; +import com.l2jserver.loginserver.GameServerThread; + +/** + * Blowfish Key. + * @author -Wooden- + * @version 2.6.1.0 + */ +public class BlowFishKey extends BaseRecievePacket { + + private static final Logger LOG = LoggerFactory.getLogger(BlowFishKey.class); + + public BlowFishKey(byte[] decrypt, GameServerThread server) { + super(decrypt); + int size = readD(); + byte[] tempKey = readB(size); + try { + byte[] tempDecryptKey; + Cipher rsaCipher = Cipher.getInstance("RSA/ECB/nopadding"); + rsaCipher.init(DECRYPT_MODE, server.getPrivateKey()); + tempDecryptKey = rsaCipher.doFinal(tempKey); + // there are nulls before the key we must remove them + int i = 0; + int len = tempDecryptKey.length; + for (; i < len; i++) { + if (tempDecryptKey[i] != 0) { + break; + } + } + byte[] key = new byte[len - i]; + System.arraycopy(tempDecryptKey, i, key, 0, len - i); + + server.SetBlowFish(new NewCrypt(key)); + if (server().isDebug()) { + LOG.info("New BlowFish key received, Blowfish Engine initialized:"); + } + server.setLoginConnectionState(BF_CONNECTED); + } catch (Exception ex) { + LOG.error("There has been an error while decrypting blowfish key (RSA)!", ex); + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ChangeAccessLevel.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ChangeAccessLevel.java new file mode 100644 index 00000000..b29db21d --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ChangeAccessLevel.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.GameServerThread; +import com.l2jserver.loginserver.LoginController; + +/** + * Change Access Level packet. + * @author -Wooden- + * @version 2.6.1.0 + */ +public class ChangeAccessLevel extends BaseRecievePacket { + + private static final Logger LOG = LoggerFactory.getLogger(ChangeAccessLevel.class); + + public ChangeAccessLevel(byte[] decrypt, GameServerThread server) { + super(decrypt); + int level = readD(); + String account = readS(); + + LoginController.getInstance().setAccountAccessLevel(account, level); + LOG.info("Changed {} access level to {}.", account, level); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ChangePassword.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ChangePassword.java new file mode 100644 index 00000000..382973ed --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ChangePassword.java @@ -0,0 +1,109 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.security.MessageDigest; +import java.sql.ResultSet; +import java.util.Base64; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.GameServerTable; +import com.l2jserver.loginserver.GameServerTable.GameServerInfo; +import com.l2jserver.loginserver.GameServerThread; + +/** + * Change Password packet. + * @author Nik + * @version 2.6.1.0 + */ +public class ChangePassword extends BaseRecievePacket { + + private static final Logger LOG = LoggerFactory.getLogger(ChangePassword.class); + + private static GameServerThread gst = null; + + public ChangePassword(byte[] decrypt) { + super(decrypt); + + String accountName = readS(); + String characterName = readS(); + String currentPassword = readS(); + String newPassword = readS(); + + for (GameServerInfo gsi : GameServerTable.getInstance().getRegisteredGameServers().values()) { + if ((gsi.getGameServerThread() != null) && gsi.getGameServerThread().hasAccountOnGameServer(accountName)) { + gst = gsi.getGameServerThread(); + } + } + + if (gst == null) { + return; + } + + if ((currentPassword == null) || (newPassword == null)) { + gst.ChangePasswordResponse((byte) 0, characterName, "Invalid password data! Try again."); + } else { + try { + final var md = MessageDigest.getInstance("SHA"); + final var raw = md.digest(currentPassword.getBytes(UTF_8)); + String currentPasswordEnc = Base64.getEncoder().encodeToString(raw); + String pass = null; + int passUpdated; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT password FROM accounts WHERE login=?")) { + ps.setString(1, accountName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + pass = rs.getString("password"); + } + } + } + + if (currentPasswordEnc.equals(pass)) { + final var password = md.digest(newPassword.getBytes(UTF_8)); + final var newPasswordEnc = Base64.getEncoder().encodeToString(password); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE accounts SET password=? WHERE login=?")) { + ps.setString(1, newPasswordEnc); + ps.setString(2, accountName); + passUpdated = ps.executeUpdate(); + } + + LOG.info("The password for account {} has been changed from {} to {}.", accountName, currentPasswordEnc, newPasswordEnc); + if (passUpdated > 0) { + gst.ChangePasswordResponse((byte) 1, characterName, "You have successfully changed your password!"); + } else { + gst.ChangePasswordResponse((byte) 0, characterName, "The password change was unsuccessful!"); + } + } else { + gst.ChangePasswordResponse((byte) 0, characterName, "The typed current password doesn't match with your current one."); + } + } catch (Exception ex) { + LOG.warn("Error while changing password for account {} requested by player {}!", accountName, characterName, ex); + } + } + } +} \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/GameServerAuth.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/GameServerAuth.java new file mode 100644 index 00000000..af9ccd40 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/GameServerAuth.java @@ -0,0 +1,173 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import static com.l2jserver.loginserver.config.Configuration.server; + +import java.util.Arrays; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.dao.ServerNameDAO; +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.GameServerTable; +import com.l2jserver.loginserver.GameServerTable.GameServerInfo; +import com.l2jserver.loginserver.GameServerThread; +import com.l2jserver.loginserver.model.GameServerVersion; +import com.l2jserver.loginserver.network.L2JGameServerPacketHandler.GameServerState; +import com.l2jserver.loginserver.network.loginserverpackets.AuthResponse; +import com.l2jserver.loginserver.network.loginserverpackets.LoginServerFail; + +/** + * Game Server Auth packet. + * + *
+ * Format: CCCCHDDBD[S]
+ * C server version
+ * C desired ID
+ * C accept alternative ID
+ * C reserve Host
+ * H port
+ * D max players
+ * D hex Id size
+ * B hex Id
+ * D subnet size
+ * [S] subnets
+ * 
+ * + * @author -Wooden- + * @author Zoey76 + * @version 2.6.1.0 + */ +public class GameServerAuth extends BaseRecievePacket { + + private static final Logger LOG = LoggerFactory.getLogger(GameServerAuth.class); + + private final GameServerThread _server; + + private final byte[] _hexId; + + private final int _desiredId; + + @SuppressWarnings("unused") + private final boolean _hostReserved; + + private final boolean _acceptAlternativeId; + + private final int _maxPlayers; + + private final int _serverVersion; + + private final int _port; + + private final String[] _hosts; + + public GameServerAuth(byte[] decrypt, GameServerThread server) { + super(decrypt); + _server = server; + _serverVersion = readC(); + _desiredId = readC(); + _acceptAlternativeId = (readC() != 0); + _hostReserved = (readC() != 0); + _port = readH(); + _maxPlayers = readD(); + int size = readD(); + _hexId = readB(size); + size = 2 * readD(); + _hosts = new String[size]; + for (int i = 0; i < size; i++) { + _hosts[i] = readS(); + } + + if (server().isDebug()) { + LOG.info("Auth request received."); + } + + if (handleRegProcess()) { + server.sendPacket(new AuthResponse(server.getGameServerInfo().getId())); + LOG.info("Game Server {} enabled.", GameServerVersion.valueOf(_serverVersion)); + + server.broadcastToTelnet("GameServer [" + server.getServerId() + "] " + ServerNameDAO.getServer(server.getServerId()) + " is connected"); + server.setLoginConnectionState(GameServerState.AUTHED); + } + } + + private boolean handleRegProcess() { + if (!server().getServerVersions().contains(_serverVersion)) { + _server.forceClose(LoginServerFail.REASON_INVALID_GAME_SERVER_VERSION); + return false; + } + + GameServerTable gameServerTable = GameServerTable.getInstance(); + + int id = _desiredId; + byte[] hexId = _hexId; + + GameServerInfo gsi = gameServerTable.getRegisteredGameServerById(id); + // is there a game server registered with this id? + if (gsi != null) { + // does the hex id match? + if (Arrays.equals(gsi.getHexId(), hexId)) { + // check to see if this GS is already connected + synchronized (gsi) { + if (gsi.isAuthed()) { + _server.forceClose(LoginServerFail.REASON_ALREADY_LOGGED_IN); + return false; + } + _server.attachGameServerInfo(gsi, _port, _hosts, _maxPlayers); + } + } else { + // there is already a server registered with the desired id and different hex id + // try to register this one with an alternative id + if (server().isAcceptNetGameServer() && _acceptAlternativeId) { + gsi = new GameServerInfo(id, hexId, _server); + if (gameServerTable.registerWithFirstAvailableId(gsi)) { + _server.attachGameServerInfo(gsi, _port, _hosts, _maxPlayers); + gameServerTable.registerServerOnDB(gsi); + } else { + _server.forceClose(LoginServerFail.REASON_NO_FREE_ID); + return false; + } + } else { + // server id is already taken, and we cant get a new one for you + _server.forceClose(LoginServerFail.REASON_WRONG_HEXID); + return false; + } + } + } else { + // can we register on this id? + if (server().isAcceptNetGameServer()) { + gsi = new GameServerInfo(id, hexId, _server); + if (gameServerTable.register(id, gsi)) { + _server.attachGameServerInfo(gsi, _port, _hosts, _maxPlayers); + gameServerTable.registerServerOnDB(gsi); + } else { + // someone took this ID meanwhile + _server.forceClose(LoginServerFail.REASON_ID_RESERVED); + return false; + } + } else { + _server.forceClose(LoginServerFail.REASON_WRONG_HEXID); + return false; + } + } + return true; + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerAuthRequest.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerAuthRequest.java new file mode 100644 index 00000000..88e592c5 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerAuthRequest.java @@ -0,0 +1,53 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.GameServerThread; +import com.l2jserver.loginserver.LoginController; +import com.l2jserver.loginserver.SessionKey; +import com.l2jserver.loginserver.network.loginserverpackets.PlayerAuthResponse; + +/** + * Player Auth Request packet. + * @author -Wooden- + * @version 2.6.1.0 + */ +public class PlayerAuthRequest extends BaseRecievePacket { + + public PlayerAuthRequest(byte[] decrypt, GameServerThread server) { + super(decrypt); + String account = readS(); + int playKey1 = readD(); + int playKey2 = readD(); + int loginKey1 = readD(); + int loginKey2 = readD(); + SessionKey sessionKey = new SessionKey(loginKey1, loginKey2, playKey1, playKey2); + + PlayerAuthResponse authResponse; + SessionKey key = LoginController.getInstance().getKeyForAccount(account); + if ((key != null) && key.equals(sessionKey)) { + LoginController.getInstance().removeAuthedLoginClient(account); + authResponse = new PlayerAuthResponse(account, true); + } else { + authResponse = new PlayerAuthResponse(account, false); + } + server.sendPacket(authResponse); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerInGame.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerInGame.java new file mode 100644 index 00000000..edac78e2 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerInGame.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import static com.l2jserver.loginserver.config.Configuration.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.dao.ServerNameDAO; +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.GameServerThread; + +/** + * Player In Game packet. + * @author -Wooden- + * @author Zoey76 + * @version 2.6.1.0 + */ +public class PlayerInGame extends BaseRecievePacket { + private static final Logger LOG = LoggerFactory.getLogger(PlayerInGame.class); + + public PlayerInGame(byte[] decrypt, GameServerThread server) { + super(decrypt); + int size = readH(); + for (int i = 0; i < size; i++) { + String account = readS(); + server.addAccountOnGameServer(account); + if (server().isDebug()) { + LOG.info("Account {} logged in Game Server {}[{}].", account, ServerNameDAO.getServer(server.getServerId()), server.getServerId()); + } + server.broadcastToTelnet("Account " + account + " logged in GameServer " + server.getServerId()); + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerLogout.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerLogout.java new file mode 100644 index 00000000..65f33925 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerLogout.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import static com.l2jserver.loginserver.config.Configuration.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.dao.ServerNameDAO; +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.GameServerThread; + +/** + * Player Logout packet. + * @author -Wooden- + * @author Zoey76 + * @version 2.6.1.0 + */ +public class PlayerLogout extends BaseRecievePacket { + private static final Logger LOG = LoggerFactory.getLogger(PlayerLogout.class); + + public PlayerLogout(byte[] decrypt, GameServerThread server) { + super(decrypt); + String account = readS(); + + server.removeAccountOnGameServer(account); + if (server().isDebug()) { + LOG.info("Player {} logged out from game server {}[{}].", account, ServerNameDAO.getServer(server.getServerId()), server.getServerId()); + } + + server.broadcastToTelnet("Player " + account + " disconnected from Game Server " + server.getServerId() + "."); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerTracert.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerTracert.java new file mode 100644 index 00000000..eb13cd8b --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/PlayerTracert.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.LoginController; + +/** + * Player Tracert packet. + * @author mrTJO + * @version 2.6.1.0 + */ +public class PlayerTracert extends BaseRecievePacket { + + public PlayerTracert(byte[] decrypt) { + super(decrypt); + String account = readS(); + String pcIp = readS(); + String hop1 = readS(); + String hop2 = readS(); + String hop3 = readS(); + String hop4 = readS(); + + LoginController.getInstance().setAccountLastTracert(account, pcIp, hop1, hop2, hop3, hop4); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ReplyCharacters.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ReplyCharacters.java new file mode 100644 index 00000000..062b5ce8 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ReplyCharacters.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.GameServerThread; +import com.l2jserver.loginserver.LoginController; + +/** + * @author mrTJO + * @version 2.6.1.0 + */ +public class ReplyCharacters extends BaseRecievePacket { + public ReplyCharacters(byte[] decrypt, GameServerThread server) { + super(decrypt); + String account = readS(); + int chars = readC(); + int charsToDel = readC(); + long[] charsList = new long[charsToDel]; + for (int i = 0; i < charsToDel; i++) { + charsList[i] = readQ(); + } + LoginController.getInstance().setCharactersOnServer(account, chars, charsList, server.getServerId()); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/RequestSendMail.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/RequestSendMail.java new file mode 100644 index 00000000..0e2089bf --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/RequestSendMail.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.mail.MailSystem; + +/** + * @author mrTJO + * @version 2.6.1.0 + */ +public class RequestSendMail extends BaseRecievePacket { + + public RequestSendMail(byte[] decrypt) { + super(decrypt); + String accountName = readS(); + String mailId = readS(); + int argNum = readC(); + String[] args = new String[argNum]; + for (int i = 0; i < argNum; i++) { + args[i] = readS(); + } + MailSystem.getInstance().sendMail(accountName, mailId, args); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/RequestTempBan.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/RequestTempBan.java new file mode 100644 index 00000000..012f4b88 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/RequestTempBan.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.LoginController; + +/** + * Request Temp Ban packet. + * @author mrTJO + * @version 2.6.1.0 + */ +public class RequestTempBan extends BaseRecievePacket { + + private static final Logger LOG = LoggerFactory.getLogger(RequestTempBan.class); + + private final String _accountName; + + @SuppressWarnings("unused") + private String _banReason; + + private final String _ip; + + private final long _banTime; + + public RequestTempBan(byte[] decrypt) { + super(decrypt); + _accountName = readS(); + _ip = readS(); + _banTime = readQ(); + boolean haveReason = readC() != 0; + if (haveReason) { + _banReason = readS(); + } + banUser(); + } + + private void banUser() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO account_data VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value=?")) { + ps.setString(1, _accountName); + ps.setString(2, "ban_temp"); + ps.setString(3, Long.toString(_banTime)); + ps.setString(4, Long.toString(_banTime)); + ps.execute(); + } catch (Exception ex) { + LOG.warn("There has been an error inserting ban for account {}!", _accountName, ex); + } + + try { + LoginController.getInstance().addBanForAddress(_ip, _banTime); + } catch (Exception e) { + + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ServerStatus.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ServerStatus.java new file mode 100644 index 00000000..c6050f3f --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/gameserverpackets/ServerStatus.java @@ -0,0 +1,93 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.gameserverpackets; + +import com.l2jserver.commons.network.BaseRecievePacket; +import com.l2jserver.loginserver.GameServerTable; +import com.l2jserver.loginserver.GameServerTable.GameServerInfo; +import com.l2jserver.loginserver.GameServerThread; + +/** + * Server Status packet. + * @author -Wooden- + * @version 2.6.1.0 + */ +public class ServerStatus extends BaseRecievePacket { + + public static final String[] STATUS_STRING = { + "Auto", + "Good", + "Normal", + "Full", + "Down", + "Gm Only" + }; + + public static final int SERVER_LIST_STATUS = 0x01; + public static final int SERVER_TYPE = 0x02; + public static final int SERVER_LIST_SQUARE_BRACKET = 0x03; + public static final int MAX_PLAYERS = 0x04; + public static final int TEST_SERVER = 0x05; + public static final int SERVER_AGE = 0x06; + + // Server Status + public static final int STATUS_AUTO = 0x00; + public static final int STATUS_GOOD = 0x01; + public static final int STATUS_NORMAL = 0x02; + public static final int STATUS_FULL = 0x03; + public static final int STATUS_DOWN = 0x04; + public static final int STATUS_GM_ONLY = 0x05; + + // Server Types + public static final int SERVER_NORMAL = 0x01; + public static final int SERVER_RELAX = 0x02; + public static final int SERVER_TEST = 0x04; + public static final int SERVER_NOLABEL = 0x08; + public static final int SERVER_CREATION_RESTRICTED = 0x10; + public static final int SERVER_EVENT = 0x20; + public static final int SERVER_FREE = 0x40; + + // Server Ages + public static final int SERVER_AGE_ALL = 0x00; + public static final int SERVER_AGE_15 = 0x0F; + public static final int SERVER_AGE_18 = 0x12; + + public static final int ON = 0x01; + public static final int OFF = 0x00; + + public ServerStatus(byte[] decrypt, GameServerThread server) { + super(decrypt); + + GameServerInfo gsi = GameServerTable.getInstance().getRegisteredGameServerById(server.getServerId()); + if (gsi != null) { + int size = readD(); + for (int i = 0; i < size; i++) { + int type = readD(); + int value = readD(); + switch (type) { + case SERVER_LIST_STATUS -> gsi.setStatus(value); + case SERVER_LIST_SQUARE_BRACKET -> gsi.setShowingBrackets(value == ON); + case MAX_PLAYERS -> gsi.setMaxPlayers(value); + case SERVER_TYPE -> gsi.setServerType(value); + case SERVER_AGE -> gsi.setAgeLimit(value); + } + } + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/AuthResponse.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/AuthResponse.java new file mode 100644 index 00000000..0c1704a0 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/AuthResponse.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.loginserverpackets; + +import com.l2jserver.commons.dao.ServerNameDAO; +import com.l2jserver.commons.network.BaseSendablePacket; + +/** + * @author -Wooden- + * @version 2.6.1.0 + */ +public class AuthResponse extends BaseSendablePacket { + + public AuthResponse(int serverId) { + writeC(0x02); + writeC(serverId); + writeS(ServerNameDAO.getServer(serverId)); + } + + @Override + public byte[] getContent() { + return getBytes(); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/ChangePasswordResponse.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/ChangePasswordResponse.java new file mode 100644 index 00000000..2e7ad92b --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/ChangePasswordResponse.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.loginserverpackets; + +import com.l2jserver.commons.network.BaseSendablePacket; + +/** + * @author Nik + * @version 2.6.1.0 + */ +public class ChangePasswordResponse extends BaseSendablePacket { + public ChangePasswordResponse(byte successful, String characterName, String msgToSend) { + writeC(0x06); + // writeC(successful); //0 false, 1 true + writeS(characterName); + writeS(msgToSend); + } + + @Override + public byte[] getContent() { + return getBytes(); + } +} \ No newline at end of file diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/InitLS.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/InitLS.java new file mode 100644 index 00000000..e8560fd8 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/InitLS.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.loginserverpackets; + +import com.l2jserver.commons.network.BaseSendablePacket; + +/** + * Initialize Login Server packet. + * + *
+ * C ID 0x00
+ * D key size
+ * B key
+ * 
+ * + * @author -Wooden- + * @author Zoey76 + * @version 2.6.1.0 + */ +public class InitLS extends BaseSendablePacket { + + public InitLS(byte[] publicKey) { + writeC(0x00); + writeD(publicKey.length); + writeB(publicKey); + } + + @Override + public byte[] getContent() { + return getBytes(); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/KickPlayer.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/KickPlayer.java new file mode 100644 index 00000000..4b0f4c72 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/KickPlayer.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.loginserverpackets; + +import com.l2jserver.commons.network.BaseSendablePacket; + +/** + * @author -Wooden- + * @version 2.6.1.0 + */ +public class KickPlayer extends BaseSendablePacket { + public KickPlayer(String account) { + writeC(0x04); + writeS(account); + } + + @Override + public byte[] getContent() { + return getBytes(); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/LoginServerFail.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/LoginServerFail.java new file mode 100644 index 00000000..d29aa2c8 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/LoginServerFail.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.loginserverpackets; + +import com.l2jserver.commons.network.BaseSendablePacket; + +/** + * @author -Wooden- + * @version 2.6.1.0 + */ +public class LoginServerFail extends BaseSendablePacket { + + public static final int REASON_INVALID_GAME_SERVER_VERSION = 0; + + public static final int REASON_IP_BANNED = 1; + + public static final int REASON_IP_RESERVED = 2; + + public static final int REASON_WRONG_HEXID = 3; + + public static final int REASON_ID_RESERVED = 4; + + public static final int REASON_NO_FREE_ID = 5; + + public static final int NOT_AUTHED = 6; + + public static final int REASON_ALREADY_LOGGED_IN = 7; + + public LoginServerFail(int reason) { + writeC(0x01); + writeC(reason); + } + + @Override + public byte[] getContent() { + return getBytes(); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/PlayerAuthResponse.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/PlayerAuthResponse.java new file mode 100644 index 00000000..448ec8ab --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/PlayerAuthResponse.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.loginserverpackets; + +import com.l2jserver.commons.network.BaseSendablePacket; + +/** + * @author -Wooden- + * @version 2.6.1.0 + */ +public class PlayerAuthResponse extends BaseSendablePacket { + public PlayerAuthResponse(String account, boolean response) { + writeC(0x03); + writeS(account); + writeC(response ? 1 : 0); + } + + @Override + public byte[] getContent() { + return getBytes(); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/RequestCharacters.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/RequestCharacters.java new file mode 100644 index 00000000..ab1428d9 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/loginserverpackets/RequestCharacters.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.loginserverpackets; + +import com.l2jserver.commons.network.BaseSendablePacket; + +/** + * @author mrTJO + * @version 2.6.1.0 + */ +public class RequestCharacters extends BaseSendablePacket { + public RequestCharacters(String account) { + writeC(0x05); + writeS(account); + } + + @Override + public byte[] getContent() { + return getBytes(); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/AccountKicked.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/AccountKicked.java new file mode 100644 index 00000000..8b2d8406 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/AccountKicked.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.serverpackets; + +/** + * @author KenM + * @version 2.6.1.0 + */ +public final class AccountKicked extends L2LoginServerPacket { + public enum AccountKickedReason { + REASON_DATA_STEALER(0x01), + REASON_GENERIC_VIOLATION(0x08), + REASON_7_DAYS_SUSPENDED(0x10), + REASON_PERMANENTLY_BANNED(0x20); + + private final int _code; + + AccountKickedReason(int code) { + _code = code; + } + + public final int getCode() { + return _code; + } + } + + private final AccountKickedReason _reason; + + public AccountKicked(AccountKickedReason reason) { + _reason = reason; + } + + @Override + protected void write() { + writeC(0x02); + writeD(_reason.getCode()); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/GGAuth.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/GGAuth.java new file mode 100644 index 00000000..d0bea4d4 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/GGAuth.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.serverpackets; + +/** + * Format: d d: response + * @version 2.6.1.0 + */ +public final class GGAuth extends L2LoginServerPacket { + + public static final int SKIP_GG_AUTH_REQUEST = 0x0b; + + private final int _response; + + public GGAuth(int response) { + _response = response; + } + + @Override + protected void write() { + writeC(0x0b); + writeD(_response); + writeD(0x00); + writeD(0x00); + writeD(0x00); + writeD(0x00); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/Init.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/Init.java new file mode 100644 index 00000000..ed0ec68d --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/Init.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.serverpackets; + +import com.l2jserver.loginserver.network.L2LoginClient; + +/** + *
+ * Format: dd b dddd s
+ * d: session id
+ * d: protocol revision
+ * b: 0x90 bytes : 0x80 bytes for the scrambled RSA public key
+ *                 0x10 bytes at 0x00
+ * d: unknown
+ * d: unknown
+ * d: unknown
+ * d: unknown
+ * s: blowfish key
+ * 
+ * + * @version 2.6.1.0 + */ +public final class Init extends L2LoginServerPacket { + private final int _sessionId; + + private final byte[] _publicKey; + private final byte[] _blowfishKey; + + public Init(L2LoginClient client) { + this(client.getScrambledModulus(), client.getBlowfishKey(), client.getSessionId()); + } + + public Init(byte[] publicKey, byte[] blowfishKey, int sessionId) { + _sessionId = sessionId; + _publicKey = publicKey; + _blowfishKey = blowfishKey; + } + + @Override + protected void write() { + writeC(0x00); // init packet id + + writeD(_sessionId); // session id + writeD(0x0000c621); // protocol revision + + writeB(_publicKey); // RSA Public Key + + // unk GG related? + writeD(0x29DD954E); + writeD(0x77C39CFC); + writeD(0x97ADB620); + writeD(0x07BDE0F7); + + writeB(_blowfishKey); // BlowFish key + writeC(0x00); // null termination ;) + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/L2LoginServerPacket.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/L2LoginServerPacket.java new file mode 100644 index 00000000..8d9ef386 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/L2LoginServerPacket.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.serverpackets; + +import com.l2jserver.loginserver.network.L2LoginClient; +import com.l2jserver.mmocore.SendablePacket; + +/** + * @author KenM + * @version 2.6.1.0 + */ +public abstract class L2LoginServerPacket extends SendablePacket { + +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/LoginFail.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/LoginFail.java new file mode 100644 index 00000000..de750133 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/LoginFail.java @@ -0,0 +1,89 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.serverpackets; + +/** + * Format: d d: the failure reason + * @version 2.6.1.0 + */ +public final class LoginFail extends L2LoginServerPacket { + public enum LoginFailReason { + REASON_NO_MESSAGE(0x00), + REASON_SYSTEM_ERROR_LOGIN_LATER(0x01), + REASON_USER_OR_PASS_WRONG(0x02), + REASON_ACCESS_FAILED_TRY_AGAIN_LATER(0x04), + REASON_ACCOUNT_INFO_INCORRECT_CONTACT_SUPPORT(0x05), + REASON_ACCOUNT_IN_USE(0x07), + REASON_UNDER_18_YEARS_KR(0x0C), + REASON_SERVER_OVERLOADED(0x0F), + REASON_SERVER_MAINTENANCE(0x10), + REASON_TEMP_PASS_EXPIRED(0x11), + REASON_GAME_TIME_EXPIRED(0x12), + REASON_NO_TIME_LEFT(0x13), + REASON_SYSTEM_ERROR(0x14), + REASON_ACCESS_FAILED(0x15), + REASON_RESTRICTED_IP(0x16), + REASON_WEEK_USAGE_FINISHED(0x1E), + REASON_SECURITY_CARD_NUMBER_INVALID(0x1F), + REASON_AGE_NOT_VERIFIED_CANT_LOG_BETWEEN_10PM_6AM(0x20), + REASON_SERVER_CANNOT_BE_ACCESSED_BY_YOUR_COUPON(0x21), + REASON_DUAL_BOX(0x23), + REASON_INACTIVE(0x24), + REASON_USER_AGREEMENT_REJECTED_ON_WEBSITE(0x25), + REASON_GUARDIAN_CONSENT_REQUIRED(0x26), + REASON_USER_AGREEMENT_DECLINED_OR_WITHDRAWAL_REQUEST(0x27), + REASON_ACCOUNT_SUSPENDED_CALL(0x28), + REASON_CHANGE_PASSWORD_AND_QUIZ_ON_WEBSITE(0x29), + REASON_ALREADY_LOGGED_INTO_10_ACCOUNTS(0x2A), + REASON_MASTER_ACCOUNT_RESTRICTED(0x2B), + REASON_CERTIFICATION_FAILED(0x2E), + REASON_TELEPHONE_CERTIFICATION_UNAVAILABLE(0x2F), + REASON_TELEPHONE_SIGNALS_DELAYED(0x30), + REASON_CERTIFICATION_FAILED_LINE_BUSY(0x31), + REASON_CERTIFICATION_SERVICE_NUMBER_EXPIRED_OR_INCORRECT(0x32), + REASON_CERTIFICATION_SERVICE_CURRENTLY_BEING_CHECKED(0x33), + REASON_CERTIFICATION_SERVICE_CANT_BE_USED_HEAVY_VOLUME(0x34), + REASON_CERTIFICATION_SERVICE_EXPIRED_GAMEPLAY_BLOCKED(0x35), + REASON_CERTIFICATION_FAILED_3_TIMES_GAMEPLAY_BLOCKED_30_MIN(0x36), + REASON_CERTIFICATION_DAILY_USE_EXCEEDED(0x37), + REASON_CERTIFICATION_UNDERWAY_TRY_AGAIN_LATER(0x38); + + private final int _code; + + LoginFailReason(int code) { + _code = code; + } + + public final int getCode() { + return _code; + } + } + + private final LoginFailReason _reason; + + public LoginFail(LoginFailReason reason) { + _reason = reason; + } + + @Override + protected void write() { + writeC(0x01); + writeC(_reason.getCode()); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/LoginOk.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/LoginOk.java new file mode 100644 index 00000000..dc56760f --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/LoginOk.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.serverpackets; + +import com.l2jserver.loginserver.SessionKey; + +/** + *
+ * Format: dddddddd
+ * f: the session key
+ * d: ?
+ * d: ?
+ * d: ?
+ * d: ?
+ * d: ?
+ * d: ?
+ * b: 16 bytes - unknown
+ * 
+ * + * @version 2.6.1.0 + */ +public final class LoginOk extends L2LoginServerPacket { + private final int _loginOk1, _loginOk2; + + public LoginOk(SessionKey sessionKey) { + _loginOk1 = sessionKey.loginOkID1; + _loginOk2 = sessionKey.loginOkID2; + } + + @Override + protected void write() { + writeC(0x03); + writeD(_loginOk1); + writeD(_loginOk2); + writeD(0x00); + writeD(0x00); + writeD(0x000003ea); + writeD(0x00); + writeD(0x00); + writeD(0x00); + writeB(new byte[16]); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/PlayFail.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/PlayFail.java new file mode 100644 index 00000000..b5f630db --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/PlayFail.java @@ -0,0 +1,88 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.serverpackets; + +/** + * @version 2.6.1.0 + */ +public final class PlayFail extends L2LoginServerPacket { + public enum PlayFailReason { + REASON_NO_MESSAGE(0x00), + REASON_SYSTEM_ERROR_LOGIN_LATER(0x01), + REASON_USER_OR_PASS_WRONG(0x02), + REASON_ACCESS_FAILED_TRY_AGAIN_LATER(0x04), + REASON_ACCOUNT_INFO_INCORRECT_CONTACT_SUPPORT(0x05), + REASON_ACCOUNT_IN_USE(0x07), + REASON_UNDER_18_YEARS_KR(0x0C), + REASON_SERVER_OVERLOADED(0x0F), + REASON_SERVER_MAINTENANCE(0x10), + REASON_TEMP_PASS_EXPIRED(0x11), + REASON_GAME_TIME_EXPIRED(0x12), + REASON_NO_TIME_LEFT(0x13), + REASON_SYSTEM_ERROR(0x14), + REASON_ACCESS_FAILED(0x15), + REASON_RESTRICTED_IP(0x16), + REASON_WEEK_USAGE_FINISHED(0x1E), + REASON_SECURITY_CARD_NUMBER_INVALID(0x1F), + REASON_AGE_NOT_VERIFIED_CANT_LOG_BETWEEN_10PM_6AM(0x20), + REASON_SERVER_CANNOT_BE_ACCESSED_BY_YOUR_COUPON(0x21), + REASON_DUAL_BOX(0x23), + REASON_INACTIVE(0x24), + REASON_USER_AGREEMENT_REJECTED_ON_WEBSITE(0x25), + REASON_GUARDIAN_CONSENT_REQUIRED(0x26), + REASON_USER_AGREEMENT_DECLINED_OR_WITHDRAWAL_REQUEST(0x27), + REASON_ACCOUNT_SUSPENDED_CALL(0x28), + REASON_CHANGE_PASSWORD_AND_QUIZ_ON_WEBSITE(0x29), + REASON_ALREADY_LOGGED_INTO_10_ACCOUNTS(0x2A), + REASON_MASTER_ACCOUNT_RESTRICTED(0x2B), + REASON_CERTIFICATION_FAILED(0x2E), + REASON_TELEPHONE_CERTIFICATION_UNAVAILABLE(0x2F), + REASON_TELEPHONE_SIGNALS_DELAYED(0x30), + REASON_CERTIFICATION_FAILED_LINE_BUSY(0x31), + REASON_CERTIFICATION_SERVICE_NUMBER_EXPIRED_OR_INCORRECT(0x32), + REASON_CERTIFICATION_SERVICE_CURRENTLY_BEING_CHECKED(0x33), + REASON_CERTIFICATION_SERVICE_CANT_BE_USED_HEAVY_VOLUME(0x34), + REASON_CERTIFICATION_SERVICE_EXPIRED_GAMEPLAY_BLOCKED(0x35), + REASON_CERTIFICATION_FAILED_3_TIMES_GAMEPLAY_BLOCKED_30_MIN(0x36), + REASON_CERTIFICATION_DAILY_USE_EXCEEDED(0x37), + REASON_CERTIFICATION_UNDERWAY_TRY_AGAIN_LATER(0x38); + + private final int _code; + + PlayFailReason(int code) { + _code = code; + } + + public final int getCode() { + return _code; + } + } + + private final PlayFailReason _reason; + + public PlayFail(PlayFailReason reason) { + _reason = reason; + } + + @Override + protected void write() { + writeC(0x06); + writeC(_reason.getCode()); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/PlayOk.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/PlayOk.java new file mode 100644 index 00000000..edbe75da --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/PlayOk.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.serverpackets; + +import com.l2jserver.loginserver.SessionKey; + +/** + * @version 2.6.1.0 + */ +public final class PlayOk extends L2LoginServerPacket { + private final int _playOk1, _playOk2; + + public PlayOk(SessionKey sessionKey) { + _playOk1 = sessionKey.playOkID1; + _playOk2 = sessionKey.playOkID2; + } + + @Override + protected void write() { + writeC(0x07); + writeD(_playOk1); + writeD(_playOk2); + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/ServerList.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/ServerList.java new file mode 100644 index 00000000..12b5d117 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/network/serverpackets/ServerList.java @@ -0,0 +1,162 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.network.serverpackets; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.loginserver.GameServerTable; +import com.l2jserver.loginserver.GameServerTable.GameServerInfo; +import com.l2jserver.loginserver.network.L2LoginClient; +import com.l2jserver.loginserver.network.gameserverpackets.ServerStatus; + +/** + * ServerList + * + *
+ * Format: cc [cddcchhcdc]
+ * 
+ * c: server list size (number of servers)
+ * c: ?
+ * [ (repeat for each servers)
+ * c: server id (ignored by client?)
+ * d: server ip
+ * d: server port
+ * c: age limit (used by client?)
+ * c: pvp or not (used by client?)
+ * h: current number of players
+ * h: max number of players
+ * c: 0 if server is down
+ * d: 2nd bit: clock
+ *    3rd bit: won't display server name
+ *    4th bit: test server (used by client?)
+ * c: 0 if you don't want to display brackets in front of sever name
+ * ]
+ * 
+ * + * Server will be considered as Good when the number of online players
+ * is less than half the maximum. as Normal between half and 4/5
+ * and Full when there's more than 4/5 of the maximum number of players. + * @version 2.6.1.0 + */ +public final class ServerList extends L2LoginServerPacket { + + private static final Logger LOG = LoggerFactory.getLogger(ServerList.class); + + private final List _servers; + private final int _lastServer; + private final Map _charsOnServers; + private final Map _charsToDelete; + + static class ServerData { + protected byte[] _ip; + protected final int _port; + protected final int _ageLimit; + protected final boolean _pvp; + protected final int _currentPlayers; + protected final int _maxPlayers; + protected final boolean _brackets; + protected boolean _clock; + protected final int _status; + protected final int _serverId; + protected final int _serverType; + + ServerData(L2LoginClient client, GameServerInfo gsi) { + try { + _ip = InetAddress.getByName(gsi.getServerAddress(client.getConnection().getInetAddress())).getAddress(); + } catch (UnknownHostException ex) { + LOG.warn("There has been an error getting IP from host!", ex); + _ip = new byte[4]; + _ip[0] = 127; + _ip[1] = 0; + _ip[2] = 0; + _ip[3] = 1; + } + + _port = gsi.getPort(); + _pvp = gsi.isPvp(); + _serverType = gsi.getServerType(); + _currentPlayers = gsi.getCurrentPlayerCount(); + _maxPlayers = gsi.getMaxPlayers(); + _ageLimit = 0; + _brackets = gsi.isShowingBrackets(); + // If server GM-only - show status only to GMs + _status = gsi.getStatus() != ServerStatus.STATUS_GM_ONLY ? gsi.getStatus() : client.getAccessLevel() > 0 ? gsi.getStatus() : ServerStatus.STATUS_DOWN; + _serverId = gsi.getId(); + } + } + + public ServerList(L2LoginClient client) { + _servers = new ArrayList<>(GameServerTable.getInstance().getRegisteredGameServers().size()); + _lastServer = client.getLastServer(); + for (GameServerInfo gsi : GameServerTable.getInstance().getRegisteredGameServers().values()) { + _servers.add(new ServerData(client, gsi)); + } + _charsOnServers = client.getCharsOnServ(); + _charsToDelete = client.getCharsWaitingDelOnServ(); + } + + @Override + public void write() { + writeC(0x04); + writeC(_servers.size()); + writeC(_lastServer); + for (ServerData server : _servers) { + writeC(server._serverId); // server id + + writeC(server._ip[0] & 0xff); + writeC(server._ip[1] & 0xff); + writeC(server._ip[2] & 0xff); + writeC(server._ip[3] & 0xff); + + writeD(server._port); + writeC(server._ageLimit); // Age Limit 0, 15, 18 + writeC(server._pvp ? 0x01 : 0x00); + writeH(server._currentPlayers); + writeH(server._maxPlayers); + writeC(server._status == ServerStatus.STATUS_DOWN ? 0x00 : 0x01); + writeD(server._serverType); // 1: Normal, 2: Relax, 4: Public Test, 8: No Label, 16: Character Creation Restricted, 32: Event, 64: Free + writeC(server._brackets ? 0x01 : 0x00); + } + writeH(0x00); // unknown + if (_charsOnServers != null) { + writeC(_charsOnServers.size()); + for (int servId : _charsOnServers.keySet()) { + writeC(servId); + writeC(_charsOnServers.get(servId)); + if ((_charsToDelete == null) || !_charsToDelete.containsKey(servId)) { + writeC(0x00); + } else { + writeC(_charsToDelete.get(servId).length); + for (long deleteTime : _charsToDelete.get(servId)) { + writeD((int) ((deleteTime - System.currentTimeMillis()) / 1000)); + } + } + } + } else { + writeC(0x00); + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/security/ScrambledKeyPair.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/security/ScrambledKeyPair.java new file mode 100644 index 00000000..e7790c95 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/security/ScrambledKeyPair.java @@ -0,0 +1,72 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.security; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.interfaces.RSAPublicKey; + +public class ScrambledKeyPair { + + private final KeyPair pair; + + private final byte[] scrambledModulus; + + public ScrambledKeyPair(KeyPair pPair) { + pair = pPair; + scrambledModulus = scrambleModulus(((RSAPublicKey) pair.getPublic()).getModulus()); + } + + public KeyPair getPair() { + return pair; + } + + public byte[] getScrambledModulus() { + return scrambledModulus; + } + + private byte[] scrambleModulus(BigInteger modulus) { + byte[] scrambledMod = modulus.toByteArray(); + + if ((scrambledMod.length == 0x81) && (scrambledMod[0] == 0x00)) { + byte[] temp = new byte[0x80]; + System.arraycopy(scrambledMod, 1, temp, 0, 0x80); + scrambledMod = temp; + } + // step 1 : 0x4d-0x50 <-> 0x00-0x04 + for (int i = 0; i < 4; i++) { + byte temp = scrambledMod[i]; + scrambledMod[i] = scrambledMod[0x4d + i]; + scrambledMod[0x4d + i] = temp; + } + // step 2 : xor first 0x40 bytes with last 0x40 bytes + for (int i = 0; i < 0x40; i++) { + scrambledMod[i] = (byte) (scrambledMod[i] ^ scrambledMod[0x40 + i]); + } + // step 3 : xor bytes 0x0d-0x10 with bytes 0x34-0x38 + for (int i = 0; i < 4; i++) { + scrambledMod[0x0d + i] = (byte) (scrambledMod[0x0d + i] ^ scrambledMod[0x34 + i]); + } + // step 4 : xor last 0x40 bytes with first 0x40 bytes + for (int i = 0; i < 0x40; i++) { + scrambledMod[0x40 + i] = (byte) (scrambledMod[0x40 + i] ^ scrambledMod[i]); + } + return scrambledMod; + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/security/crypt/LoginCrypt.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/security/crypt/LoginCrypt.java new file mode 100644 index 00000000..19db5bf8 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/security/crypt/LoginCrypt.java @@ -0,0 +1,117 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.security.crypt; + +import java.io.IOException; + +import com.l2jserver.commons.security.crypt.NewCrypt; +import com.l2jserver.commons.util.Rnd; + +/** + * @author KenM + */ +public class LoginCrypt { + private static final byte[] STATIC_BLOWFISH_KEY = { + (byte) 0x6b, + (byte) 0x60, + (byte) 0xcb, + (byte) 0x5b, + (byte) 0x82, + (byte) 0xce, + (byte) 0x90, + (byte) 0xb1, + (byte) 0xcc, + (byte) 0x2b, + (byte) 0x6c, + (byte) 0x55, + (byte) 0x6c, + (byte) 0x6c, + (byte) 0x6c, + (byte) 0x6c + }; + + private static final NewCrypt _STATIC_CRYPT = new NewCrypt(STATIC_BLOWFISH_KEY); + private NewCrypt _crypt = null; + private boolean _static = true; + + /** + * Method to initialize the blowfish cipher with dynamic key. + * @param key the blowfish key to initialize the dynamic blowfish cipher with + */ + public void setKey(byte[] key) { + _crypt = new NewCrypt(key); + } + + /** + * Method to decrypt an incoming login client packet. + * @param raw array with encrypted data + * @param offset offset where the encrypted data is located + * @param size number of bytes of encrypted data + * @return true when checksum could be verified, false otherwise + * @throws IOException the size is not multiple of blowfish block size or the raw array can't hold size bytes starting at offset due to its size + */ + public boolean decrypt(byte[] raw, final int offset, final int size) throws IOException { + if ((size % 8) != 0) { + throw new IOException("size have to be multiple of 8"); + } + if ((offset + size) > raw.length) { + throw new IOException("raw array too short for size starting from offset"); + } + + _crypt.decrypt(raw, offset, size); + return NewCrypt.verifyChecksum(raw, offset, size); + } + + /** + * Method to encrypt an outgoing packet to login client.
+ * Performs padding and resizing of data array. + * @param raw array with plain data + * @param offset offset where the plain data is located + * @param size number of bytes of plain data + * @return the new array size + * @throws IOException packet is too long to make padding and add verification data + */ + public int encrypt(byte[] raw, final int offset, int size) throws IOException { + // reserve checksum + size += 4; + + if (_static) { + // reserve for XOR "key" + size += 4; + + // padding + size += 8 - (size % 8); + if ((offset + size) > raw.length) { + throw new IOException("packet too long"); + } + NewCrypt.encXORPass(raw, offset, size, Rnd.nextInt()); + _STATIC_CRYPT.crypt(raw, offset, size); + _static = false; + } else { + // padding + size += 8 - (size % 8); + if ((offset + size) > raw.length) { + throw new IOException("packet too long"); + } + NewCrypt.appendChecksum(raw, offset, size); + _crypt.crypt(raw, offset, size); + } + return size; + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/status/LoginStatusThread.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/status/LoginStatusThread.java new file mode 100644 index 00000000..fcddb9a7 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/status/LoginStatusThread.java @@ -0,0 +1,216 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.status; + +import static com.l2jserver.loginserver.config.Configuration.server; +import static com.l2jserver.loginserver.config.Configuration.telnet; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.loginserver.GameServerTable; +import com.l2jserver.loginserver.LoginController; +import com.l2jserver.loginserver.LoginServer; + +public final class LoginStatusThread extends Thread { + + private static final Logger LOG = LoggerFactory.getLogger(LoginStatusThread.class); + + private final Socket _cSocket; + + private final PrintWriter _print; + + private final BufferedReader _read; + + private boolean _redirectLogger; + + private void telnetOutput(int type, String text) { + if (type == 1) { + System.out.println("TELNET | " + text); + } else if (type == 2) { + System.out.print("TELNET | " + text); + } else if (type == 3) { + System.out.print(text); + } else if (type == 4) { + System.out.println(text); + } else { + System.out.println("TELNET | " + text); + } + } + + private boolean isValidIP(Socket client) { + boolean result = false; + InetAddress clientIP = client.getInetAddress(); + + // convert IP to String, and compare with list + String clientStringIP = clientIP.getHostAddress(); + + telnetOutput(1, "Connection from: " + clientStringIP); + + // read and loop through a list of IPs, compare with them to the newIP + if (server().isDebug()) { + telnetOutput(2, ""); + } + + if (server().isDebug()) { + telnetOutput(3, "Comparing ip to list..."); + } + + for (String host : telnet().getHosts()) { + try { + String ipToCompare = InetAddress.getByName(host).getHostAddress(); + if (clientStringIP.equals(ipToCompare)) { + result = true; + } + + if (server().isDebug()) { + telnetOutput(3, clientStringIP + " = " + ipToCompare + "(" + host + ") = " + result); + } + } catch (Exception ex) { + LOG.warn("There has been an error parsing host {}!", host, ex); + } + } + + if (server().isDebug()) { + telnetOutput(4, "Allow IP: " + result); + } + return result; + } + + public LoginStatusThread(Socket client, int uptime, String StatusPW) throws Exception { + _cSocket = client; + + _print = new PrintWriter(_cSocket.getOutputStream()); + _read = new BufferedReader(new InputStreamReader(_cSocket.getInputStream())); + + if (isValidIP(client)) { + telnetOutput(1, client.getInetAddress().getHostAddress() + " accepted."); + _print.println("Welcome To The L2J Telnet Session."); + _print.println("Please Insert Your Password!"); + _print.print("Password: "); + _print.flush(); + String tmpLine = _read.readLine(); + if (tmpLine == null) { + _print.println("Error."); + _print.println("Disconnected..."); + _print.flush(); + _cSocket.close(); + } else { + if (!tmpLine.equals(StatusPW)) { + _print.println("Incorrect Password!"); + _print.println("Disconnected..."); + _print.flush(); + _cSocket.close(); + } else { + _print.println("Password Correct!"); + _print.println("[L2J Login Server]"); + _print.print(""); + _print.flush(); + start(); + } + } + } else { + telnetOutput(5, "Connection attempt from " + client.getInetAddress().getHostAddress() + " rejected."); + _cSocket.close(); + } + } + + @Override + public void run() { + String _usrCommand = ""; + try { + while ((_usrCommand.compareTo("quit") != 0) && (_usrCommand.compareTo("exit") != 0)) { + _usrCommand = _read.readLine(); + if (_usrCommand == null) { + _cSocket.close(); + break; + } + if (_usrCommand.equals("help")) { + _print.println("The following is a list of all available commands: "); + _print.println("help - shows this help."); + _print.println("status - displays basic server statistics."); + _print.println("unblock - removes from the ban list."); + _print.println("shutdown - shuts down server."); + _print.println("restart - restarts the server."); + _print.println("RedirectLogger - Telnet will give you some info about server in real time."); + _print.println("quit - closes telnet session."); + _print.println(""); + } else if (_usrCommand.equals("status")) { + // TODO enhance the output + _print.println("Registered Server Count: " + GameServerTable.getInstance().getRegisteredGameServers().size()); + } else if (_usrCommand.startsWith("unblock")) { + try { + _usrCommand = _usrCommand.substring(8); + if (LoginController.getInstance().removeBanForAddress(_usrCommand)) { + LOG.warn("IP removed via TELNET by host {}!", _cSocket.getInetAddress().getHostAddress()); + _print.println("The IP " + _usrCommand + " has been removed from the hack protection list!"); + } else { + _print.println("IP not found in hack protection list..."); + } + } catch (StringIndexOutOfBoundsException e) { + _print.println("Please Enter the IP to Unblock!"); + } + } else if (_usrCommand.startsWith("shutdown")) { + LoginServer.getInstance().shutdown(false); + _print.println("Bye Bye!"); + _print.flush(); + _cSocket.close(); + } else if (_usrCommand.startsWith("restart")) { + LoginServer.getInstance().shutdown(true); + _print.println("Bye Bye!"); + _print.flush(); + _cSocket.close(); + } else if (_usrCommand.equals("RedirectLogger")) { + _redirectLogger = true; + } else if (_usrCommand.equals("quit")) { /* Do Nothing :p - Just here to save us from the "Command Not Understood" Text */ + } else if (_usrCommand.isEmpty()) { /* Do Nothing Again - Same reason as the quit part */ + } else { + _print.println("Invalid Command"); + } + _print.print(""); + _print.flush(); + } + if (!_cSocket.isClosed()) { + _print.println("Bye Bye!"); + _print.flush(); + _cSocket.close(); + } + telnetOutput(1, "Connection from " + _cSocket.getInetAddress().getHostAddress() + " was closed by client."); + } catch (Exception ex) { + LOG.warn("There has been an error executing login status task!", ex); + } + } + + public void printToTelnet(String msg) { + synchronized (_print) { + _print.println(msg); + _print.flush(); + } + } + + public boolean isRedirectLogger() { + return _redirectLogger; + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/status/Status.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/status/Status.java new file mode 100644 index 00000000..d4c45c48 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/status/Status.java @@ -0,0 +1,103 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.status; + +import static com.l2jserver.loginserver.config.Configuration.telnet; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.LinkedList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Util; + +public class Status extends Thread { + + private static final Logger LOG = LoggerFactory.getLogger(Status.class); + + private final ServerSocket statusServerSocket; + + private final int _uptime; + + private String _statusPw; + + private final List _loginStatus; + + public Status() throws IOException { + super("Status"); + _statusPw = telnet().getPassword(); + + if (_statusPw == null) { + _statusPw = Util.randomPassword(10); + LOG.info("Server's Telnet function has no password defined!"); + LOG.info("A password has been automatically created!"); + LOG.info("Password has been set to: {}", _statusPw); + } + + statusServerSocket = new ServerSocket(telnet().getPort()); + _uptime = (int) System.currentTimeMillis(); + _loginStatus = new LinkedList<>(); + LOG.info("Telnet server started successfully, listening on port {}.", telnet().getPort()); + } + + @Override + public void run() { + setPriority(Thread.MAX_PRIORITY); + + while (!isInterrupted()) { + try { + Socket connection = statusServerSocket.accept(); + LoginStatusThread lst = new LoginStatusThread(connection, _uptime, _statusPw); + if (lst.isAlive()) { + _loginStatus.add(lst); + } + + if (isInterrupted()) { + try { + statusServerSocket.close(); + } catch (Exception ex) { + LOG.warn("There has been an error closing status server socket!", ex); + } + break; + } + } catch (Exception ex1) { + if (isInterrupted()) { + try { + statusServerSocket.close(); + } catch (Exception ex2) { + LOG.warn("There has been an error closing status server socket!", ex2); + } + break; + } + } + } + } + + public void sendMessageToTelnets(String msg) { + for (LoginStatusThread ls : _loginStatus) { + if (!ls.isInterrupted()) { + ls.printToTelnet(msg); + } + } + } +} diff --git a/l2j-server-login/src/main/java/com/l2jserver/loginserver/util/IPSubnet.java b/l2j-server-login/src/main/java/com/l2jserver/loginserver/util/IPSubnet.java new file mode 100644 index 00000000..370a5733 --- /dev/null +++ b/l2j-server-login/src/main/java/com/l2jserver/loginserver/util/IPSubnet.java @@ -0,0 +1,132 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server 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. + * + * L2J Server 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 . + */ +package com.l2jserver.loginserver.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; + +public class IPSubnet { + final byte[] _addr; + final byte[] _mask; + final boolean _isIPv4; + + public IPSubnet(String input) throws UnknownHostException, NumberFormatException, ArrayIndexOutOfBoundsException { + int idx = input.indexOf("/"); + if (idx > 0) { + _addr = InetAddress.getByName(input.substring(0, idx)).getAddress(); + _mask = getMask(Integer.parseInt(input.substring(idx + 1)), _addr.length); + _isIPv4 = _addr.length == 4; + + if (!applyMask(_addr)) { + throw new UnknownHostException(input); + } + } else { + _addr = InetAddress.getByName(input).getAddress(); + _mask = getMask(_addr.length * 8, _addr.length); // host, no need to check mask + _isIPv4 = _addr.length == 4; + } + } + + public IPSubnet(InetAddress addr, int mask) throws UnknownHostException { + _addr = addr.getAddress(); + _isIPv4 = _addr.length == 4; + _mask = getMask(mask, _addr.length); + if (!applyMask(_addr)) { + throw new UnknownHostException(addr + "/" + mask); + } + } + + public byte[] getAddress() { + return _addr; + } + + public boolean applyMask(byte[] addr) { + // V4 vs V4 or V6 vs V6 checks + if (_isIPv4 == (addr.length == 4)) { + for (int i = 0; i < _addr.length; i++) { + if ((addr[i] & _mask[i]) != _addr[i]) { + return false; + } + } + } else { + // check for embedded v4 in v6 addr (not done !) + if (_isIPv4) { + // my V4 vs V6 + for (int i = 0; i < _addr.length; i++) { + if ((addr[i + 12] & _mask[i]) != _addr[i]) { + return false; + } + } + } else { + // my V6 vs V4 + for (int i = 0; i < _addr.length; i++) { + if ((addr[i] & _mask[i + 12]) != _addr[i + 12]) { + return false; + } + } + } + } + + return true; + } + + @Override + public String toString() { + int size = 0; + for (byte element : _mask) { + size += Integer.bitCount((element & 0xFF)); + } + + try { + return InetAddress.getByAddress(_addr) + "/" + size; + } catch (UnknownHostException e) { + return "Invalid"; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof IPSubnet) { + return applyMask(((IPSubnet) o).getAddress()); + } else if (o instanceof InetAddress) { + return applyMask(((InetAddress) o).getAddress()); + } + + return false; + } + + private static byte[] getMask(int n, int maxLength) throws UnknownHostException { + if ((n > (maxLength << 3)) || (n < 0)) { + throw new UnknownHostException("Invalid netmask: " + n); + } + + final byte[] result = new byte[maxLength]; + Arrays.fill(result, (byte) 0xFF); + + for (int i = (maxLength << 3) - 1; i >= n; i--) { + result[i >> 3] = (byte) (result[i >> 3] << 1); + } + + return result; + } +} \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/LoginServer_loop.sh b/l2j-server-login/src/main/resources/LoginServer_loop.sh new file mode 100644 index 00000000..db1bdfa1 --- /dev/null +++ b/l2j-server-login/src/main/resources/LoginServer_loop.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +err=1 +until [ $err -eq 0 ]; +do + [ -f log/java0.log.0 ] && mv log/java0.log.0 "log/`date +%Y-%m-%d_%H-%M-%S`_java.log" + [ -f log/stdout.log ] && mv log/stdout.log "log/`date +%Y-%m-%d_%H-%M-%S`_stdout.log" + java -Xms128m -Xmx256m -jar l2jlogin.jar >> log/stdout.log 2>&1 + err=$? + sleep 10; +done diff --git a/l2j-server-login/src/main/resources/config/banned_ip.cfg b/l2j-server-login/src/main/resources/config/banned_ip.cfg new file mode 100644 index 00000000..ee951522 --- /dev/null +++ b/l2j-server-login/src/main/resources/config/banned_ip.cfg @@ -0,0 +1,7 @@ +# Subnets can be defined like 123.45.0.0 +# Comments after # are ignored +# Expiration timestamp can be added after IP for when to disable the ban +# Example: +# 192.168.0.0 1237618800000 # Bans all address starting with 192.168 and will expire on 21 Mar 2009 07:00:00 GMT +10.255.255.255 +10.254.254.254 diff --git a/l2j-server-login/src/main/resources/config/database.properties b/l2j-server-login/src/main/resources/config/database.properties new file mode 100644 index 00000000..fbb9fb17 --- /dev/null +++ b/l2j-server-login/src/main/resources/config/database.properties @@ -0,0 +1,21 @@ +# --------------------------------------------------------------------------- +# Database +# --------------------------------------------------------------------------- + +# Database URL +# Default: jdbc:mariadb://localhost/l2jls +URL = jdbc:mariadb://localhost/l2jls + +# Database User +# Default: l2j +User = l2j + +# Database Password +# Default: l2jserver2019 +Password = l2jserver2019 + +# Default: 10 +MaxConnections = 10 + +# Default: 0 +MaxIdleTime = 0 \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/config/email.properties b/l2j-server-login/src/main/resources/config/email.properties new file mode 100644 index 00000000..5688561d --- /dev/null +++ b/l2j-server-login/src/main/resources/config/email.properties @@ -0,0 +1,65 @@ +# --------------------------------------------------------------------------- +# Email Settings +# --------------------------------------------------------------------------- +# The defaults are set to be retail-like. If you modify any of these settings your server will deviate from being retail-like. +# Warning: +# Please take extreme caution when changing anything. Also, please understand what you are changing before you do so on a live server. +# --------------------------------------------------------------------------- +# Server Information +# --------------------------------------------------------------------------- + +# Server Name +# Default: L2J Server +ServerName = L2J Server + +# Contact Address +# Default: info@l2jserver.com +ServerEmail = info@l2jserver.com + +# --------------------------------------------------------------------------- +# E-mail System Settings +# --------------------------------------------------------------------------- + +# Enable Email System +# Default: False +EnableEmail = False + +# Select Query for Email Addresses +# Default: SELECT value FROM account_data WHERE account_name=? AND var='email_addr' +EmailSelectQuery = SELECT value FROM account_data WHERE account_name=? AND var='email_addr' + +# Email Address Field +# Default: value +DatabaseField = value + +# Mail Server Host +# Default: smtp.gmail.com +Host = smtp.gmail.com + +# Mail Server Port +# Default: 465 +Port = 465 + +# Auth SMTP +# Default: True +SmtpAuthRequired = True + +# Mail Socket Factory +# Default: javax.net.ssl.SSLSocketFactory +SmtpFactory = javax.net.ssl.SSLSocketFactory + +# Mail Factory Callback +# Default: False +SmtpFactoryCallback = False + +# Mail Server Auth - Username +# Default: user@gmail.com +SmtpUsername = user@gmail.com + +# Mail Server Auth - Password +# Default: password +SmtpPassword = password + +# Mail Address +# Default: noreply@l2jserver.com +SystemAddress = noreply@l2jserver.com \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/config/mmo.properties b/l2j-server-login/src/main/resources/config/mmo.properties new file mode 100644 index 00000000..5e48a3d3 --- /dev/null +++ b/l2j-server-login/src/main/resources/config/mmo.properties @@ -0,0 +1,24 @@ +#--------------------------------------------------------------- +# MMO +#--------------------------------------------------------------- + +# Sleep time for all Selectors +# After he finished his job the Selector waits the given time in milliseconds +# Lower values will speed up the loop and the Ping is smaller +SleepTime = 20 + +# Every loop, it sends a maximum of the given packages to each connection +# Lower values will speed up the loop and the Ping is smaller but cause less output +MaxSendPerPass = 12 + +# Every loop it read a maximum of the given packages from each connection +# Lower values will speed up the loop and the Ping is smaller but cause less input +MaxReadPerPass = 12 + +# Each unfinished read/write need a TEMP storage Buffer +# on large player amount we need more Buffers +# if there are not enough buffers new ones are generated but not stored for future usage +HelperBufferCount = 20 + +# Setting this to True will lower your ping, at the cost of an increase in bandwidth consumption. +TcpNoDelay = False \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/config/server.properties b/l2j-server-login/src/main/resources/config/server.properties new file mode 100644 index 00000000..79df5490 --- /dev/null +++ b/l2j-server-login/src/main/resources/config/server.properties @@ -0,0 +1,116 @@ +# --------------------------------------------------------------------------- +# Login Server Settings +# --------------------------------------------------------------------------- +# This is the server configuration file. Here you can set up the connection information for your server. +# This was written with the assumption that you are behind a router. +# Definitions: +# LAN (Local area network) - typically consists of computers connected to the same router as you. +# WAN (Wide area network) - typically consists of computers OUTSIDE your router (i.e. the Internet). +# x.x.x.x - Format of an IP address. Do not include the xes into settings. Must be real numbers. +# --------------------------------------------------------------------------- +# Networking +# --------------------------------------------------------------------------- + +# Enables automatic port mapping for login server. +# If you have a router login server will request for port forwarding. +# Default: True +EnableUPnP = True + +# Bind IP of the LoginServer, use * to bind on all available IPs +# WARNING: Please don't change default IPs here if you don't know what are you doing! +# WARNING: External/Internal IPs are now inside "ipconfig.xml" file. +# Default: * (0.0.0.0) +Host = * + +# Default: 2106 +Port = 2106 + +# The address on which login will listen for GameServers, use * to bind on all available IPs +# WARNING: Please don't change default IPs here if you don't know what are you doing! +# WARNING: External/Internal IPs are now inside "ipconfig.xml" file. +# Default: 127.0.0.1 +GameServerHost = 127.0.0.1 + +# The port on which login will listen for GameServers +# Default: 9014 +GameServerPort = 9014 + +# Allowed Server Versions +# Default: 6, 14 +ServerVersions = 6, 14 + +# --------------------------------------------------------------------------- +# Security +# --------------------------------------------------------------------------- + +# How many times you can provide an invalid account/pass before the IP gets banned. +# Default: 5 +LoginTryBeforeBan = 5 + +# Time you won't be able to log in back again after LoginTryBeforeBan tries to login. +# Default: 900 (15 minutes) +LoginBlockAfterBan = 900 + +# If set to True any GameServer can register on your login's free slots +# Default: True +AcceptNewGameServer = True + +# Flood Protection. All values are in milliseconds. +# Default: True +EnableFloodProtection = True + +# Default: 15 +FastConnectionLimit = 15 + +# Default: 700 +NormalConnectionTime = 700 + +# Default: 350 +FastConnectionTime = 350 + +# Default: 50 +MaxConnectionPerIP = 50 + +# --------------------------------------------------------------------------- +# Well known access levels. Access levels specified here must be negative. 0 +# is always a normal user and positive is a GM. +# --------------------------------------------------------------------------- +# Default: -127 +AccountInactiveAccessLevel = -127 + +# --------------------------------------------------------------------------- +# Misc. +# --------------------------------------------------------------------------- + +# If False, the license (after the login) will not be shown. +# Default: True +ShowLicence = True + +# Default: True +AutoCreateAccounts = True + +# Specifies the access level which is set for auto created accounts. +# Default: 0 +AutoCreateAccountsAccessLevel = 0 + +# Datapack root directory. +# Defaults to current directory from which the server is started. +DatapackRoot = . + +# --------------------------------------------------------------------------- +# Developer Settings +# --------------------------------------------------------------------------- + +# Default: False +Debug = False + +# --------------------------------------------------------------------------- +# Restart LS every 24 hours? +# --------------------------------------------------------------------------- + +# Enable disable scheduled login restart. +# Default: False +LoginRestartSchedule = False +# Time in hours. +# Default: 24 +LoginRestartTime = 24 \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/config/telnet.properties b/l2j-server-login/src/main/resources/config/telnet.properties new file mode 100644 index 00000000..efa42dde --- /dev/null +++ b/l2j-server-login/src/main/resources/config/telnet.properties @@ -0,0 +1,28 @@ +# --------------------------------------------------------------------------- +# Telnet Settings +# --------------------------------------------------------------------------- +# The defaults are set to be retail-like. If you modify any of these settings your server will deviate from being retail-like. +# Warning: +# Please take extreme caution when changing anything. Also, please understand what you are changing before you do so on a live server. +# --------------------------------------------------------------------------- + +# Allows text based monitoring and administration of L2J LS +# by using a telnet client. Communication protocol is insecure, +# and you should use SSL tunnels, VPN, etc. if you plan to connect +# over non-trusted channels. +# Default: False +EnableTelnet = False + +# This is the port L2J should listen to for incoming telnet +# requests. +# Default: 12345 +Port = 12345 + +# If the following is not set, a random password is generated on server startup. +# Usage: Password = somePass +Password = + +# This list can contain IPs or Hosts of clients you wish to allow. Hostnames must be resolvable to an IP. +# Example: 0.0.0.0,host,0.0.0.1,host2,host3,host4,0.0.0.3 +# Default: 127.0.0.1,localhost +Hosts = 127.0.0.1,localhost \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/data/mail/MailList.xml b/l2j-server-login/src/main/resources/data/mail/MailList.xml new file mode 100644 index 00000000..4fed34e1 --- /dev/null +++ b/l2j-server-login/src/main/resources/data/mail/MailList.xml @@ -0,0 +1,4 @@ + + + + diff --git a/l2j-server-login/src/main/resources/data/mail/MailList.xsd b/l2j-server-login/src/main/resources/data/mail/MailList.xsd new file mode 100644 index 00000000..d13b894e --- /dev/null +++ b/l2j-server-login/src/main/resources/data/mail/MailList.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/data/mail/html/SecAuthTempBan.htm b/l2j-server-login/src/main/resources/data/mail/html/SecAuthTempBan.htm new file mode 100644 index 00000000..cc2dfbfc --- /dev/null +++ b/l2j-server-login/src/main/resources/data/mail/html/SecAuthTempBan.htm @@ -0,0 +1,15 @@ +Greetings,
+
+We have temporally suspended your account:
+
+Account Name: %accountname%
+Reason: Typed wrong character password %var1% times in row from ip %var0%.
+Consequences: Your account has been temporally suspended for %var2% minutes.
+
+If you've lost your character password, you can recover them by
clicking here.
+
+If you have any question, click here to email an administrator.
+Regards,
+%servername% Game Master Team
+
+

Powered By: L2J Server

diff --git a/l2j-server-login/src/main/resources/data/servername.xml b/l2j-server-login/src/main/resources/data/servername.xml new file mode 100644 index 00000000..a4247445 --- /dev/null +++ b/l2j-server-login/src/main/resources/data/servername.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/data/servername.xsd b/l2j-server-login/src/main/resources/data/servername.xsd new file mode 100644 index 00000000..13f9c223 --- /dev/null +++ b/l2j-server-login/src/main/resources/data/servername.xsd @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/l2j-server-login-logo_256x256.png b/l2j-server-login/src/main/resources/l2j-server-login-logo_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..9937161605a0d9191411a4fa45019ffc8bd7c8fa GIT binary patch literal 19918 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 literal 0 HcmV?d00001 diff --git a/l2j-server-login/src/main/resources/log4j2.xml b/l2j-server-login/src/main/resources/log4j2.xml new file mode 100644 index 00000000..b67d9b40 --- /dev/null +++ b/l2j-server-login/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/sql/account_data.sql b/l2j-server-login/src/main/resources/sql/account_data.sql new file mode 100644 index 00000000..e4b0c887 --- /dev/null +++ b/l2j-server-login/src/main/resources/sql/account_data.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS `account_data` ( + `account_name` VARCHAR(45) NOT NULL DEFAULT '', + `var` VARCHAR(20) NOT NULL DEFAULT '', + `value` VARCHAR(255) , + PRIMARY KEY (`account_name`,`var`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/sql/accounts.sql b/l2j-server-login/src/main/resources/sql/accounts.sql new file mode 100644 index 00000000..ef5b3110 --- /dev/null +++ b/l2j-server-login/src/main/resources/sql/accounts.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS `accounts` ( + `login` VARCHAR(45) NOT NULL default '', + `password` VARCHAR(45), + `email` varchar(255) DEFAULT NULL, + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `lastactive` bigint(13) unsigned NOT NULL DEFAULT '0', + `accessLevel` TINYINT NOT NULL DEFAULT 0, + `lastIP` CHAR(15) NULL DEFAULT NULL, + `lastServer` TINYINT DEFAULT 1, + `pcIp` char(15) DEFAULT NULL, + `hop1` char(15) DEFAULT NULL, + `hop2` char(15) DEFAULT NULL, + `hop3` char(15) DEFAULT NULL, + `hop4` char(15) DEFAULT NULL, + PRIMARY KEY (`login`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/sql/accounts_ipauth.sql b/l2j-server-login/src/main/resources/sql/accounts_ipauth.sql new file mode 100644 index 00000000..ac24ff7d --- /dev/null +++ b/l2j-server-login/src/main/resources/sql/accounts_ipauth.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS `accounts_ipauth` ( + `login` varchar(45) NOT NULL, + `ip` char(15) NOT NULL, + `type` enum('deny','allow') NULL DEFAULT 'allow' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/sql/cleanup/cleanup.sql b/l2j-server-login/src/main/resources/sql/cleanup/cleanup.sql new file mode 100644 index 00000000..8bf8d961 --- /dev/null +++ b/l2j-server-login/src/main/resources/sql/cleanup/cleanup.sql @@ -0,0 +1,10 @@ +-- These queries are meant to delete any login server table you may have from previous L2J installations. +-- +-- Queries that create these tables and/or populate them appropriately are +-- available in the ../sql/ folder. See the documentation.txt file for more details. + +DROP TABLE IF EXISTS +account_data, +accounts, +accounts_ipauth, +gameservers; \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/sql/gameservers.sql b/l2j-server-login/src/main/resources/sql/gameservers.sql new file mode 100644 index 00000000..a8e24819 --- /dev/null +++ b/l2j-server-login/src/main/resources/sql/gameservers.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS `gameservers` ( + `server_id` int(11) NOT NULL DEFAULT '0', + `hexid` varchar(50) NOT NULL DEFAULT '', + `host` varchar(50) NOT NULL DEFAULT '', + PRIMARY KEY (`server_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/startLoginServer.bat b/l2j-server-login/src/main/resources/startLoginServer.bat new file mode 100644 index 00000000..3b022e13 --- /dev/null +++ b/l2j-server-login/src/main/resources/startLoginServer.bat @@ -0,0 +1,29 @@ +@echo off +title Login Server Console + +:start +echo Starting L2J Login Server. +echo. + +java -Xms128m -Xmx256m -jar l2jlogin.jar + +if ERRORLEVEL 2 goto restart +if ERRORLEVEL 1 goto error +goto end + +:restart +echo. +echo Admin Restarted Login Server. +echo. +goto start + +:error +echo. +echo Login Server terminated abnormally! +echo. + +:end +echo. +echo Login Server Terminated. +echo. +pause \ No newline at end of file diff --git a/l2j-server-login/src/main/resources/startLoginServer.sh b/l2j-server-login/src/main/resources/startLoginServer.sh new file mode 100644 index 00000000..90e07a64 --- /dev/null +++ b/l2j-server-login/src/main/resources/startLoginServer.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +./LoginServer_loop.sh &