diff --git a/DiscordAuthSystem_module/README.md b/DiscordAuthSystem_module/README.md new file mode 100644 index 00000000..62835cc3 --- /dev/null +++ b/DiscordAuthSystem_module/README.md @@ -0,0 +1,148 @@ +# DiscordAuthSystem + +Позволяет входить в лаунчер через Discord. +Модуль использует библиотеку [JSOUP](https://jsoup.org/download). + +#### Установка модуля + +1. Скопировать модуль **DiscordAuthSystem_module.jar** в папку **/LaunchServer/modules/** +2. Создать приложение в панели управления разработчика https://discord.com/developers/applications, и секретный токен. +Если вам нужно проверять находится ли пользователь в необходимых вам гильдиях, то опциональные пункты обязательны. + 1. Скопировать его CLIENT ID + 2. Скопировать его CLIENT SECRET + 3. [Опционально] Создать бота из данного приложения + 4. [Опционально] Добавить его на необходимые вам сервера. + 5. [Опционально] В настройках бота включить пункт "SERVER MEMBERS INTENT". +4. В настройках приложение discord oauth добавить redirect_url. Он должен состоять из пути до webapi + /auth/discord. Пример: http://127.0.0.1:9274/webapi/auth/discord +5. Настроить конфигурацию модуля +6. Добавить авторизацию в LaunchServer +7. [Опционально] Обновить Runtime + +#### Конфигурация модуля + +```json +{ + "clientId": "сюда вставляется id", + "clientSecret": "сюда вставляется секрет", + "redirectUrl": "это редирект, который вы указали", + "discordAuthorizeUrl": "https://discord.com/oauth2/authorize", + "discordApiEndpointVersion": "https://discord.com/api/v10", + "discordApiEndpoint": "https://discord.com/api", + "guildIdsJoined": [ + { + "id": "id гильдии №1", + "name": "наименование гильдии", + "url": "ссылка для входа" + }, + { + "id": "id гильдии №2", + "name": "наименование гильдии", + "url": "ссылка для входа" + } + ], + "guildIdGetNick": "id гильдии с которой будет браться ник. если не надо, то оставить пустым", + "usernameRegex": "regex для валидации ника (если не нужно, то оставьте пустым)" +} +``` + +#### Конфигурация в LaunchServer + +```json +{ + "std": { + "isDefault": true, + "core": { + "type": "discordauthsystem", + "mySQLHolder": { + "address": "localhost", + "port": 3306, + "username": "root", + "password": "root", + "database": "test", + "useHikari": false + }, + "uuidColumn": "uuid", + "usernameColumn": "username", + "accessTokenColumn": "accessToken", + "refreshTokenColumn": "refreshToken", + "expiresInColumn": "expiresIn", + "discordIdColumn": "discordId", + "bannedAtColumn": "bannedAt", + "hardwareIdColumn": "hwidId", + "serverIDColumn": "serverID", + "table": "users", + "tableHwid": "hwids" + }, + "textureProvider": { + "skinURL": "http://example.com/skins/%username%.png", + "cloakURL": "http://example.com/cloaks/%username%.png", + "type": "request" + }, + "displayName": "Default" + } +} +``` + +- В mySQLHolder указывается коннект к mysql (данные аккаунтов хрантся там) +- \*\*\*\*Column - строки наименования колонок. +- tableHwid - таблица hwid юзеров. + +#### Дефолтный запрос на создание таблицы + +```mysql +-- Создаём таблицу пользователей +CREATE TABLE `users` ( + `uuid` CHAR(36) UNIQUE, + `username` CHAR(32) UNIQUE, + `accessToken` CHAR(32) DEFAULT NULL, + `refreshToken` CHAR(32) DEFAULT NULL, + `expiresIn` BIGINT DEFAULT NULL, + `discordId` VARCHAR(32) DEFAULT NULL, + `bannedAt` DATETIME DEFAULT NULL, + `serverID` VARCHAR(41) DEFAULT NULL, + `hwidId` BIGINT DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Создаём таблицу hwids данных +CREATE TABLE `hwids` ( + `id` bigint(20) NOT NULL, + `publickey` blob, + `hwDiskId` varchar(255) DEFAULT NULL, + `baseboardSerialNumber` varchar(255) DEFAULT NULL, + `graphicCard` varchar(255) DEFAULT NULL, + `displayId` blob, + `bitness` int(11) DEFAULT NULL, + `totalMemory` bigint(20) DEFAULT NULL, + `logicalProcessors` int(11) DEFAULT NULL, + `physicalProcessors` int(11) DEFAULT NULL, + `processorMaxFreq` bigint(11) DEFAULT NULL, + `battery` tinyint(1) NOT NULL DEFAULT "0", + `banned` tinyint(1) NOT NULL DEFAULT "0" +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Добавляем модификаторы hwids таблицы +ALTER TABLE `hwids` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `publickey` (`publickey`(255)); +ALTER TABLE `hwids` + MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT; + +-- Связываем пользователей и hwids +ALTER TABLE `users` + ADD CONSTRAINT `users_hwidfk` FOREIGN KEY (`hwidId`) REFERENCES `hwids` (`id`); +``` + +#### [Опционально] Обновить Runtime + +Если вы хотите, чтобы окно открывалось в браузере, а также авторизация у +пользователя сохранялась, то необходимо будет отредактировать (пропатчить) и пересобрать runtime. +Модуль будет работать и без этого, но не так красиво. + +```shell +cd ./src/srcRuntime +git am DiscordAuthSystemRuntime.patch #Надеюсь не нужно объяснять, +# что тут нужен путь до файла DiscordAuthSystemRuntime.patch +gradlew build +``` + +Если вам впадлу делать все эти изменения, то я приложил готовы билд рантайма. Он лежит рядом с билдом модуля. \ No newline at end of file diff --git a/DiscordAuthSystem_module/build.gradle b/DiscordAuthSystem_module/build.gradle new file mode 100644 index 00000000..455bc585 --- /dev/null +++ b/DiscordAuthSystem_module/build.gradle @@ -0,0 +1,17 @@ +def mainClassName = "pro.gravit.launchermodules.discordauthsystem.ModuleImpl" +def configClassName = "pro.gravit.launchermodules.discordauthsystem.Config" + +sourceCompatibility = '17' +targetCompatibility = '17' + +dependencies { + implementation("org.jsoup:jsoup:1.15.1") + implementation("com.ibm.icu:icu4j:71.1") + implementation("com.github.slugify:slugify:3.0.1") +} + +jar { + manifest.attributes("Module-Main-Class": mainClassName, + "Module-Config-Class": configClassName, + ) +} diff --git a/DiscordAuthSystem_module/patch/DiscordAuthSystemRuntime.patch b/DiscordAuthSystem_module/patch/DiscordAuthSystemRuntime.patch new file mode 100644 index 00000000..587b4165 --- /dev/null +++ b/DiscordAuthSystem_module/patch/DiscordAuthSystemRuntime.patch @@ -0,0 +1,129 @@ +From ddeb1507e0a6e74737877906db56c62769dd9b2b Mon Sep 17 00:00:00 2001 +From: Meido +Date: Wed, 29 Jun 2022 12:17:57 +0700 +Subject: [PATCH] DiscordAuthSystemRuntime + +--- + .../client/gui/impl/GuiEventHandler.java | 18 +++++++++-- + .../client/gui/scenes/login/LoginScene.java | 9 ++++++ + .../scenes/login/methods/WebAuthMethod.java | 31 ++++++++++++++----- + 3 files changed, 48 insertions(+), 10 deletions(-) + +diff --git a/src/main/java/pro/gravit/launcher/client/gui/impl/GuiEventHandler.java b/src/main/java/pro/gravit/launcher/client/gui/impl/GuiEventHandler.java +index c817abe..c9b5f02 100644 +--- a/src/main/java/pro/gravit/launcher/client/gui/impl/GuiEventHandler.java ++++ b/src/main/java/pro/gravit/launcher/client/gui/impl/GuiEventHandler.java +@@ -3,12 +3,15 @@ package pro.gravit.launcher.client.gui.impl; + import pro.gravit.launcher.client.gui.JavaFXApplication; + import pro.gravit.launcher.client.gui.scenes.login.LoginScene; + import pro.gravit.launcher.events.RequestEvent; ++import pro.gravit.launcher.events.request.AdditionalDataRequestEvent; + import pro.gravit.launcher.events.request.AuthRequestEvent; + import pro.gravit.launcher.request.RequestService; + import pro.gravit.launcher.request.WebSocketEvent; + import pro.gravit.launcher.request.websockets.ClientWebSocketService; + import pro.gravit.utils.helper.LogHelper; + ++import java.util.Map; ++ + public class GuiEventHandler implements RequestService.EventHandler { + private final JavaFXApplication application; + +@@ -26,11 +29,22 @@ public class GuiEventHandler implements RequestService.EventHandler { + try { + if (event instanceof AuthRequestEvent) { + boolean isNextScene = application.getCurrentScene() instanceof LoginScene; ++ AuthRequestEvent rawAuthResult = (AuthRequestEvent) event; + ((LoginScene) application.getCurrentScene()).isLoginStarted = true; + LogHelper.dev("Receive auth event. Send next scene %s", isNextScene ? "true" : "false"); +- application.stateService.setAuthResult(null, (AuthRequestEvent) event); +- if (isNextScene && ((LoginScene) application.getCurrentScene()).isLoginStarted) ++ application.stateService.setAuthResult(null, rawAuthResult); ++ if (isNextScene && ((LoginScene) application.getCurrentScene()).isLoginStarted) { + ((LoginScene) application.getCurrentScene()).onGetProfiles(); ++ ++ if (((LoginScene) application.getCurrentScene()).getSavePasswordCheckBoxSelected()) { ++ application.runtimeSettings.login = rawAuthResult.playerProfile.username; ++ application.runtimeSettings.oauthAccessToken = rawAuthResult.oauth.accessToken; ++ application.runtimeSettings.oauthRefreshToken = rawAuthResult.oauth.refreshToken; ++ application.runtimeSettings.oauthExpire = System.currentTimeMillis() + rawAuthResult.oauth.expire; ++ application.runtimeSettings.lastAuth = ((LoginScene) application.getCurrentScene()).getAuthAvailability(); ++ } ++ } ++ + } + } catch (Throwable e) { + LogHelper.error(e); +diff --git a/src/main/java/pro/gravit/launcher/client/gui/scenes/login/LoginScene.java b/src/main/java/pro/gravit/launcher/client/gui/scenes/login/LoginScene.java +index 80cf2ed..d12f6c0 100644 +--- a/src/main/java/pro/gravit/launcher/client/gui/scenes/login/LoginScene.java ++++ b/src/main/java/pro/gravit/launcher/client/gui/scenes/login/LoginScene.java +@@ -146,6 +146,15 @@ public class LoginScene extends AbstractScene { + LogHelper.info("Selected auth: %s", authAvailability.name); + } + ++ public GetAvailabilityAuthRequestEvent.AuthAvailability getAuthAvailability() { ++ return this.authAvailability; ++ } ++ ++ public boolean getSavePasswordCheckBoxSelected() { ++ CheckBox checkBox = this.savePasswordCheckBox; ++ return checkBox != null && checkBox.isSelected(); ++ } ++ + public void addAuthAvailability(GetAvailabilityAuthRequestEvent.AuthAvailability authAvailability) { + RadioButton radio = new RadioButton(); + radio.setToggleGroup(authToggleGroup); +diff --git a/src/main/java/pro/gravit/launcher/client/gui/scenes/login/methods/WebAuthMethod.java b/src/main/java/pro/gravit/launcher/client/gui/scenes/login/methods/WebAuthMethod.java +index cf888f1..d6cb6f9 100644 +--- a/src/main/java/pro/gravit/launcher/client/gui/scenes/login/methods/WebAuthMethod.java ++++ b/src/main/java/pro/gravit/launcher/client/gui/scenes/login/methods/WebAuthMethod.java +@@ -13,6 +13,10 @@ import pro.gravit.launcher.request.auth.details.AuthWebViewDetails; + import pro.gravit.launcher.request.auth.password.AuthCodePassword; + import pro.gravit.utils.helper.LogHelper; + ++import java.awt.Desktop; ++import java.io.IOException; ++import java.net.URI; ++import java.net.URISyntaxException; + import java.util.concurrent.CompletableFuture; + import java.util.function.Consumer; + +@@ -52,15 +56,26 @@ public class WebAuthMethod extends AbstractAuthMethod { + @Override + public CompletableFuture auth(AuthWebViewDetails details) { + overlay.future = new CompletableFuture<>(); +- overlay.follow(details.url, details.redirectUrl, (r) -> { +- String code = r; +- LogHelper.debug("Code: %s", code); +- if(code.startsWith("?code=")) { +- code = r.substring("?code=".length(), r.indexOf("&")); ++ if (details.onlyBrowser) { ++ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { ++ try { ++ Desktop.getDesktop().browse(new URI(details.url)); ++ } catch (IOException | URISyntaxException e) { ++ e.printStackTrace(); ++ } + } +- LogHelper.debug("Code: %s", code); +- overlay.future.complete(new LoginScene.LoginAndPasswordResult(null, new AuthCodePassword(code))); +- }); ++ overlay.disable(); ++ } else { ++ overlay.follow(details.url, details.redirectUrl, (r) -> { ++ String code = r; ++ LogHelper.debug("Code: %s", code); ++ if(code.startsWith("?code=")) { ++ code = r.substring("?code=".length(), r.indexOf("&")); ++ } ++ LogHelper.debug("Code: %s", code); ++ overlay.future.complete(new LoginScene.LoginAndPasswordResult(null, new AuthCodePassword(code))); ++ }); ++ } + return overlay.future; + } + +-- +2.29.2.windows.2 + diff --git a/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/Config.java b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/Config.java new file mode 100644 index 00000000..88627387 --- /dev/null +++ b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/Config.java @@ -0,0 +1,29 @@ +package pro.gravit.launchermodules.discordauthsystem; + +import javax.xml.crypto.dsig.keyinfo.KeyValue; +import java.util.*; + +public class Config { + public String clientId = "clientId"; + public String clientSecret = "clientSecret"; + public String redirectUrl = "redirectUrl"; + public String discordAuthorizeUrl = "https://discord.com/oauth2/authorize"; + public String discordApiEndpointVersion = "https://discord.com/api/v10"; + public String discordApiEndpoint = "https://discord.com/api"; + public List guildIdsJoined = new ArrayList<>(); + public String guildIdGetNick = ""; + public String usernameRegex = ""; + + public static class DiscordGuild { + public String id; + public String name; + public String url; + + public DiscordGuild(String id, String name, String url) { + this.id = id; + this.name = name; + this.url = url; + } + } +} + diff --git a/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/ModuleImpl.java b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/ModuleImpl.java new file mode 100644 index 00000000..efa140f6 --- /dev/null +++ b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/ModuleImpl.java @@ -0,0 +1,61 @@ +package pro.gravit.launchermodules.discordauthsystem; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import pro.gravit.launcher.config.JsonConfigurable; +import pro.gravit.launcher.modules.LauncherInitContext; +import pro.gravit.launcher.modules.LauncherModule; +import pro.gravit.launcher.modules.LauncherModuleInfo; +import pro.gravit.launcher.modules.events.PreConfigPhase; +import pro.gravit.launchermodules.discordauthsystem.providers.DiscordApi; +import pro.gravit.launchermodules.discordauthsystem.providers.DiscordSystemAuthCoreProvider; +import pro.gravit.launchermodules.discordauthsystem.responses.ExitResponse; +import pro.gravit.launchserver.LaunchServer; +import pro.gravit.launchserver.auth.core.AuthCoreProvider; +import pro.gravit.launchserver.modules.events.LaunchServerFullInitEvent; +import pro.gravit.launchserver.socket.WebSocketService; +import pro.gravit.launchserver.socket.handlers.NettyWebAPIHandler; +import pro.gravit.utils.Version; +import pro.gravit.utils.helper.LogHelper; + +import java.io.IOException; + +public class ModuleImpl extends LauncherModule { + public static final Version version = new Version(1, 0, 0, 0, Version.Type.LTS); + private static boolean registred = false; + private final transient Logger logger = LogManager.getLogger(); + public JsonConfigurable jsonConfigurable; + public Config config; + + public ModuleImpl() { + super(new LauncherModuleInfo("DiscordAuthSystem", version, new String[]{"LaunchServerCore"})); + } + + public void preConfig(PreConfigPhase preConfigPhase) { + if (!registred) { + AuthCoreProvider.providers.register("discordauthsystem", DiscordSystemAuthCoreProvider.class); + WebSocketService.providers.unregister("exit"); + WebSocketService.providers.register("exit", ExitResponse.class); + registred = true; + } + } + + @Override + public void init(LauncherInitContext initContext) { + registerEvent(this::preConfig, PreConfigPhase.class); + registerEvent(this::finish, LaunchServerFullInitEvent.class); + jsonConfigurable = modulesConfigManager.getConfigurable(Config.class, moduleInfo.name); + } + + public void finish(LaunchServerFullInitEvent event) { + LaunchServer launchServer = event.server; + try { + jsonConfigurable.loadConfig(); + config = jsonConfigurable.getConfig(); + } catch (IOException e) { + LogHelper.error(e); + } + DiscordApi.initialize(config); + NettyWebAPIHandler.addNewSeverlet("auth/discord", new WebApi(this, launchServer)); + } +} \ No newline at end of file diff --git a/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/WebApi.java b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/WebApi.java new file mode 100644 index 00000000..c06028f5 --- /dev/null +++ b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/WebApi.java @@ -0,0 +1,153 @@ +package pro.gravit.launchermodules.discordauthsystem; + +import com.github.slugify.Slugify; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import io.netty.util.CharsetUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import pro.gravit.launcher.ClientPermissions; +import pro.gravit.launcher.events.RequestEvent; +import pro.gravit.launcher.events.request.AdditionalDataRequestEvent; +import pro.gravit.launcher.events.request.AuthRequestEvent; +import pro.gravit.launcher.profiles.PlayerProfile; +import pro.gravit.launcher.request.auth.password.AuthCodePassword; +import pro.gravit.launchermodules.discordauthsystem.providers.DiscordApi; +import pro.gravit.launchermodules.discordauthsystem.providers.DiscordSystemAuthCoreProvider; +import pro.gravit.launchserver.LaunchServer; +import pro.gravit.launchserver.auth.AuthException; +import pro.gravit.launchserver.auth.AuthProviderPair; +import pro.gravit.launchserver.manangers.AuthManager; +import pro.gravit.launchserver.socket.Client; +import pro.gravit.launchserver.socket.NettyConnectContext; +import pro.gravit.launchserver.socket.handlers.NettyWebAPIHandler; +import pro.gravit.launchserver.socket.response.auth.AuthResponse; + +import java.io.File; +import java.text.Normalizer; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +public class WebApi implements NettyWebAPIHandler.SimpleSeverletHandler { + + private static final Pattern NONLATIN = Pattern.compile("[^\\w-]"); + private static final Pattern WHITESPACE = Pattern.compile("[\\s]"); + private final ModuleImpl module; + private final LaunchServer server; + private transient final Logger logger = LogManager.getLogger(); + private final Slugify slg = Slugify.builder().underscoreSeparator(true).lowerCase(false).transliterator(true).build(); + + public WebApi(ModuleImpl module, LaunchServer server) { + this.module = module; + this.server = server; + } + + public static String toSlug(String input) { + String nowhitespace = WHITESPACE.matcher(input).replaceAll("_"); + String normalized = Normalizer.normalize(nowhitespace, Normalizer.Form.NFD); + return NONLATIN.matcher(normalized).replaceAll(""); + } + + @Override + public void handle(ChannelHandlerContext ctx, FullHttpRequest msg, NettyConnectContext context) throws Exception { + Map params = getParamsFromUri(msg.uri()); + + String state = params.get("state"); + + if (state == null || state.isEmpty()) { + sendHttpResponse(ctx, simpleResponse(HttpResponseStatus.NOT_FOUND, "The \"state\" parameter was not found.")); + return; + } + + AtomicBoolean userFined = new AtomicBoolean(false); + + server.nettyServerSocketHandler.nettyServer.service.forEachActiveChannels((ch, ws) -> { + + Client client = ws.getClient(); + if (client == null) { + return; + } + + String wsState = client.getProperty("state"); + if (wsState == null || wsState.isEmpty() || !wsState.equals(state)) { + return; + } + + userFined.set(true); + }); + + if (!userFined.get()) { + sendHttpResponse(ctx, simpleResponse(HttpResponseStatus.NOT_FOUND, "The \"state\" parameter is invalid.")); + return; + } + + String code = params.get("code"); + + if (code == null || code.isEmpty()) { + sendHttpResponse(ctx, simpleResponse(HttpResponseStatus.NOT_FOUND, "The \"code\" parameter was not found.")); + return; + } + + AuthProviderPair pair = server.config.getAuthProviderPair(); + AuthManager.AuthReport report; + + try { + report = pair.core.authorize("", null, new AuthCodePassword(code), true); + } catch (AuthException e) { + sendHttpResponse(ctx, simpleHtmlResponse(HttpResponseStatus.FORBIDDEN, e.getMessage())); + return; + } + + String minecraftAccessToken = report.minecraftAccessToken(); + AuthRequestEvent.OAuthRequestEvent oauth = new AuthRequestEvent.OAuthRequestEvent(report.oauthAccessToken(), report.oauthRefreshToken(), report.oauthExpire()); + + DiscordSystemAuthCoreProvider.DiscordUser user = (DiscordSystemAuthCoreProvider.DiscordUser) report.session().getUser(); + + server.nettyServerSocketHandler.nettyServer.service.forEachActiveChannels((ch, ws) -> { + + Client client = ws.getClient(); + if (client == null) { + return; + } + + String wsState = client.getProperty("state"); + if (wsState == null || wsState.isEmpty() || !wsState.equals(state)) { + return; + } + + client.coreObject = user; + client.sessionObject = report.session(); + server.authManager.internalAuth(client, AuthResponse.ConnectTypes.CLIENT, pair, user.getUsername(), user.getUUID(), ClientPermissions.DEFAULT, true); + PlayerProfile playerProfile = server.authManager.getPlayerProfile(client); + AuthRequestEvent request = new AuthRequestEvent(ClientPermissions.DEFAULT, playerProfile, minecraftAccessToken, null, null, oauth); + request.requestUUID = RequestEvent.eventUUID; + + server.nettyServerSocketHandler.nettyServer.service.sendObject(ch, request); + }); + sendHttpResponse(ctx, simpleResponse(HttpResponseStatus.OK, "You are successfully authorized! Please return to the launcher.")); + } + + public FullHttpResponse simpleHtmlResponse(HttpResponseStatus status, String body) { + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK); + response.setStatus(status); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); + + StringBuilder buf = new StringBuilder() + .append("\r\n") + .append("") + .append("\r\n") + .append(body) + .append("\r\n"); + + ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8); + response.content().writeBytes(buffer); + buffer.release(); + return response; + } +} diff --git a/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/providers/DiscordApi.java b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/providers/DiscordApi.java new file mode 100644 index 00000000..288d9a0c --- /dev/null +++ b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/providers/DiscordApi.java @@ -0,0 +1,165 @@ +package pro.gravit.launchermodules.discordauthsystem.providers; + +import com.google.gson.reflect.TypeToken; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jsoup.Connection; +import org.jsoup.Jsoup; +import pro.gravit.launcher.Launcher; +import pro.gravit.launchermodules.discordauthsystem.Config; + +import java.io.IOException; +import java.util.List; + +public class DiscordApi { + private static final String GRANT_TYPE_AUTHORIZATION = "authorization_code"; + private static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; + private static Config config; + private static Logger logger; + + public static void initialize(Config config) { + DiscordApi.config = config; + DiscordApi.logger = LogManager.getLogger(); + } + + public static DiscordAccessTokenResponse sendRefreshToken(String refreshToken) throws IOException { + Connection request = Jsoup.connect(config.discordApiEndpointVersion + "/oauth2/token") + .data("client_id", config.clientId) + .data("client_secret", config.clientSecret) + .data("grant_type", GRANT_TYPE_REFRESH_TOKEN) + .data("refresh_token", refreshToken) + .ignoreContentType(true); + + + return Launcher.gsonManager.gson.fromJson( + request.post().body().text(), + DiscordAccessTokenResponse.class + ); + } + + public static DiscordAccessTokenResponse getAccessTokenByCode(String code) throws IOException { + Connection request = Jsoup.connect(config.discordApiEndpointVersion + "/oauth2/token") + .data("client_id", config.clientId) + .data("client_secret", config.clientSecret) + .data("grant_type", GRANT_TYPE_AUTHORIZATION) + .data("code", code) + .data("redirect_uri", config.redirectUrl) + .data("scope", "") + .ignoreContentType(true); + + + return Launcher.gsonManager.gson.fromJson( + request.post().body().text(), + DiscordAccessTokenResponse.class + ); + } + + public static OauthMeResponse getDiscordUserByAccessToken(String accessToken) throws IOException { + + org.jsoup.Connection request = Jsoup.connect(config.discordApiEndpoint + "/oauth2/@me") + .header("Authorization", "Bearer " + accessToken) + .ignoreContentType(true); + + return Launcher.gsonManager.gson.fromJson( + request.get().body().text(), + OauthMeResponse.class + ); + } + + public static List getUserGuilds(String accessToken) throws IOException { + org.jsoup.Connection request = Jsoup.connect(config.discordApiEndpoint + "/users/@me/guilds") + .header("Authorization", "Bearer " + accessToken) + .ignoreContentType(true); + + return Launcher.gsonManager.gson.fromJson( + request.get().body().text(), + new TypeToken>(){}.getType() + ); + } + + public static MemberGuildResponse getUserGuildMember(String accessToken, String guildId) throws IOException { + org.jsoup.Connection request = Jsoup.connect(config.discordApiEndpoint + "/users/@me/guilds/" + guildId + "/member") + .header("Authorization", "Bearer " + accessToken) + .ignoreContentType(true); + + return Launcher.gsonManager.gson.fromJson( + request.get().body().text(), + MemberGuildResponse.class + ); + } + + public static class UserGuildResponse { + public String id; + + public String name; + + public UserGuildResponse (String id, String name) { + this.id = id; + this.name = name; + } + } + + public static class MemberGuildResponse { + public String nick; + + public MemberGuildResponse (String nick) { + this.nick = nick; + } + } + + public static class DiscordUserResponse { + public String id; + public String username; + public String discriminator; + public String avatar; + public String verified; + public String email; + public Integer flags; + public String banner; + public Integer accent_color; + public Integer premium_type; + public Integer public_flags; + + public DiscordUserResponse(String id, String username, String discriminator, String avatar, String verified, String email, Integer flags, String banner, Integer accent_color, Integer premium_type, Integer public_flags) { + this.id = id; + this.username = username; + this.discriminator = discriminator; + this.avatar = avatar; + this.verified = verified; + this.email = email; + this.flags = flags; + this.banner = banner; + this.accent_color = accent_color; + this.premium_type = premium_type; + this.public_flags = public_flags; + } + } + + public static class OauthMeResponse { + public String[] scopes; + public String expires; + public DiscordUserResponse user; + + public OauthMeResponse(String[] scopes, String expires, DiscordUserResponse user) { + this.scopes = scopes; + this.expires = expires; + this.user = user; + } + } + + public static class DiscordAccessTokenResponse { + public String access_token; + public String token_type; + public long expires_in; + public String refresh_token; + public String scope; + + public DiscordAccessTokenResponse(String access_token, String token_type, long expires_in, String refresh_token, String scope) { + this.access_token = access_token; + this.token_type = token_type; + this.expires_in = expires_in; + this.refresh_token = refresh_token; + this.scope = scope; + } + } +} diff --git a/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/providers/DiscordSystemAuthCoreProvider.java b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/providers/DiscordSystemAuthCoreProvider.java new file mode 100644 index 00000000..bdd1feb5 --- /dev/null +++ b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/providers/DiscordSystemAuthCoreProvider.java @@ -0,0 +1,783 @@ +package pro.gravit.launchermodules.discordauthsystem.providers; + +import com.github.slugify.Slugify; +import io.netty.handler.codec.http.HttpResponseStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import pro.gravit.launcher.ClientPermissions; +import pro.gravit.launcher.events.request.GetAvailabilityAuthRequestEvent; +import pro.gravit.launcher.request.auth.AuthRequest; +import pro.gravit.launcher.request.auth.details.AuthWebViewDetails; +import pro.gravit.launcher.request.auth.password.AuthCodePassword; +import pro.gravit.launcher.request.secure.HardwareReportRequest; +import pro.gravit.launchermodules.discordauthsystem.ModuleImpl; +import pro.gravit.launchserver.LaunchServer; +import pro.gravit.launchserver.auth.AuthException; +import pro.gravit.launchserver.auth.AuthProviderPair; +import pro.gravit.launchserver.auth.MySQLSourceConfig; +import pro.gravit.launchserver.auth.core.AuthCoreProvider; +import pro.gravit.launchserver.auth.core.User; +import pro.gravit.launchserver.auth.core.UserSession; +import pro.gravit.launchserver.auth.core.interfaces.UserHardware; +import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportExit; +import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportHardware; +import pro.gravit.launchserver.manangers.AuthManager; +import pro.gravit.launchserver.socket.Client; +import pro.gravit.launchserver.socket.response.auth.AuthResponse; +import pro.gravit.utils.helper.IOHelper; +import pro.gravit.utils.helper.SecurityHelper; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.sql.*; +import java.util.Date; +import java.util.*; + +public class DiscordSystemAuthCoreProvider extends AuthCoreProvider implements AuthSupportExit, AuthSupportHardware { + private final transient Logger logger = LogManager.getLogger(); + private final transient Slugify slg = Slugify.builder().underscoreSeparator(true).lowerCase(false).transliterator(true).build(); + public MySQLSourceConfig mySQLHolder; + public double criticalCompareLevel = 1.0; + public String uuidColumn; + public String usernameColumn; + public String accessTokenColumn; + public String refreshTokenColumn; + public String expiresInColumn; + public String discordIdColumn; + public String bannedAtColumn; + public String serverIDColumn; + public String hardwareIdColumn; + public String table; + public String tableHWID = "hwids"; + private transient ModuleImpl module; + // hwid sql + private transient String sqlFindHardwareByPublicKey; + private transient String sqlFindHardwareByData; + private transient String sqlFindHardwareById; + private transient String sqlCreateHardware; + private transient String sqlUpdateHardwarePublicKey; + private transient String sqlUpdateHardwareBanned; + private transient String sqlUpdateUser; + private transient String sqlUsersByHwidId; + + // Prepared SQL queries + private transient String queryByUUIDSQL; + private transient String queryByUsernameSQL; + private transient String queryByAccessTokenSQL; + private transient String queryByDiscordIdSQL; + private transient String insertNewUserSQL; + private transient String updateServerIDSQL; + + @Override + public void init(LaunchServer server) { + module = server.modulesManager.getModule(ModuleImpl.class); + if (mySQLHolder == null) logger.error("mySQLHolder cannot be null"); + if (uuidColumn == null) logger.error("uuidColumn cannot be null"); + if (usernameColumn == null) logger.error("usernameColumn cannot be null"); + if (accessTokenColumn == null) logger.error("accessTokenColumn cannot be null"); + if (refreshTokenColumn == null) logger.error("refreshTokenColumn cannot be null"); + if (expiresInColumn == null) logger.error("expiresInColumn cannot be null"); + if (discordIdColumn == null) logger.error("discordIdColumn cannot be null"); + if (bannedAtColumn == null) logger.error("bannedAtColumn cannot be null"); + if (serverIDColumn == null) logger.error("serverIDColumn cannot be null"); + if (hardwareIdColumn == null) logger.error("hardwareIdColumn cannot be null"); + if (table == null) logger.error("table cannot be null"); + + String userInfoCols = String.format("%s, %s, %s, %s, %s, %s, %s, %s, %s", uuidColumn, usernameColumn, accessTokenColumn, refreshTokenColumn, expiresInColumn, discordIdColumn, bannedAtColumn, serverIDColumn, hardwareIdColumn); + + queryByUsernameSQL = String.format("SELECT %s FROM %s WHERE %s=? LIMIT 1", + userInfoCols, table, usernameColumn); + + queryByUUIDSQL = String.format("SELECT %s FROM %s WHERE %s=? LIMIT 1", + userInfoCols, table, uuidColumn); + + queryByAccessTokenSQL = String.format("SELECT %s FROM %s WHERE %s=? LIMIT 1", + userInfoCols, table, accessTokenColumn); + + queryByDiscordIdSQL = String.format("SELECT %s FROM %s WHERE %s=? LIMIT 1", + userInfoCols, table, discordIdColumn); + + insertNewUserSQL = String.format("INSERT INTO %s (%s) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", table, userInfoCols); + updateServerIDSQL = String.format("UPDATE %s SET %s=? WHERE %s=?", table, serverIDColumn, discordIdColumn); + + String hardwareInfoCols = "id, hwDiskId, baseboardSerialNumber, displayId, bitness, totalMemory, logicalProcessors, physicalProcessors, processorMaxFreq, battery, id, graphicCard, banned, publicKey"; + if (sqlFindHardwareByPublicKey == null) + sqlFindHardwareByPublicKey = String.format("SELECT %s FROM %s WHERE `publicKey` = ?", hardwareInfoCols, tableHWID); + if (sqlFindHardwareById == null) + sqlFindHardwareById = String.format("SELECT %s FROM %s WHERE `id` = ?", hardwareInfoCols, tableHWID); + if (sqlUsersByHwidId == null) + sqlUsersByHwidId = String.format("SELECT %s FROM %s WHERE `%s` = ?", userInfoCols, table, hardwareIdColumn); + if (sqlFindHardwareByData == null) + sqlFindHardwareByData = String.format("SELECT %s FROM %s", hardwareInfoCols, tableHWID); + if (sqlCreateHardware == null) + sqlCreateHardware = String.format("INSERT INTO `%s` (`publickey`, `hwDiskId`, `baseboardSerialNumber`, `displayId`, `bitness`, `totalMemory`, `logicalProcessors`, `physicalProcessors`, `processorMaxFreq`, `graphicCard`, `battery`, `banned`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '0')", tableHWID); + if (sqlUpdateHardwarePublicKey == null) + sqlUpdateHardwarePublicKey = String.format("UPDATE %s SET `publicKey` = ? WHERE `id` = ?", tableHWID); + + sqlUpdateHardwareBanned = String.format("UPDATE %s SET `banned` = ? WHERE `id` = ?", tableHWID); + sqlUpdateUser = String.format("UPDATE %s SET `%s` = ? WHERE `%s` = ?", table, hardwareIdColumn, discordIdColumn); + } + + @Override + public User getUserByUsername(String username) { + return getDiscordUserByUsername(username); + } + + public DiscordUser getDiscordUserByUsername(String username) { + try { + return query(queryByUsernameSQL, username); + } catch (IOException e) { + logger.error("SQL error", e); + return null; + } + } + + public DiscordUser updateDataUser(String discordId, String accessToken) { + try (Connection connection = mySQLHolder.getConnection()) { + return updateDataUser(connection, discordId, accessToken); + } catch (SQLException e) { + logger.error("updateDataUser SQL error", e); + return null; + } + } + + public DiscordUser updateDataUser(String discordId, String accessToken, String refreshToken, Long expiresIn) { + try (Connection connection = mySQLHolder.getConnection()) { + return updateDataUser(connection, discordId, accessToken, refreshToken, expiresIn); + } catch (SQLException e) { + logger.error("updateDataUser SQL error", e); + return null; + } + } + + private DiscordUser updateDataUser(Connection connection, String discordId, String accessToken) throws SQLException { + String sql = String.format("UPDATE %s SET %s=? WHERE %s=?", table, accessTokenColumn, discordIdColumn); + PreparedStatement s = connection.prepareStatement(sql); + s.setString(1, accessToken); + s.setString(2, discordId); + s.executeUpdate(); + + return getUserByDiscordId(discordId); + } + + private DiscordUser updateDataUser(Connection connection, String discordId, String accessToken, String refreshToken, Long expiresIn) throws SQLException { + + String sql = String.format("UPDATE %s SET %s=?, %s=?, %s=? WHERE %s=?", table, accessTokenColumn, refreshTokenColumn, expiresInColumn, discordIdColumn); + PreparedStatement s = connection.prepareStatement(sql); + s.setString(1, accessToken); + s.setString(2, refreshToken); + s.setLong(3, expiresIn); + s.setString(4, discordId); + s.executeUpdate(); + + return getUserByDiscordId(discordId); + } + + @Override + public User getUserByLogin(String login) { + return getUserByUsername(login); + } + + @Override + public User getUserByUUID(UUID uuid) { + try { + return query(queryByUUIDSQL, uuid.toString()); + } catch (IOException e) { + logger.error("getUserByUUID SQL error", e); + return null; + } + } + + @Override + public User checkServer(Client client, String username, String serverID) throws IOException { + User user = getUserByUsername(username); + if (user == null) { + return null; + } + String usernameUser = user.getUsername(); + String serverId = user.getServerId(); + if (usernameUser != null && usernameUser.equals(username) && serverId != null && serverId.equals(serverID)) { + return user; + } + return null; + } + + @Override + public boolean joinServer(Client client, String username, String accessToken, String serverID) throws IOException { + User user = client.getUser(); + if (user == null) return false; + String usernameUser = user.getUsername(); + String userAccessToken = user.getAccessToken(); + return usernameUser != null && usernameUser.equals(username) && userAccessToken != null && userAccessToken.equals(accessToken) && updateServerID(user, serverID); + } + + public DiscordUser getUserByAccessToken(String accessToken) { + try { + return query(queryByAccessTokenSQL, accessToken); + } catch (IOException e) { + logger.error("getUserByAccessToken SQL error", e); + return null; + } + } + + public DiscordUser getUserByDiscordId(String discordId) { + try { + return query(queryByDiscordIdSQL, discordId); + } catch (IOException e) { + logger.error("getUserByDiscordId SQL error", e); + return null; + } + } + + public DiscordUser createUser(String uuid, String username, String accessToken, String refreshToken, Long expiresIn, String discordId) { + try (Connection connection = mySQLHolder.getConnection()) { + return createUser(connection, uuid, username, accessToken, refreshToken, expiresIn, discordId); + } catch (SQLException e) { + logger.error("createUser SQL error", e); + return null; + } + } + + private DiscordUser createUser(Connection connection, String uuid, String username, String accessToken, String refreshToken, Long expiresIn, String discordId) throws SQLException { + PreparedStatement s = connection.prepareStatement(insertNewUserSQL); + s.setString(1, uuid); + s.setString(2, username); + s.setString(3, accessToken); + s.setString(4, refreshToken); + s.setLong(5, expiresIn); + s.setString(6, discordId); + s.setDate(7, null); + s.setString(8, null); + s.setString(9, null); + s.executeUpdate(); + return getUserByAccessToken(accessToken); + } + + @Override + public UserSession getUserSessionByOAuthAccessToken(String accessToken) throws OAuthAccessTokenExpired { + DiscordUser user = getUserByAccessToken(accessToken); + if (user == null) return null; + return new DiscordUserSession(user, accessToken); + } + + @Override + public AuthManager.AuthReport refreshAccessToken(String refreshToken, AuthResponse.AuthContext context) { + try { + var response = DiscordApi.sendRefreshToken(refreshToken); + if (response == null) { + return null; + } + DiscordUser user = getUserByAccessToken(response.access_token); + if (user != null) { + updateDataUser(user.getDiscordId(), response.access_token, response.refresh_token, response.expires_in * 1000); + } + return AuthManager.AuthReport.ofOAuth(response.access_token, response.refresh_token, response.expires_in * 1000, null); + } catch (IOException e) { + logger.error("DiscordAuth refresh failed", e); + return null; + } + } + + @Override + public AuthManager.AuthReport authorize(String login, AuthResponse.AuthContext context, AuthRequest.AuthPasswordInterface password, boolean minecraftAccess) throws AuthException { + + DiscordApi.DiscordAccessTokenResponse accessTokenResponse; + + AuthCodePassword codePassword = (AuthCodePassword) password; + var code = codePassword.code; + + try { + accessTokenResponse = DiscordApi.getAccessTokenByCode(code); + } catch (Exception e) { + throw new AuthException("Discord authorization denied your code."); + } + + DiscordApi.OauthMeResponse response; + + try{ + response = DiscordApi.getDiscordUserByAccessToken(accessTokenResponse.access_token); + } catch (IOException e) { + throw new AuthException("Discord authorization denied your access_token."); + } + + if (!module.config.guildIdsJoined.isEmpty()) { + + List guilds; + + try { + guilds = DiscordApi.getUserGuilds(accessTokenResponse.access_token); + } catch (IOException e) { + throw new AuthException("Error getting user guilds."); + } + + + var needGuilds = module.config.guildIdsJoined; + + for (var guild : guilds) { + needGuilds.removeIf(g -> Objects.equals(g.id, guild.id)); + } + + if (!needGuilds.isEmpty()) { + String body = "To enter the server you must be a member of these guilds: "; + List guildData = new ArrayList<>(); + for (var g : needGuilds) { + guildData.add("" + g.name + ""); + } + throw new AuthException(body + String.join(", ", guildData)); + } + } + + DiscordSystemAuthCoreProvider.DiscordUser user = getUserByDiscordId(response.user.id); + + if (user == null) { + String username = response.user.username; + if (module.config.guildIdGetNick.length() > 0) { + try { + var member = DiscordApi.getUserGuildMember(accessTokenResponse.access_token, module.config.guildIdGetNick); + if (member.nick != null) { + username = member.nick; + } + } catch (IOException e) { + throw new AuthException("An unexpected error occurred!"); + } + } + + username = slg.slugify(username); + + var usernameLength = username.length(); + + if (usernameLength == 0) { + throw new AuthException("Your nickname does not meet the requirements. Please change it."); + } + + if (module.config.usernameRegex.length() > 0) { + if (!username.matches(module.config.usernameRegex)) { + throw new AuthException("Your nickname does not meet the requirements. Please change it."); + } + } + + if (getUserByUsername(username) != null) { + username = username.substring(0, usernameLength-1-response.user.discriminator.length()); + username += "_" + response.user.discriminator; + } + + user = createUser( + UUID.randomUUID().toString(), + username, + accessTokenResponse.access_token, + accessTokenResponse.refresh_token, + accessTokenResponse.expires_in * 1000, + response.user.id + ); + } else { + user = updateDataUser(response.user.id, accessTokenResponse.access_token, accessTokenResponse.refresh_token, accessTokenResponse.expires_in * 1000); + } + + if (user.isBanned()) { + throw new AuthException("You have been banned!"); + } + + DiscordUserSession session = new DiscordUserSession(user, user.accessToken); + return AuthManager.AuthReport.ofOAuth(user.accessToken, user.refreshToken, user.expiresIn * 1000, session); + } + + @Override + protected boolean updateServerID(User user, String serverID) throws IOException { + try (Connection c = mySQLHolder.getConnection()) { + DiscordUser discordUser = (DiscordUser) user; + discordUser.serverId = serverID; + PreparedStatement s = c.prepareStatement(updateServerIDSQL); + s.setString(1, serverID); + s.setString(2, discordUser.getDiscordId()); + s.setQueryTimeout(MySQLSourceConfig.TIMEOUT); + return s.executeUpdate() > 0; + } catch (SQLException e) { + throw new IOException(e); + } + } + + @Override + public void close() throws IOException { + + } + + @Override + public boolean deleteSession(UserSession session) { + return exitUser(session.getUser()); + } + + @Override + public boolean exitUser(User user) { + DiscordUser discordUser = getUserByAccessToken(user.getAccessToken()); + if (discordUser == null) { + return true; + } + return updateDataUser(discordUser.getDiscordId(), null) != null; + } + + private void setUserHardwareId(Connection connection, String discordId, long hwidId) throws SQLException { + PreparedStatement s = connection.prepareStatement(sqlUpdateUser); + s.setLong(1, hwidId); + s.setString(2, discordId); + s.executeUpdate(); + } + + private DiscordUser query(String sql, String value) throws IOException { + try (Connection c = mySQLHolder.getConnection()) { + PreparedStatement s = c.prepareStatement(sql); + s.setString(1, value); + s.setQueryTimeout(MySQLSourceConfig.TIMEOUT); + try (ResultSet set = s.executeQuery()) { + return constructUser(set); + } + } catch (SQLException e) { + throw new IOException(e); + } + } + + private DiscordUser constructUser(ResultSet set) throws SQLException { + return set.next() ? + new DiscordUser( + set.getString(usernameColumn), + UUID.fromString(set.getString(uuidColumn)), + set.getString(accessTokenColumn), + set.getString(refreshTokenColumn), + set.getLong(expiresInColumn), + set.getString(discordIdColumn), + set.getDate(bannedAtColumn), + set.getString(serverIDColumn), + set.getLong(hardwareIdColumn) + ) + : null; + } + + @Override + public List getDetails(Client client) { + String state = UUID.randomUUID().toString(); + client.setProperty("state", state); + String responseType = "code"; + String[] scope = new String[]{"identify", "guilds", "guilds.members.read", "email"}; + String url = String.format("%s?response_type=%s&client_id=%s&scope=%s&state=%s&redirect_uri=%s&prompt=consent", module.config.discordAuthorizeUrl, responseType, module.config.clientId, String.join("%20", scope), state, module.config.redirectUrl); + return List.of(new AuthWebViewDetails(url, module.config.redirectUrl, true, true)); + } + + private DiscordUserHardware fetchHardwareInfo(ResultSet set) throws SQLException, IOException { + HardwareReportRequest.HardwareInfo hardwareInfo = new HardwareReportRequest.HardwareInfo(); + hardwareInfo.hwDiskId = set.getString("hwDiskId"); + hardwareInfo.baseboardSerialNumber = set.getString("baseboardSerialNumber"); + Blob displayId = set.getBlob("displayId"); + hardwareInfo.displayId = displayId == null ? null : IOHelper.read(displayId.getBinaryStream()); + hardwareInfo.bitness = set.getInt("bitness"); + hardwareInfo.totalMemory = set.getLong("totalMemory"); + hardwareInfo.logicalProcessors = set.getInt("logicalProcessors"); + hardwareInfo.physicalProcessors = set.getInt("physicalProcessors"); + hardwareInfo.processorMaxFreq = set.getLong("processorMaxFreq"); + hardwareInfo.battery = set.getBoolean("battery"); + hardwareInfo.graphicCard = set.getString("graphicCard"); + Blob publicKey = set.getBlob("publicKey"); + long id = set.getLong("id"); + boolean banned = set.getBoolean("banned"); + return new DiscordUserHardware(hardwareInfo, publicKey == null ? null : IOHelper.read(publicKey.getBinaryStream()), id, banned); + } + + @Override + public UserHardware getHardwareInfoByPublicKey(byte[] publicKey) { + try (Connection connection = mySQLHolder.getConnection()) { + PreparedStatement s = connection.prepareStatement(sqlFindHardwareByPublicKey); + s.setBlob(1, new ByteArrayInputStream(publicKey)); + try (ResultSet set = s.executeQuery()) { + if (set.next()) { + return fetchHardwareInfo(set); + } else { + return null; + } + } + } catch (SQLException | IOException e) { + logger.error("SQL Error", e); + return null; + } + } + + @Override + public UserHardware getHardwareInfoByData(HardwareReportRequest.HardwareInfo info) { + try (Connection connection = mySQLHolder.getConnection()) { + PreparedStatement s = connection.prepareStatement(sqlFindHardwareByData); + try (ResultSet set = s.executeQuery()) { + while (set.next()) { + DiscordUserHardware hw = fetchHardwareInfo(set); + HardwareInfoCompareResult result = compareHardwareInfo(hw.getHardwareInfo(), info); + if (result.compareLevel > criticalCompareLevel) { + return hw; + } + } + } + } catch (SQLException | IOException e) { + logger.error("SQL Error", e); + } + return null; + } + + @Override + public UserHardware getHardwareInfoById(String id) { + try (Connection connection = mySQLHolder.getConnection()) { + PreparedStatement s = connection.prepareStatement(sqlFindHardwareById); + s.setLong(1, Long.parseLong(id)); + try (ResultSet set = s.executeQuery()) { + if (set.next()) { + return fetchHardwareInfo(set); + } else { + return null; + } + } + } catch (SQLException | IOException e) { + logger.error("SQL Error", e); + return null; + } + } + + @Override + public UserHardware createHardwareInfo(HardwareReportRequest.HardwareInfo hardwareInfo, byte[] publicKey) { + try (Connection connection = mySQLHolder.getConnection()) { + PreparedStatement s = connection.prepareStatement(sqlCreateHardware, Statement.RETURN_GENERATED_KEYS); + s.setBlob(1, new ByteArrayInputStream(publicKey)); + s.setString(2, hardwareInfo.hwDiskId); + s.setString(3, hardwareInfo.baseboardSerialNumber); + s.setBlob(4, hardwareInfo.displayId == null ? null : new ByteArrayInputStream(hardwareInfo.displayId)); + s.setInt(5, hardwareInfo.bitness); + s.setLong(6, hardwareInfo.totalMemory); + s.setInt(7, hardwareInfo.logicalProcessors); + s.setInt(8, hardwareInfo.physicalProcessors); + s.setLong(9, hardwareInfo.processorMaxFreq); + s.setString(10, hardwareInfo.graphicCard); + s.setBoolean(11, hardwareInfo.battery); + s.executeUpdate(); + try (ResultSet generatedKeys = s.getGeneratedKeys()) { + if (generatedKeys.next()) { + //writeHwidLog(connection, generatedKeys.getLong(1), publicKey); + long id = generatedKeys.getLong(1); + return new DiscordUserHardware(hardwareInfo, publicKey, id, false); + } + } + return null; + } catch (SQLException e) { + logger.error("SQL Error", e); + return null; + } + } + + @Override + public void connectUserAndHardware(UserSession userSession, UserHardware hardware) { + DiscordUserSession discordUserSession = (DiscordUserSession) userSession; + DiscordUser discordUser = discordUserSession.user; + DiscordUserHardware discordUserHardware = (DiscordUserHardware) hardware; + if (discordUser.hwidId == discordUserHardware.id) return; + discordUser.hwidId = discordUserHardware.id; + try (Connection connection = mySQLHolder.getConnection()) { + setUserHardwareId(connection, discordUser.getDiscordId(), discordUserHardware.id); + } catch (SQLException e) { + logger.error("SQL Error", e); + } + } + + @Override + public void addPublicKeyToHardwareInfo(UserHardware hardware, byte[] publicKey) { + DiscordUserHardware discordUserHardware = (DiscordUserHardware) hardware; + discordUserHardware.publicKey = publicKey; + try (Connection connection = mySQLHolder.getConnection()) { + PreparedStatement s = connection.prepareStatement(sqlUpdateHardwarePublicKey); + s.setBlob(1, new ByteArrayInputStream(publicKey)); + s.setLong(2, discordUserHardware.id); + s.executeUpdate(); + } catch (SQLException e) { + logger.error("SQL error", e); + } + } + + @Override + public Iterable getUsersByHardwareInfo(UserHardware hardware) { + List users = new LinkedList<>(); + try (Connection c = mySQLHolder.getConnection()) { + PreparedStatement s = c.prepareStatement(sqlUsersByHwidId); + s.setLong(1, Long.parseLong(hardware.getId())); + s.setQueryTimeout(MySQLSourceConfig.TIMEOUT); + try (ResultSet set = s.executeQuery()) { + while (!set.isLast()) { + users.add(constructUser(set)); + } + } + } catch (SQLException e) { + logger.error("SQL error", e); + return null; + } + return users; + } + + @Override + public void banHardware(UserHardware hardware) { + DiscordUserHardware discordUserHardware = (DiscordUserHardware) hardware; + discordUserHardware.banned = true; + try (Connection connection = mySQLHolder.getConnection()) { + PreparedStatement s = connection.prepareStatement(sqlUpdateHardwareBanned); + s.setBoolean(1, true); + s.setLong(2, discordUserHardware.id); + s.executeUpdate(); + } catch (SQLException e) { + logger.error("SQL Error", e); + } + } + + @Override + public void unbanHardware(UserHardware hardware) { + DiscordUserHardware discordUserHardware = (DiscordUserHardware) hardware; + discordUserHardware.banned = false; + try (Connection connection = mySQLHolder.getConnection()) { + PreparedStatement s = connection.prepareStatement(sqlUpdateHardwareBanned); + s.setBoolean(1, false); + s.setLong(2, discordUserHardware.id); + s.executeUpdate(); + } catch (SQLException e) { + logger.error("SQL error", e); + } + } + + public static class DiscordUser implements User { + public String username; + public String discordId; + public UUID uuid; + public ClientPermissions permissions; + public String serverId; + public String accessToken; + public String refreshToken; + public Long expiresIn; + public Date bannedAt; + protected Long hwidId; + + public DiscordUser(String username, UUID uuid, String accessToken, String refreshToken, Long expiresIn, String discordId, Date bannedAt, String serverId, Long hwidId) { + this.username = username; + this.uuid = uuid; + this.discordId = discordId; + this.accessToken = accessToken; + this.expiresIn = expiresIn; + this.bannedAt = bannedAt; + this.refreshToken = refreshToken; + this.permissions = new ClientPermissions(); + this.serverId = serverId; + this.hwidId = hwidId; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public UUID getUUID() { + return uuid; + } + + @Override + public String getServerId() { + return serverId; + } + + @Override + public String getAccessToken() { + return accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public Long getExpiresIn() { + return expiresIn; + } + + public String getDiscordId() { + return discordId; + } + + @Override + public ClientPermissions getPermissions() { + return permissions; + } + + @Override + public boolean isBanned() { + return this.bannedAt != null; + } + } + + public static class DiscordUserSession implements UserSession { + private final String id; + public transient DiscordUser user; + public String accessToken; + public long expireMillis; + + public DiscordUserSession(DiscordUser user, String accessToken) { + this.id = SecurityHelper.randomStringToken(); + this.user = user; + this.accessToken = accessToken; + } + + @Override + public String getID() { + return id; + } + + @Override + public User getUser() { + return user; + } + + @Override + public long getExpireIn() { + return expireMillis; + } + } + + public static class DiscordUserHardware implements UserHardware { + + private final HardwareReportRequest.HardwareInfo hardwareInfo; + private final long id; + private byte[] publicKey; + private boolean banned; + + public DiscordUserHardware(HardwareReportRequest.HardwareInfo hardwareInfo, byte[] publicKey, long id, boolean banned) { + this.hardwareInfo = hardwareInfo; + this.publicKey = publicKey; + this.id = id; + this.banned = banned; + } + + @Override + public HardwareReportRequest.HardwareInfo getHardwareInfo() { + return hardwareInfo; + } + + @Override + public byte[] getPublicKey() { + return publicKey; + } + + @Override + public String getId() { + return String.valueOf(id); + } + + @Override + public boolean isBanned() { + return banned; + } + + @Override + public String toString() { + return "DiscordUserHardware{" + + "hardwareInfo=" + hardwareInfo + + ", publicKey=" + (publicKey == null ? null : new String(Base64.getEncoder().encode(publicKey))) + + ", id=" + id + + ", banned=" + banned + + '}'; + } + } +} \ No newline at end of file diff --git a/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/responses/ExitResponse.java b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/responses/ExitResponse.java new file mode 100644 index 00000000..1bf81899 --- /dev/null +++ b/DiscordAuthSystem_module/src/main/java/pro/gravit/launchermodules/discordauthsystem/responses/ExitResponse.java @@ -0,0 +1,36 @@ +package pro.gravit.launchermodules.discordauthsystem.responses; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import pro.gravit.launcher.events.RequestEvent; +import pro.gravit.launcher.events.request.ExitRequestEvent; +import pro.gravit.launchserver.LaunchServer; +import pro.gravit.launchserver.socket.Client; +import pro.gravit.launchserver.socket.handlers.WebSocketFrameHandler; + +public class ExitResponse extends pro.gravit.launchserver.socket.response.auth.ExitResponse { + + public static void exit(LaunchServer server, WebSocketFrameHandler wsHandler, Channel channel, ExitRequestEvent.ExitReason reason) { + Client chClient = wsHandler.getClient(); + Client newCusClient = new Client(); + newCusClient.setProperty("state", chClient.getProperty("state")); + newCusClient.checkSign = chClient.checkSign; + wsHandler.setClient(newCusClient); + ExitRequestEvent event = new ExitRequestEvent(reason); + event.requestUUID = RequestEvent.eventUUID; + wsHandler.service.sendObject(channel, event); + } + + @Override + public void execute(ChannelHandlerContext ctx, Client client) { + var state = client.getProperty("state"); + super.execute(ctx, client); + if (username == null) { + WebSocketFrameHandler handler = ctx.pipeline().get(WebSocketFrameHandler.class); + handler.getClient().setProperty("state", state); + } + } + +}