├── .classpath ├── .dockerignore ├── .gitignore ├── .project ├── .settings ├── org.eclipse.core.resources.prefs ├── org.eclipse.jdt.core.prefs └── org.eclipse.m2e.core.prefs ├── README.md ├── docker ├── Dockerfile ├── docker-compose.yml ├── frontend │ ├── .htaccess │ ├── config.json │ └── httpd.conf └── launcher_config.json ├── pom.xml ├── resources └── .gitkeep └── src └── me └── mrletsplay └── shittyauth ├── ShittyAuth.java ├── auth ├── AccessToken.java ├── AccessTokenStorage.java ├── FileAccessTokenStorage.java ├── SQLAccessTokenStorage.java └── StoredAccessToken.java ├── config └── ShittyAuthSettings.java ├── page ├── AccountPage.java ├── AdminPage.java ├── CreateAccountPage.java ├── SettingsPage.java └── api │ ├── UserCapeDocument.java │ ├── UserSkinDocument.java │ ├── authlibinjector │ ├── AuthLibInjectorDocument.java │ └── AuthLibInjectorMetadataDocument.java │ ├── legacy │ ├── LegacyCheckServerDocument.java │ ├── LegacyJoinServerDocument.java │ ├── LegacyUserCapeDocument.java │ └── LegacyUserSkinDocument.java │ ├── services │ ├── PlayerAttributesDocument.java │ ├── PlayerCertificatesDocument.java │ └── PlayerReportDocument.java │ ├── shittyauth │ ├── ShittyAuthAPI.java │ └── ShittyAuthAdminAPI.java │ └── yggdrasil │ ├── AuthenticatePage.java │ ├── HasJoinedPage.java │ ├── InvalidatePage.java │ ├── JoinPage.java │ ├── ProfilePage.java │ ├── PublicKeysPage.java │ ├── RefreshPage.java │ ├── SignoutPage.java │ └── ValidatePage.java ├── textures ├── SkinType.java └── TexturesHelper.java ├── user ├── FileUserDataStorage.java ├── SQLUserDataStorage.java ├── UserData.java └── UserDataStorage.java ├── util ├── CryptoHelper.java ├── DefaultTexture.java ├── InvalidSkinException.java ├── InvalidUsernameException.java └── UUIDHelper.java └── webinterface └── ShittyAuthWIHandler.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /target/ 3 | dependency-reduced-pom.xml 4 | /run/ 5 | /cfg/ 6 | /data/ 7 | /include/ 8 | /logs/ 9 | /shittyauth/ 10 | 11 | /docker/frontend 12 | /docker/docker-compose.yml 13 | /docker/Dockerfile 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /target/ 3 | dependency-reduced-pom.xml 4 | /run/ 5 | /cfg/ 6 | /data/ 7 | /include/ 8 | /logs/ 9 | /shittyauth/ 10 | /docker/shittyauth/ 11 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | ShittyAuthServer 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.m2e.core.maven2Builder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.m2e.core.maven2Nature 21 | org.eclipse.jdt.core.javanature 22 | 23 | 24 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding/src=UTF-8 3 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=11 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled 11 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 12 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 13 | org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning 14 | org.eclipse.jdt.core.compiler.release=enabled 15 | org.eclipse.jdt.core.compiler.source=11 16 | -------------------------------------------------------------------------------- /.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShittyAuthServer 2 | A shitty implementation of the Yggdrasil authentication scheme 3 | 4 | This project implements Mojang's Yggdrasil authentication scheme (which was used before the transition to Microsoft accounts). 5 | 6 | It is intended to be used in conjunction with the [ShittyAuthLauncher](https://github.com/MrLetsplay2003/ShittyAuthLauncher) 7 | 8 | # Compiling the server 9 | The server uses Maven for building. 10 | 11 | To compile the server, use 12 | ``` 13 | $ mvn package 14 | ``` 15 | which will generate a `ShittyAuthServer-VERSION.jar` in the `target` folder 16 | 17 | # Running the server 18 | First, compile the server or download a prebuilt JAR file from [here](https://ci.graphite-official.com/job/ShittyAuthServer/), then, run it using any Java 11+ VM. 19 | 20 | Afterwards, navigate to `http://your.server.ip:8880` in your web browser. You will be prompted to set up the WebinterfaceAPI server the authentication server uses. 21 | 22 | Once you're done, your players will be able to create accounts (using password authentication) and use their credentials to login in the launcher (after having changing the auth server URL to the correct domain/IP). 23 | 24 | Optional: If you have a setup using an HTTP(S) proxy server (e.g. Apache), make sure to change the Skin base URL in the `Minecraft > Settings` tab to your public domain. 25 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:latest as builder 2 | 3 | COPY . /workspace 4 | 5 | WORKDIR /workspace 6 | 7 | RUN mvn clean package 8 | 9 | 10 | 11 | FROM alpine:latest 12 | 13 | ENV UID=1000 14 | ENV GID=1000 15 | 16 | RUN apk update && apk add openjdk11 shadow sudo 17 | 18 | COPY --from=mrletsplay/docker_launcher /usr/local/bin/docker_launcher /usr/local/bin/docker_launcher 19 | COPY ./docker/launcher_config.json /shittyauth/launcher_config.json 20 | COPY --from=builder /workspace/target/ShittyAuthServer-*.jar /shittyauth/ShittyAuthServer.jar 21 | 22 | RUN useradd shittyauth 23 | 24 | RUN mkdir /shittyauth/data && chown -R shittyauth /shittyauth 25 | 26 | VOLUME ["/shittyauth/data"] 27 | 28 | WORKDIR /shittyauth/data 29 | 30 | EXPOSE 8880 31 | 32 | ENTRYPOINT [ "docker_launcher", "--config", "/shittyauth/launcher_config.json", "sudo", "-E", "-u", "shittyauth" ] 33 | CMD [ "java", "-jar", "/shittyauth/ShittyAuthServer.jar" ] 34 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | server: 3 | image: mrletsplay/shittyauth 4 | environment: 5 | - "UID=1000" 6 | - "GID=1000" 7 | - "SHITTYAUTH_SKIP_SETUP=true" 8 | - "SHITTYAUTH_ADMIN_USER=admin" 9 | - "SHITTYAUTH_ADMIN_PASSWORD=admin" 10 | volumes: 11 | - "./shittyauth/:/shittyauth/data" 12 | frontend: 13 | image: mrletsplay/shittyauth_frontend 14 | ports: 15 | - 80:80 16 | - 443:443 17 | volumes: 18 | - ./frontend/config.json:/usr/local/apache2/htdocs/config.json 19 | - ./frontend/httpd.conf:/usr/local/apache2/conf/extra/include-shittyauth.conf 20 | - ./frontend/.htaccess:/usr/local/apache2/htdocs/.htaccess 21 | -------------------------------------------------------------------------------- /docker/frontend/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_URI} !^/server 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule ^(.*)$ /server/$1 [P,QSA,L] 5 | -------------------------------------------------------------------------------- /docker/frontend/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiBaseURL": "/api/shittyauth" 3 | } 4 | -------------------------------------------------------------------------------- /docker/frontend/httpd.conf: -------------------------------------------------------------------------------- 1 | LoadModule proxy_module modules/mod_proxy.so 2 | LoadModule proxy_http_module modules/mod_proxy_http.so 3 | LoadModule rewrite_module modules/mod_rewrite.so 4 | 5 | 6 | ProxyPass /server http://server:8880 7 | ProxyPassReverse /server http://server:8880 8 | 9 | -------------------------------------------------------------------------------- /docker/launcher_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "updateUID": true, 3 | "user": "shittyauth", 4 | 5 | "updateGID": true, 6 | "group": "shittyauth", 7 | 8 | "runBefore": [ 9 | [ "chown", "-R", "shittyauth:shittyauth", "/shittyauth" ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | me.mrletsplay 6 | ShittyAuthServer 7 | 1.7-SNAPSHOT 8 | 9 | src 10 | 11 | 12 | maven-compiler-plugin 13 | 3.8.1 14 | 15 | 11 16 | UTF-8 17 | 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-jar-plugin 22 | 3.2.0 23 | 24 | 25 | 26 | me.mrletsplay.shittyauth.ShittyAuth 27 | 28 | 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-shade-plugin 34 | 2.4.3 35 | 36 | 37 | package 38 | 39 | shade 40 | 41 | 42 | 43 | 44 | 45 | 46 | *:* 47 | 48 | META-INF/*.SF 49 | META-INF/*.DSA 50 | META-INF/*.RSA 51 | 52 | 53 | 54 | 55 | 56 | 57 | me.mrletsplay 58 | resource-index-maven-plugin 59 | 1.2.1 60 | 61 | 62 | compile 63 | 64 | generate-index 65 | 66 | 67 | 68 | 69 | shittyauth-resources.list 70 | 71 | 72 | 73 | 74 | 75 | ${project.basedir}/resources 76 | include 77 | 78 | 79 | 80 | 81 | 82 | Graphite-Official 83 | https://maven.graphite-official.com/releases 84 | 85 | 86 | 87 | 88 | Graphite-Official 89 | https://maven.graphite-official.com/releases 90 | 91 | 92 | 93 | 94 | me.mrletsplay 95 | WebinterfaceAPI 96 | 3.5-SNAPSHOT 97 | 98 | 99 | me.mrletsplay 100 | MrCore 101 | 4.4 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /resources/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/ShittyAuth.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.ByteArrayInputStream; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.OutputStream; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.nio.file.Paths; 11 | import java.nio.file.StandardOpenOption; 12 | import java.security.KeyFactory; 13 | import java.security.KeyPair; 14 | import java.security.KeyPairGenerator; 15 | import java.security.NoSuchAlgorithmException; 16 | import java.security.PrivateKey; 17 | import java.security.PublicKey; 18 | import java.security.spec.InvalidKeySpecException; 19 | import java.security.spec.PKCS8EncodedKeySpec; 20 | import java.security.spec.X509EncodedKeySpec; 21 | import java.util.Arrays; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | import java.util.UUID; 26 | 27 | import javax.imageio.ImageIO; 28 | 29 | import me.mrletsplay.mrcore.http.HttpRequest; 30 | import me.mrletsplay.shittyauth.auth.AccessTokenStorage; 31 | import me.mrletsplay.shittyauth.auth.FileAccessTokenStorage; 32 | import me.mrletsplay.shittyauth.auth.SQLAccessTokenStorage; 33 | import me.mrletsplay.shittyauth.config.ShittyAuthSettings; 34 | import me.mrletsplay.shittyauth.page.AccountPage; 35 | import me.mrletsplay.shittyauth.page.AdminPage; 36 | import me.mrletsplay.shittyauth.page.CreateAccountPage; 37 | import me.mrletsplay.shittyauth.page.SettingsPage; 38 | import me.mrletsplay.shittyauth.page.api.UserCapeDocument; 39 | import me.mrletsplay.shittyauth.page.api.UserSkinDocument; 40 | import me.mrletsplay.shittyauth.page.api.authlibinjector.AuthLibInjectorDocument; 41 | import me.mrletsplay.shittyauth.page.api.authlibinjector.AuthLibInjectorMetadataDocument; 42 | import me.mrletsplay.shittyauth.page.api.legacy.LegacyCheckServerDocument; 43 | import me.mrletsplay.shittyauth.page.api.legacy.LegacyJoinServerDocument; 44 | import me.mrletsplay.shittyauth.page.api.legacy.LegacyUserCapeDocument; 45 | import me.mrletsplay.shittyauth.page.api.legacy.LegacyUserSkinDocument; 46 | import me.mrletsplay.shittyauth.page.api.services.PlayerAttributesDocument; 47 | import me.mrletsplay.shittyauth.page.api.services.PlayerCertificatesDocument; 48 | import me.mrletsplay.shittyauth.page.api.services.PlayerReportDocument; 49 | import me.mrletsplay.shittyauth.page.api.shittyauth.ShittyAuthAPI; 50 | import me.mrletsplay.shittyauth.page.api.shittyauth.ShittyAuthAdminAPI; 51 | import me.mrletsplay.shittyauth.page.api.yggdrasil.AuthenticatePage; 52 | import me.mrletsplay.shittyauth.page.api.yggdrasil.HasJoinedPage; 53 | import me.mrletsplay.shittyauth.page.api.yggdrasil.InvalidatePage; 54 | import me.mrletsplay.shittyauth.page.api.yggdrasil.JoinPage; 55 | import me.mrletsplay.shittyauth.page.api.yggdrasil.ProfilePage; 56 | import me.mrletsplay.shittyauth.page.api.yggdrasil.PublicKeysPage; 57 | import me.mrletsplay.shittyauth.page.api.yggdrasil.RefreshPage; 58 | import me.mrletsplay.shittyauth.page.api.yggdrasil.ValidatePage; 59 | import me.mrletsplay.shittyauth.user.FileUserDataStorage; 60 | import me.mrletsplay.shittyauth.user.SQLUserDataStorage; 61 | import me.mrletsplay.shittyauth.user.UserData; 62 | import me.mrletsplay.shittyauth.user.UserDataStorage; 63 | import me.mrletsplay.shittyauth.util.DefaultTexture; 64 | import me.mrletsplay.shittyauth.util.InvalidSkinException; 65 | import me.mrletsplay.shittyauth.util.InvalidUsernameException; 66 | import me.mrletsplay.shittyauth.webinterface.ShittyAuthWIHandler; 67 | import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; 68 | import me.mrletsplay.simplehttpserver.http.document.DocumentProvider; 69 | import me.mrletsplay.simplehttpserver.http.document.FileDocument; 70 | import me.mrletsplay.webinterfaceapi.Webinterface; 71 | import me.mrletsplay.webinterfaceapi.auth.Account; 72 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 73 | import me.mrletsplay.webinterfaceapi.config.DefaultSettings; 74 | import me.mrletsplay.webinterfaceapi.config.FileConfig; 75 | import me.mrletsplay.webinterfaceapi.page.PageCategory; 76 | import me.mrletsplay.webinterfaceapi.setup.Setup; 77 | import me.mrletsplay.webinterfaceapi.sql.SQLHelper; 78 | 79 | public class ShittyAuth { 80 | 81 | public static final String ACCOUNT_CONNECTION_NAME = "shittyauth"; 82 | 83 | private static final Path 84 | SKINS_PATH = Paths.get("shittyauth/skins/"), 85 | CAPES_PATH = Paths.get("shittyauth/capes/"), 86 | DEFAULT_SKIN_PATH = DefaultTexture.SKIN_STEVE.getPath(), 87 | DEFAULT_CAPE_PATH = DefaultTexture.CAPE_MIGRATOR.getPath(); 88 | 89 | public static PublicKey publicKey; 90 | public static PrivateKey privateKey; 91 | public static AccessTokenStorage tokenStorage; 92 | public static UserDataStorage dataStorage; 93 | public static FileConfig config; 94 | public static Map userServers = new HashMap<>(); 95 | 96 | public static void main(String[] args) throws NoSuchAlgorithmException, IOException, InvalidKeySpecException { 97 | DefaultSettings.HOME_PAGE_PATH.defaultValue(AccountPage.PATH); 98 | DefaultSettings.CORS_ALLOW_ALL_ORIGINS.defaultValue(true); 99 | DefaultSettings.CORS_ALLOWED_HEADERS.defaultValue(Arrays.asList("*")); 100 | DefaultSettings.CORS_ALLOW_CREDENTIALS.defaultValue(true); 101 | DefaultSettings.ALLOW_ANONYMOUS.defaultValue(false); 102 | 103 | if(!Webinterface.getSetup().isDone() && "true".equals(System.getenv("SHITTYAUTH_SKIP_SETUP"))) { 104 | Webinterface.getLogger().info("Skipping setup"); 105 | 106 | Setup setup = Webinterface.getSetup(); 107 | setup.getSteps().forEach(s -> { 108 | if(setup.isStepDone(s.getID())) return; 109 | setup.addCompletedStep(s.getID()); 110 | }); 111 | } 112 | 113 | Webinterface.start(); 114 | Webinterface.extractResources("/shittyauth-resources.list"); 115 | 116 | Webinterface.getLogger().info("Downloading default textures"); 117 | for(DefaultTexture texture : DefaultTexture.values()) { 118 | Path path = texture.getPath(); 119 | if(!Files.exists(path)) { 120 | Webinterface.getLogger().info("Downloading " + texture.name()); 121 | HttpRequest.createGet(texture.getURL()).execute().transferTo(path.toFile()); 122 | } 123 | } 124 | 125 | if(SQLHelper.isAvailable()) { 126 | tokenStorage = new SQLAccessTokenStorage(); 127 | dataStorage = new SQLUserDataStorage(); 128 | }else { 129 | tokenStorage = new FileAccessTokenStorage(); 130 | dataStorage = new FileUserDataStorage(); 131 | } 132 | 133 | tokenStorage.initialize(); 134 | dataStorage.initialize(); 135 | 136 | config = new FileConfig(new File("shittyauth/shittyauth.yml")); 137 | config.registerSettings(ShittyAuthSettings.INSTANCE); 138 | 139 | File publicKeyFile = new File("shittyauth/public_key.der"); 140 | File privateKeyFile = new File("shittyauth/private_key.der"); 141 | if(!privateKeyFile.exists() || !publicKeyFile.exists()) { 142 | KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); 143 | gen.initialize(4096); 144 | 145 | KeyPair pair = gen.generateKeyPair(); 146 | PrivateKey priv = pair.getPrivate(); 147 | PublicKey pub = pair.getPublic(); 148 | 149 | Files.write(publicKeyFile.toPath(), pub.getEncoded(), StandardOpenOption.CREATE); 150 | Files.write(privateKeyFile.toPath(), priv.getEncoded(), StandardOpenOption.CREATE); 151 | 152 | Webinterface.getLogger().info("Generated a new key pair"); 153 | } 154 | 155 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 156 | 157 | X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(Files.readAllBytes(publicKeyFile.toPath())); 158 | publicKey = keyFactory.generatePublic(pubSpec); 159 | PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(Files.readAllBytes(privateKeyFile.toPath())); 160 | privateKey = keyFactory.generatePrivate(privSpec); 161 | 162 | DocumentProvider provider = Webinterface.getDocumentProvider(); 163 | 164 | provider.register(HttpRequestMethod.GET, "/session/minecraft/hasJoined", new HasJoinedPage()); 165 | provider.register(HttpRequestMethod.POST, "/session/minecraft/join", new JoinPage()); 166 | provider.registerPattern(HttpRequestMethod.GET, "/session/minecraft/profile/{uuid}", ProfilePage.INSTANCE); 167 | provider.register(HttpRequestMethod.POST, "/authenticate", new AuthenticatePage()); 168 | provider.register(HttpRequestMethod.POST, "/validate", new ValidatePage()); 169 | provider.register(HttpRequestMethod.POST, "/invalidate", new InvalidatePage()); 170 | provider.register(HttpRequestMethod.POST, "/signout", new InvalidatePage()); 171 | provider.register(HttpRequestMethod.POST, "/refresh", new RefreshPage()); 172 | provider.register(HttpRequestMethod.GET, "/publickeys", new PublicKeysPage()); 173 | 174 | PlayerAttributesDocument doc = new PlayerAttributesDocument(); 175 | provider.register(HttpRequestMethod.GET, "/player/attributes", doc); 176 | provider.register(HttpRequestMethod.GET, "/privileges", doc); // for MC 1.16 or older 177 | provider.register(HttpRequestMethod.POST, "/player/certificates", new PlayerCertificatesDocument()); 178 | provider.register(HttpRequestMethod.POST, "/player/report", new PlayerReportDocument()); 179 | provider.register(HttpRequestMethod.GET, "/yggdrasil_session_pubkey.der", new FileDocument(Paths.get("shittyauth/public_key.der"))); 180 | 181 | provider.register(HttpRequestMethod.GET, "/game/joinserver.jsp", new LegacyJoinServerDocument()); 182 | provider.register(HttpRequestMethod.GET, "/game/checkserver.jsp", new LegacyCheckServerDocument()); 183 | provider.registerPattern(HttpRequestMethod.GET, "/cape/{uuid}", UserCapeDocument.INSTANCE); 184 | provider.registerPattern(HttpRequestMethod.GET, "/skin/{uuid}", UserSkinDocument.INSTANCE); 185 | provider.registerPattern(HttpRequestMethod.GET, "/MinecraftSkins/{name}", LegacyUserSkinDocument.INSTANCE); 186 | provider.registerPattern(HttpRequestMethod.GET, "/MinecraftCapes/{name}", LegacyUserCapeDocument.INSTANCE); 187 | 188 | if(ShittyAuth.config.getSetting(ShittyAuthSettings.AUTHLIB_INJECTOR_COMPAT)) { 189 | // Provide authlib metadata 190 | provider.register(HttpRequestMethod.GET, "/authserver", new AuthLibInjectorMetadataDocument()); 191 | 192 | // Redirect all of the authlib mappings to their respective ShittyAuth mappings 193 | // See https://github.com/yushijinhun/authlib-injector/blob/961366e012c26849810a744897bf41b2e926b734/src/main/java/moe/yushi/authlibinjector/httpd/DefaultURLRedirector.java#L36 194 | AuthLibInjectorDocument authlibDoc = new AuthLibInjectorDocument(); 195 | List authlibPaths = Arrays.asList("/api", "/authserver", "/sessionserver", "/skins", "/minecraftservices"); 196 | for(String path : authlibPaths) { 197 | String p = path + "/{page...}"; 198 | provider.registerPattern(HttpRequestMethod.GET, p, authlibDoc); 199 | provider.registerPattern(HttpRequestMethod.POST, p, authlibDoc); 200 | } 201 | } 202 | 203 | new ShittyAuthAPI().register(provider); 204 | new ShittyAuthAdminAPI().register(provider); 205 | 206 | Webinterface.registerActionHandler(new ShittyAuthWIHandler()); 207 | 208 | initFromEnvvars(); 209 | 210 | PageCategory cat = Webinterface.createCategory("Minecraft"); 211 | cat.addPage(new AccountPage()); 212 | cat.addPage(new CreateAccountPage()); 213 | cat.addPage(new AdminPage()); 214 | cat.addPage(new SettingsPage()); 215 | } 216 | 217 | private static void initFromEnvvars() { 218 | String adminUser = System.getenv("SHITTYAUTH_ADMIN_USER"); 219 | String adminPass = System.getenv("SHITTYAUTH_ADMIN_PASSWORD"); 220 | if(adminUser != null && adminPass != null) { 221 | Webinterface.getLogger().info("Creating admin user"); 222 | 223 | Account acc = getAccountByUsername(adminUser); 224 | if(acc != null) { 225 | Webinterface.getLogger().info("Account with username '" + adminUser + "' already exists. Skipping"); 226 | }else { 227 | try { 228 | acc = createAccount(adminUser, adminPass); 229 | acc.addPermission("*"); 230 | } catch (InvalidUsernameException e) { 231 | Webinterface.getLogger().error("Failed to create account", e); 232 | } 233 | } 234 | } 235 | } 236 | 237 | public static Account createAccount(String username, String password) throws InvalidUsernameException { 238 | if(!ShittyAuthWIHandler.USERNAME_PATTERN.matcher(username).matches()) throw new InvalidUsernameException("Invalid username"); 239 | 240 | String uuid = UUID.randomUUID().toString(); 241 | AccountConnection conn = new AccountConnection(ACCOUNT_CONNECTION_NAME, uuid, username, null, null); 242 | Account acc = Webinterface.getAccountStorage().createAccount(conn); 243 | Webinterface.getCredentialsStorage().storeCredentials(ACCOUNT_CONNECTION_NAME, uuid, password); 244 | ShittyAuth.dataStorage.updateUserData(uuid, UserData.createNew()); 245 | return acc; 246 | } 247 | 248 | public static Account getAccountByUsername(String username) { 249 | // TODO: more efficient implementation? 250 | for(Account a : Webinterface.getAccountStorage().getAccounts()) { 251 | AccountConnection con = a.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 252 | if(con != null && con.getUserName().equalsIgnoreCase(username)) { 253 | return a; 254 | } 255 | } 256 | return null; 257 | } 258 | 259 | public static void updateUserSkin(String userID, DefaultTexture texture) throws IOException { 260 | Path texPath = texture.getPath(); 261 | try { 262 | BufferedImage image = ImageIO.read(texPath.toFile()); 263 | updateUserSkin(userID, image); 264 | }catch(InvalidSkinException ignored) {} 265 | } 266 | 267 | public static void updateUserSkin(String userID, BufferedImage image) throws IOException, InvalidSkinException { 268 | if(image.getWidth() != 64 || (image.getHeight() != 64 && image.getHeight() != 32)) throw new InvalidSkinException("Skin must be 64x64 or 64x32 pixels"); 269 | BufferedImage copy = new BufferedImage(64, image.getHeight(), BufferedImage.TYPE_INT_ARGB); 270 | copy.createGraphics().drawImage(image, 0, 0, null); 271 | 272 | if(!Files.exists(SKINS_PATH)) Files.createDirectories(SKINS_PATH); 273 | Path skinPath = SKINS_PATH.resolve(userID + ".png"); 274 | try(OutputStream out = Files.newOutputStream(skinPath)) { 275 | ImageIO.write(copy, "PNG", out); 276 | } 277 | 278 | UserData d = ShittyAuth.dataStorage.getUserData(userID); 279 | d.setSkinLastChanged(System.currentTimeMillis()); 280 | ShittyAuth.dataStorage.updateUserData(userID, d); 281 | } 282 | 283 | public static BufferedImage loadUserSkin(String userID) throws IOException { 284 | return ImageIO.read(new ByteArrayInputStream(loadUserSkinRaw(userID))); 285 | } 286 | 287 | public static byte[] loadUserSkinRaw(String userID) throws IOException { 288 | Path skinPath = SKINS_PATH.resolve(userID + ".png"); 289 | if(!skinPath.normalize().startsWith(SKINS_PATH)) throw new IOException("Invalid path"); 290 | if(!Files.exists(skinPath)) skinPath = DEFAULT_SKIN_PATH; 291 | return Files.readAllBytes(skinPath); 292 | } 293 | 294 | public static void updateUserCape(String userID, DefaultTexture texture) throws IOException { 295 | Path texPath = texture.getPath(); 296 | try { 297 | BufferedImage image = ImageIO.read(texPath.toFile()); 298 | updateUserCape(userID, image); 299 | }catch(InvalidSkinException ignored) {} 300 | } 301 | 302 | public static void updateUserCape(String userID, BufferedImage image) throws IOException, InvalidSkinException{ 303 | if(image.getWidth() != 64 || image.getHeight() != 32) throw new InvalidSkinException("Cape must be 64x32"); 304 | BufferedImage copy = new BufferedImage(64, 32, BufferedImage.TYPE_INT_ARGB); 305 | copy.createGraphics().drawImage(image, 0, 0, null); 306 | 307 | if(!Files.exists(CAPES_PATH)) Files.createDirectories(CAPES_PATH); 308 | Path skinPath = CAPES_PATH.resolve(userID + ".png"); 309 | try(OutputStream out = Files.newOutputStream(skinPath)) { 310 | ImageIO.write(copy, "PNG", out); 311 | } 312 | 313 | UserData d = ShittyAuth.dataStorage.getUserData(userID); 314 | d.setCapeLastChanged(System.currentTimeMillis()); 315 | ShittyAuth.dataStorage.updateUserData(userID, d); 316 | } 317 | 318 | public static BufferedImage loadUserCape(String userID) throws IOException { 319 | return ImageIO.read(new ByteArrayInputStream(loadUserCapeRaw(userID))); 320 | } 321 | 322 | public static byte[] loadUserCapeRaw(String userID) throws IOException { 323 | Path capePath = CAPES_PATH.resolve(userID + ".png"); 324 | if(!capePath.normalize().startsWith(CAPES_PATH)) throw new IOException("Invalid path"); 325 | if(!Files.exists(capePath)) capePath = DEFAULT_CAPE_PATH; 326 | return Files.readAllBytes(capePath); 327 | } 328 | 329 | } 330 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/auth/AccessToken.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.auth; 2 | 3 | import java.util.Objects; 4 | 5 | public class AccessToken extends StoredAccessToken { 6 | 7 | private String accessToken; 8 | 9 | public AccessToken(String clientToken, String accountID, long softExpiresAt, long expiresAt, String accessToken) { 10 | super(clientToken, accountID, softExpiresAt, expiresAt); 11 | this.accessToken = accessToken; 12 | } 13 | 14 | public String getAccessToken() { 15 | return accessToken; 16 | } 17 | 18 | @Override 19 | public int hashCode() { 20 | final int prime = 31; 21 | int result = super.hashCode(); 22 | result = prime * result + Objects.hash(accessToken); 23 | return result; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object obj) { 28 | if (this == obj) 29 | return true; 30 | if (!super.equals(obj)) 31 | return false; 32 | if (getClass() != obj.getClass()) 33 | return false; 34 | AccessToken other = (AccessToken) obj; 35 | return Objects.equals(accessToken, other.accessToken); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/auth/AccessTokenStorage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.auth; 2 | 3 | import java.time.Instant; 4 | import java.time.temporal.ChronoUnit; 5 | import java.util.UUID; 6 | 7 | public interface AccessTokenStorage { 8 | 9 | public void initialize(); 10 | 11 | public AccessToken generateToken(String accID, String clientToken); 12 | 13 | public String getAccountID(String token); 14 | 15 | public StoredAccessToken getStoredToken(String token); 16 | 17 | public boolean checkValid(String token, String clientToken); 18 | 19 | public void removeToken(String token); 20 | 21 | public void removeTokensByAccountID(String accID); 22 | 23 | public void cleanUp(); 24 | 25 | public default AccessToken createNewToken(String accountID, String clientToken) { 26 | String tok = newToken(); 27 | Instant softExpires = Instant.now().plus(7, ChronoUnit.DAYS); 28 | Instant expires = Instant.now().plus(30, ChronoUnit.DAYS); 29 | return new AccessToken(clientToken, accountID, softExpires.toEpochMilli(), expires.toEpochMilli(), tok); 30 | } 31 | 32 | private static String newToken() { 33 | return UUID.randomUUID().toString(); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/auth/FileAccessTokenStorage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.auth; 2 | 3 | import java.io.File; 4 | import java.security.NoSuchAlgorithmException; 5 | import java.security.spec.InvalidKeySpecException; 6 | import java.security.spec.KeySpec; 7 | import java.util.Base64; 8 | import java.util.UUID; 9 | 10 | import javax.crypto.SecretKeyFactory; 11 | import javax.crypto.spec.PBEKeySpec; 12 | 13 | import me.mrletsplay.mrcore.config.ConfigLoader; 14 | import me.mrletsplay.mrcore.config.FileCustomConfig; 15 | import me.mrletsplay.mrcore.misc.FriendlyException; 16 | import me.mrletsplay.webinterfaceapi.Webinterface; 17 | 18 | public class FileAccessTokenStorage implements AccessTokenStorage { 19 | 20 | // TODO: Expire tokens, keep only one token per clientToken, add a way for users to manually invalidate tokens? 21 | 22 | private FileCustomConfig config; 23 | 24 | @Override 25 | public void initialize() { 26 | config = ConfigLoader.loadFileConfig(new File(Webinterface.getDataDirectory(), "shittyauth/token-storage.yml")); 27 | } 28 | 29 | @Override 30 | public AccessToken generateToken(String accID, String clientToken) { 31 | if(clientToken == null) clientToken = UUID.randomUUID().toString(); 32 | AccessToken token = createNewToken(accID, clientToken); 33 | 34 | String tokenHash = hash(token.getAccessToken()); 35 | config.set(tokenHash + ".client-token", token.getClientToken()); 36 | config.set(tokenHash + ".account-id", token.getAccountID()); 37 | config.set(tokenHash + ".soft-expires-at", token.getSoftExpiresAt()); 38 | config.set(tokenHash + ".expires-at", token.getExpiresAt()); 39 | config.saveToFile(); 40 | return token; 41 | } 42 | 43 | @Override 44 | public String getAccountID(String token) { 45 | return config.getString(hash(token) + ".account-id"); 46 | } 47 | 48 | @Override 49 | public StoredAccessToken getStoredToken(String token) { 50 | String tokenHash = hash(token); 51 | return new StoredAccessToken( 52 | config.getString(tokenHash + ".client-token"), 53 | config.getString(tokenHash + ".account-id"), 54 | config.getLong(tokenHash + ".soft-expires-at"), 55 | config.getLong(tokenHash + ".expires-at")); 56 | } 57 | 58 | @Override 59 | public boolean checkValid(String token, String clientToken) { 60 | String clientTok = config.getString(hash(token) + ".client-token"); 61 | if(clientTok == null) return false; 62 | return clientTok.equals(clientToken); 63 | } 64 | 65 | @Override 66 | public void removeToken(String token) { 67 | config.unset(hash(token)); 68 | config.saveToFile(); 69 | } 70 | 71 | @Override 72 | public void removeTokensByAccountID(String accID) { 73 | for(String k : config.getKeys()) { 74 | if(accID.equals(config.getString(k + ".account-id"))) { 75 | config.unset(k); 76 | } 77 | } 78 | config.saveToFile(); 79 | } 80 | 81 | @Override 82 | public void cleanUp() { 83 | long currentTime = System.currentTimeMillis(); 84 | for(String k : config.getKeys()) { 85 | if(config.getLong(k + ".expires-at") > currentTime) { 86 | config.unset(k); 87 | } 88 | } 89 | config.saveToFile(); 90 | } 91 | 92 | private String hash(String raw) { 93 | try { 94 | byte[] salt = new byte[16]; // No salt initialization needed, token is already random 95 | KeySpec spec = new PBEKeySpec(raw.toCharArray(), salt, 65536, 128); 96 | SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); 97 | byte[] hash = f.generateSecret(spec).getEncoded(); 98 | return Base64.getEncoder().encodeToString(hash); 99 | }catch(NoSuchAlgorithmException | InvalidKeySpecException e) { 100 | throw new FriendlyException(e); 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/auth/SQLAccessTokenStorage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.auth; 2 | 3 | import java.security.NoSuchAlgorithmException; 4 | import java.security.spec.InvalidKeySpecException; 5 | import java.security.spec.KeySpec; 6 | import java.sql.PreparedStatement; 7 | import java.sql.ResultSet; 8 | import java.util.Base64; 9 | import java.util.UUID; 10 | 11 | import javax.crypto.SecretKeyFactory; 12 | import javax.crypto.spec.PBEKeySpec; 13 | 14 | import me.mrletsplay.mrcore.misc.FriendlyException; 15 | import me.mrletsplay.webinterfaceapi.sql.SQLHelper; 16 | 17 | public class SQLAccessTokenStorage implements AccessTokenStorage { 18 | 19 | // TODO: Expire tokens, keep only one token per clientToken, add a way for users to manually invalidate tokens? 20 | 21 | @Override 22 | public void initialize() { 23 | SQLHelper.run(c -> { 24 | try(PreparedStatement st = c.prepareStatement("CREATE TABLE IF NOT EXISTS " + SQLHelper.tableName("shittyauth_tokens") + "(TokenHash VARCHAR(255) PRIMARY KEY, AccountId VARCHAR(255), ClientToken VARCHAR(255), SoftExpiresAt BIGINT, ExpiresAt BIGINT)")) { 25 | st.execute(); 26 | } 27 | }); 28 | } 29 | 30 | @Override 31 | public AccessToken generateToken(String accID, String clientToken) { 32 | if(clientToken == null) clientToken = UUID.randomUUID().toString(); 33 | AccessToken token = createNewToken(accID, clientToken); 34 | SQLHelper.run(c -> { 35 | try(PreparedStatement st = c.prepareStatement("INSERT INTO " + SQLHelper.tableName("shittyauth_tokens") + "(TokenHash, AccountId, ClientToken, SoftExpiresAt, ExpiresAt) VALUES(?, ?, ?, ?, ?)")) { 36 | st.setString(1, hash(token.getAccessToken())); 37 | st.setString(2, accID); 38 | st.setString(3, token.getClientToken()); 39 | st.setLong(4, token.getSoftExpiresAt()); 40 | st.setLong(5, token.getExpiresAt()); 41 | st.execute(); 42 | } 43 | }); 44 | return token; 45 | } 46 | 47 | @Override 48 | public String getAccountID(String token) { 49 | return SQLHelper.run(c -> { 50 | try(PreparedStatement st = c.prepareStatement("SELECT AccountId FROM " + SQLHelper.tableName("shittyauth_tokens") + " WHERE TokenHash = ?")) { 51 | st.setString(1, hash(token)); 52 | try(ResultSet r = st.executeQuery()) { 53 | if(!r.next()) return null; 54 | return r.getString("AccountId"); 55 | } 56 | } 57 | }); 58 | } 59 | 60 | @Override 61 | public StoredAccessToken getStoredToken(String token) { 62 | return SQLHelper.run(c -> { 63 | try(PreparedStatement st = c.prepareStatement("SELECT * FROM " + SQLHelper.tableName("shittyauth_tokens") + " WHERE TokenHash = ?")) { 64 | st.setString(1, hash(token)); 65 | try(ResultSet r = st.executeQuery()) { 66 | if(!r.next()) return null; 67 | return new StoredAccessToken(r.getString("ClientToken"), r.getString("AccountId"), r.getLong("SoftExpiresAt"), r.getLong("ExpiresAt")); 68 | } 69 | } 70 | }); 71 | } 72 | 73 | @Override 74 | public boolean checkValid(String token, String clientToken) { 75 | return SQLHelper.run(c -> { 76 | try(PreparedStatement st = c.prepareStatement("SELECT ClientToken FROM " + SQLHelper.tableName("shittyauth_tokens") + " WHERE TokenHash = ?")) { 77 | st.setString(1, hash(token)); 78 | try(ResultSet r = st.executeQuery()) { 79 | if(!r.next()) return false; 80 | return r.getString("ClientToken").equals(clientToken); 81 | } 82 | } 83 | }); 84 | } 85 | 86 | @Override 87 | public void removeToken(String token) { 88 | SQLHelper.run(c -> { 89 | try(PreparedStatement st = c.prepareStatement("DELETE FROM " + SQLHelper.tableName("shittyauth_tokens") + " WHERE TokenHash = ?")) { 90 | st.setString(1, hash(token)); 91 | st.execute(); 92 | } 93 | }); 94 | } 95 | 96 | @Override 97 | public void removeTokensByAccountID(String accID) { 98 | SQLHelper.run(c -> { 99 | try(PreparedStatement st = c.prepareStatement("DELETE FROM " + SQLHelper.tableName("shittyauth_tokens") + " WHERE AccountId = ?")) { 100 | st.setString(1, accID); 101 | st.execute(); 102 | } 103 | }); 104 | } 105 | 106 | @Override 107 | public void cleanUp() { 108 | long currentTime = System.currentTimeMillis(); 109 | SQLHelper.run(c -> { 110 | try(PreparedStatement st = c.prepareStatement("DELETE FROM " + SQLHelper.tableName("shittyauth_tokens") + " WHERE ExpiresAt > ?")) { 111 | st.setLong(1, currentTime); 112 | st.execute(); 113 | } 114 | }); 115 | } 116 | 117 | private String hash(String raw) { 118 | try { 119 | byte[] salt = new byte[16]; // No salt initialization needed, token is already random 120 | KeySpec spec = new PBEKeySpec(raw.toCharArray(), salt, 65536, 128); 121 | SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); 122 | byte[] hash = f.generateSecret(spec).getEncoded(); 123 | return Base64.getEncoder().encodeToString(hash); 124 | }catch(NoSuchAlgorithmException | InvalidKeySpecException e) { 125 | throw new FriendlyException(e); 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/auth/StoredAccessToken.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.auth; 2 | 3 | public class StoredAccessToken { 4 | 5 | protected String clientToken; 6 | protected String accountID; 7 | 8 | /** 9 | * Time when the token becomes unusable for authentication when joining a server, but can still be refreshed 10 | */ 11 | protected long softExpiresAt; 12 | 13 | /** 14 | * Time when the token fully expires and cannot be refreshed anymore 15 | */ 16 | protected long expiresAt; 17 | 18 | public StoredAccessToken(String clientToken, String accountID, long softExpiresAt, long expiresAt) { 19 | this.clientToken = clientToken; 20 | this.accountID = accountID; 21 | this.softExpiresAt = softExpiresAt; 22 | this.expiresAt = expiresAt; 23 | } 24 | 25 | public String getClientToken() { 26 | return clientToken; 27 | } 28 | 29 | public String getAccountID() { 30 | return accountID; 31 | } 32 | 33 | public long getSoftExpiresAt() { 34 | return softExpiresAt; 35 | } 36 | 37 | public long getExpiresAt() { 38 | return expiresAt; 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | final int prime = 31; 44 | int result = 1; 45 | result = prime * result + ((accountID == null) ? 0 : accountID.hashCode()); 46 | result = prime * result + ((clientToken == null) ? 0 : clientToken.hashCode()); 47 | return result; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object obj) { 52 | if (this == obj) 53 | return true; 54 | if (obj == null) 55 | return false; 56 | if (getClass() != obj.getClass()) 57 | return false; 58 | StoredAccessToken other = (StoredAccessToken) obj; 59 | if (accountID == null) { 60 | if (other.accountID != null) 61 | return false; 62 | } else if (!accountID.equals(other.accountID)) 63 | return false; 64 | if (clientToken == null) { 65 | if (other.clientToken != null) 66 | return false; 67 | } else if (!clientToken.equals(other.clientToken)) 68 | return false; 69 | return true; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/config/ShittyAuthSettings.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.config; 2 | 3 | import me.mrletsplay.webinterfaceapi.config.setting.AutoSetting; 4 | import me.mrletsplay.webinterfaceapi.config.setting.AutoSettings; 5 | import me.mrletsplay.webinterfaceapi.config.setting.SettingsCategory; 6 | import me.mrletsplay.webinterfaceapi.config.setting.impl.BooleanSetting; 7 | 8 | public class ShittyAuthSettings implements AutoSettings { 9 | 10 | public static final ShittyAuthSettings INSTANCE = new ShittyAuthSettings(); 11 | 12 | @AutoSetting 13 | private static SettingsCategory 14 | general = new SettingsCategory("General"), 15 | http = new SettingsCategory("HTTP"); 16 | 17 | // General 18 | public static final BooleanSetting 19 | ALLOW_REGISTRATION = general.addBoolean("general.allow-registration", true, "Allow creation of Minecraft account", "Does not affect registration of WIAPI accounts"); 20 | 21 | // HTTP 22 | public static final BooleanSetting 23 | AUTHLIB_INJECTOR_COMPAT = http.addBoolean("http.authlib-injector-compat", false, "Authlib-injector compat (Requires restart)", "Enable compatibility for authlib-injector"); 24 | 25 | private ShittyAuthSettings() {} 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/AccountPage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page; 2 | 3 | import me.mrletsplay.shittyauth.ShittyAuth; 4 | import me.mrletsplay.shittyauth.textures.SkinType; 5 | import me.mrletsplay.shittyauth.user.UserData; 6 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 7 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 8 | import me.mrletsplay.webinterfaceapi.auth.Account; 9 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 10 | import me.mrletsplay.webinterfaceapi.page.Page; 11 | import me.mrletsplay.webinterfaceapi.page.PageSection; 12 | import me.mrletsplay.webinterfaceapi.page.action.MultiAction; 13 | import me.mrletsplay.webinterfaceapi.page.action.SendJSAction; 14 | import me.mrletsplay.webinterfaceapi.page.action.SetValueAction; 15 | import me.mrletsplay.webinterfaceapi.page.action.ShowToastAction; 16 | import me.mrletsplay.webinterfaceapi.page.action.SubmitUploadAction; 17 | import me.mrletsplay.webinterfaceapi.page.action.value.ActionValue; 18 | import me.mrletsplay.webinterfaceapi.page.element.Button; 19 | import me.mrletsplay.webinterfaceapi.page.element.CheckBox; 20 | import me.mrletsplay.webinterfaceapi.page.element.FileUpload; 21 | import me.mrletsplay.webinterfaceapi.page.element.Heading; 22 | import me.mrletsplay.webinterfaceapi.page.element.PasswordField; 23 | import me.mrletsplay.webinterfaceapi.page.element.Select; 24 | import me.mrletsplay.webinterfaceapi.page.element.Text; 25 | import me.mrletsplay.webinterfaceapi.page.element.TitleText; 26 | import me.mrletsplay.webinterfaceapi.page.element.VerticalSpacer; 27 | import me.mrletsplay.webinterfaceapi.page.element.layout.DefaultLayoutOption; 28 | import me.mrletsplay.webinterfaceapi.page.element.layout.Grid; 29 | import me.mrletsplay.webinterfaceapi.session.Session; 30 | 31 | public class AccountPage extends Page { 32 | 33 | public static final String PATH = "/shittyauth/account"; 34 | 35 | public AccountPage() { 36 | super("Account", PATH); 37 | setIcon("mdi:minecraft"); 38 | 39 | PageSection s = new PageSection(); 40 | s.setSlimLayout(true); 41 | s.setGrid(new Grid().setColumns("75fr", "25fr")); 42 | s.dynamic(els -> { 43 | AccountConnection con = Session.getCurrentSession().getAccount().getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 44 | UserData d = ShittyAuth.dataStorage.getUserData(con.getUserID()); 45 | 46 | els.add(Heading.builder() 47 | .level(2) 48 | .leftboundText() 49 | .text(con.getUserName() + " (" + con.getUserID() + ")") 50 | .fullWidth() 51 | .create()); 52 | 53 | els.add(TitleText.builder() 54 | .leftboundText() 55 | .text("Skin Type") 56 | .fullWidth() 57 | .create()); 58 | 59 | Select skinTypeSel = Select.builder() 60 | .addOption("Steve", "STEVE", d.getSkinType() == SkinType.STEVE) 61 | .addOption("Alex", "ALEX", d.getSkinType() == SkinType.ALEX) 62 | .create(); 63 | els.add(skinTypeSel); 64 | 65 | els.add(Button.builder() 66 | .text("Update skin type") 67 | .onClick(SendJSAction.of("shittyauth", "setSkinType", ActionValue.object().put("type", ActionValue.elementValue(skinTypeSel))).onSuccess(ShowToastAction.info("Updated!"))) 68 | .create()); 69 | }); 70 | 71 | VerticalSpacer sp1 = new VerticalSpacer("30px"); 72 | sp1.addLayoutOptions(DefaultLayoutOption.FULL_WIDTH); 73 | s.addElement(sp1); 74 | 75 | s.addElement(TitleText.builder() 76 | .leftboundText() 77 | .text("Skin") 78 | .fullWidth() 79 | .create()); 80 | 81 | FileUpload uploadSkin = FileUpload.builder() 82 | .uploadHandler("shittyauth", "uploadSkin") 83 | .create(); 84 | s.addElement(uploadSkin); 85 | 86 | s.addElement(Button.builder() 87 | .text("Upload Skin") 88 | .onClick(SubmitUploadAction.of(uploadSkin).onSuccess(ShowToastAction.info("Skin uploaded!"))) 89 | .create()); 90 | 91 | VerticalSpacer sp2 = new VerticalSpacer("30px"); 92 | sp2.addLayoutOptions(DefaultLayoutOption.FULL_WIDTH); 93 | s.addElement(sp2); 94 | 95 | s.addElement(TitleText.builder() 96 | .leftboundText() 97 | .text("Cape") 98 | .fullWidth() 99 | .create()); 100 | 101 | FileUpload uploadCape = FileUpload.builder() 102 | .uploadHandler("shittyauth", "uploadCape") 103 | .create(); 104 | s.addElement(uploadCape); 105 | 106 | s.addElement(Button.builder() 107 | .text("Upload Cape") 108 | .onClick(SubmitUploadAction.of(uploadCape).onSuccess(ShowToastAction.info("Cape uploaded!"))) 109 | .create()); 110 | 111 | s.addElement(Text.builder() 112 | .leftboundText() 113 | .text("Enable cape?") 114 | .create()); 115 | 116 | s.dynamic(() -> { 117 | UserData d = ShittyAuth.dataStorage.getUserData(Session.getCurrentSession().getAccount().getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME).getUserID()); 118 | 119 | return CheckBox.builder() 120 | .initialState(d.hasCape()) 121 | .onChange(ch -> SendJSAction.of("shittyauth", "setEnableCape", ActionValue.object().put("enable", ActionValue.checkboxValue(ch))).onSuccess(ShowToastAction.info("Updated!"))) 122 | .create(); 123 | }); 124 | 125 | VerticalSpacer sp3 = new VerticalSpacer("30px"); 126 | sp3.addLayoutOptions(DefaultLayoutOption.FULL_WIDTH); 127 | s.addElement(sp3); 128 | 129 | s.addElement(TitleText.builder() 130 | .leftboundText() 131 | .text("Reset password") 132 | .fullWidth() 133 | .create()); 134 | 135 | PasswordField passInput = PasswordField.builder().create(); 136 | s.addElement(passInput); 137 | 138 | s.addElement(Button.builder() 139 | .text("Change Password") 140 | .onClick(MultiAction.of( 141 | SendJSAction.of("shittyauth", "resetPassword", ActionValue.object().put("password", ActionValue.elementValue(passInput))) 142 | .onSuccess(ShowToastAction.info("Successfully changed password!")), 143 | SetValueAction.of(passInput, ActionValue.nullValue()))) 144 | .create()); 145 | 146 | addSection(s); 147 | } 148 | 149 | @Override 150 | public void createContent() { 151 | if(!Session.requireSession()) return; 152 | 153 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 154 | Account acc = Session.getCurrentSession().getAccount(); 155 | AccountConnection mcCon = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 156 | if(mcCon == null) { 157 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.FOUND_302); 158 | ctx.getServerHeader().getFields().set("Location", CreateAccountPage.PATH); 159 | return; 160 | } 161 | 162 | super.createContent(); 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/AdminPage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page; 2 | 3 | import me.mrletsplay.shittyauth.ShittyAuth; 4 | import me.mrletsplay.webinterfaceapi.DefaultPermissions; 5 | import me.mrletsplay.webinterfaceapi.Webinterface; 6 | import me.mrletsplay.webinterfaceapi.auth.Account; 7 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 8 | import me.mrletsplay.webinterfaceapi.page.Page; 9 | import me.mrletsplay.webinterfaceapi.page.PageSection; 10 | import me.mrletsplay.webinterfaceapi.page.action.ConfirmAction; 11 | import me.mrletsplay.webinterfaceapi.page.action.ReloadPageAction; 12 | import me.mrletsplay.webinterfaceapi.page.action.SendJSAction; 13 | import me.mrletsplay.webinterfaceapi.page.action.value.ActionValue; 14 | import me.mrletsplay.webinterfaceapi.page.element.Button; 15 | import me.mrletsplay.webinterfaceapi.page.element.Group; 16 | import me.mrletsplay.webinterfaceapi.page.element.Image; 17 | import me.mrletsplay.webinterfaceapi.page.element.InputField; 18 | import me.mrletsplay.webinterfaceapi.page.element.PasswordField; 19 | import me.mrletsplay.webinterfaceapi.page.element.Text; 20 | import me.mrletsplay.webinterfaceapi.page.element.TitleText; 21 | import me.mrletsplay.webinterfaceapi.page.element.VerticalSpacer; 22 | import me.mrletsplay.webinterfaceapi.page.element.builder.Align; 23 | import me.mrletsplay.webinterfaceapi.page.element.layout.DefaultLayoutOption; 24 | import me.mrletsplay.webinterfaceapi.page.element.layout.Grid; 25 | 26 | public class AdminPage extends Page { 27 | 28 | public static final String PATH = "/shittyauth/admin"; 29 | 30 | public AdminPage() { 31 | super("Admin", PATH); 32 | setIcon("mdi:security"); 33 | setPermission(DefaultPermissions.MODIFY_USERS); 34 | 35 | PageSection sc = new PageSection(); 36 | sc.setSlimLayout(true); 37 | 38 | sc.addHeading("Accounts", 2); 39 | sc.getStyle().setProperty("grid-template-columns", "1fr"); 40 | sc.getMobileStyle().setProperty("grid-template-columns", "1fr"); 41 | 42 | sc.dynamic(els -> { 43 | for(Account acc : Webinterface.getAccountStorage().getAccounts()) { 44 | AccountConnection conn = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 45 | if(conn == null) continue; 46 | 47 | Group grp = new Group(); 48 | grp.getStyle().setProperty("border", "1px solid var(--theme-color-content-border)"); 49 | grp.getStyle().setProperty("border-radius", "5px"); 50 | grp.getStyle().setProperty("margin", "5px 0px"); 51 | grp.setGrid(new Grid().setColumns("min-content", "auto")); 52 | 53 | TitleText tt = TitleText.builder() 54 | .text(conn.getUserName()) 55 | .leftboundText() 56 | .noLineBreaks() 57 | .create(); 58 | tt.getStyle().setProperty("font-size", "24px"); 59 | grp.addElement(tt); 60 | 61 | grp.addElement(Image.builder() 62 | .src("/api/shittyauth/avatar/" + conn.getUserID()) 63 | .width("64px") 64 | .height("64px") 65 | .withLayoutOptions((container, element) -> element.appendAttribute("style", "image-rendering: pixelated;")) 66 | .align(Align.LEFT_CENTER) 67 | .create()); 68 | 69 | grp.addElement(TitleText.builder() 70 | .text("User ID") 71 | .noLineBreaks() 72 | .leftboundText() 73 | .withLayoutOptions(DefaultLayoutOption.NEW_LINE) 74 | .create()); 75 | grp.addElement(Text.builder() 76 | .text(conn.getUserID()) 77 | .leftboundText() 78 | .create()); 79 | 80 | grp.addElement(TitleText.builder() 81 | .text("WIAPI account name") 82 | .noLineBreaks() 83 | .leftboundText() 84 | .withLayoutOptions(DefaultLayoutOption.NEW_LINE) 85 | .create()); 86 | grp.addElement(Text.builder() 87 | .text(acc.getUsername()) 88 | .leftboundText() 89 | .create()); 90 | 91 | grp.addElement(TitleText.builder() 92 | .text("WIAPI account ID") 93 | .noLineBreaks() 94 | .leftboundText() 95 | .withLayoutOptions(DefaultLayoutOption.NEW_LINE) 96 | .create()); 97 | grp.addElement(Text.builder() 98 | .text(acc.getID()) 99 | .leftboundText() 100 | .create()); 101 | 102 | Group actions = new Group(); 103 | actions.addLayoutOptions(new Grid().setColumns("1fr auto"), DefaultLayoutOption.FULL_WIDTH); 104 | 105 | InputField usernameField = InputField.builder() 106 | .placeholder("Username") 107 | .create(); 108 | actions.addElement(usernameField); 109 | 110 | actions.addElement(Button.builder() 111 | .text("Change MC username") 112 | .onClick(ConfirmAction.of(SendJSAction.of( 113 | "shittyauth", "changeMCUsername", 114 | ActionValue.object() 115 | .put("account", ActionValue.string(acc.getID())) 116 | .put("username", ActionValue.elementValue(usernameField)) 117 | ) 118 | .onSuccess(ReloadPageAction.delayed(100)))) 119 | .create()); 120 | 121 | PasswordField passwordField = PasswordField.builder() 122 | .placeholder("Password") 123 | .create(); 124 | actions.addElement(passwordField); 125 | 126 | actions.addElement(Button.builder() 127 | .text("Change MC password") 128 | .onClick(ConfirmAction.of(SendJSAction.of( 129 | "shittyauth", "changeMCPassword", 130 | ActionValue.object() 131 | .put("account", ActionValue.string(acc.getID())) 132 | .put("password", ActionValue.elementValue(passwordField)) 133 | ) 134 | .onSuccess(ReloadPageAction.delayed(100)))) 135 | .create()); 136 | grp.addElement(actions); 137 | 138 | // TODO: this should be in the main WIAPI users page, not here 139 | PasswordField wiapiPasswordField = PasswordField.builder() 140 | .placeholder("Password") 141 | .create(); 142 | actions.addElement(wiapiPasswordField); 143 | 144 | actions.addElement(Button.builder() 145 | .text("Change WIAPI password") 146 | .onClick(ConfirmAction.of(SendJSAction.of( 147 | "shittyauth", "changeWIAPIPassword", 148 | ActionValue.object() 149 | .put("account", ActionValue.string(acc.getID())) 150 | .put("password", ActionValue.elementValue(wiapiPasswordField)) 151 | ) 152 | .onSuccess(ReloadPageAction.delayed(100)))) 153 | .create()); 154 | 155 | grp.addElement(Button.builder() 156 | .text("Delete account") 157 | .width("auto") 158 | .align(Align.LEFT_CENTER) 159 | .withLayoutOptions(DefaultLayoutOption.FULL_WIDTH) 160 | .onClick(ConfirmAction.of(SendJSAction.of("webinterface", "deleteAccount", ActionValue.object().put("account", ActionValue.string(acc.getID()))).onSuccess(ReloadPageAction.delayed(100)))) 161 | .create()); 162 | grp.addElement(new VerticalSpacer("30px")); 163 | 164 | els.add(grp); 165 | } 166 | }); 167 | 168 | addSection(sc); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/CreateAccountPage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page; 2 | 3 | import me.mrletsplay.shittyauth.ShittyAuth; 4 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 5 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 6 | import me.mrletsplay.webinterfaceapi.auth.Account; 7 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 8 | import me.mrletsplay.webinterfaceapi.page.Page; 9 | import me.mrletsplay.webinterfaceapi.page.PageSection; 10 | import me.mrletsplay.webinterfaceapi.page.action.RedirectAction; 11 | import me.mrletsplay.webinterfaceapi.page.action.SendJSAction; 12 | import me.mrletsplay.webinterfaceapi.page.action.value.ActionValue; 13 | import me.mrletsplay.webinterfaceapi.page.element.Button; 14 | import me.mrletsplay.webinterfaceapi.page.element.InputField; 15 | import me.mrletsplay.webinterfaceapi.page.element.TitleText; 16 | import me.mrletsplay.webinterfaceapi.page.element.layout.Grid; 17 | import me.mrletsplay.webinterfaceapi.session.Session; 18 | 19 | public class CreateAccountPage extends Page { 20 | 21 | public static final String PATH = "/shittyauth/create"; 22 | 23 | public CreateAccountPage() { 24 | super("Create Account", PATH, true); 25 | 26 | PageSection s = new PageSection(); 27 | s.setSlimLayout(true); 28 | addSection(s); 29 | 30 | s.addTitle("Create your Minecraft account"); 31 | s.setGrid(new Grid().setColumns("25fr", "75fr")); 32 | 33 | s.addElement(TitleText.builder().text("Username").leftboundText().create()); 34 | InputField username = InputField.builder().placeholder("Minecraft Username").create(); 35 | username.setMinLength(3); 36 | username.setMaxLength(16); 37 | s.addElement(username); 38 | 39 | s.addElement(TitleText.builder().text("Password").leftboundText().create()); 40 | InputField password = InputField.builder().placeholder("Minecraft Password").create(); 41 | s.addElement(password); 42 | 43 | s.addElement(Button.builder() 44 | .text("Create") 45 | .fullWidth() 46 | .onClick(SendJSAction.of("shittyauth", "createAccount", ActionValue.object() 47 | .put("username", ActionValue.elementValue(username)) 48 | .put("password", ActionValue.elementValue(password))) 49 | .onSuccess(RedirectAction.to(AccountPage.PATH))) 50 | .create()); 51 | } 52 | 53 | @Override 54 | public void createContent() { 55 | if(!Session.requireSession()) return; 56 | 57 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 58 | Account acc = Session.getCurrentSession().getAccount(); 59 | AccountConnection mcCon = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 60 | if(mcCon != null) { 61 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.FOUND_302); 62 | ctx.getServerHeader().getFields().set("Location", AccountPage.PATH); 63 | return; 64 | } 65 | 66 | super.createContent(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/SettingsPage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page; 2 | 3 | import me.mrletsplay.shittyauth.ShittyAuth; 4 | import me.mrletsplay.shittyauth.config.ShittyAuthSettings; 5 | import me.mrletsplay.webinterfaceapi.DefaultPermissions; 6 | import me.mrletsplay.webinterfaceapi.page.Page; 7 | import me.mrletsplay.webinterfaceapi.page.PageSection; 8 | import me.mrletsplay.webinterfaceapi.page.element.SettingsPane; 9 | 10 | public class SettingsPage extends Page { 11 | 12 | public static final String PATH = "/shittyauth/settings"; 13 | 14 | public SettingsPage() { 15 | super("Settings", PATH, DefaultPermissions.SETTINGS); 16 | setIcon("mdi:cog"); 17 | 18 | PageSection sc2 = new PageSection(); 19 | sc2.setSlimLayout(true); 20 | sc2.addTitle("Settings"); 21 | sc2.addElement(new SettingsPane(() -> ShittyAuth.config, ShittyAuthSettings.INSTANCE.getSettingsCategories(), "shittyauth", "setSetting")); 22 | 23 | addSection(sc2); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/UserCapeDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | import me.mrletsplay.shittyauth.ShittyAuth; 7 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 8 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 9 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 10 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 11 | import me.mrletsplay.webinterfaceapi.Webinterface; 12 | import me.mrletsplay.webinterfaceapi.auth.Account; 13 | 14 | public class UserCapeDocument implements HttpDocument { 15 | 16 | public static final UserCapeDocument INSTANCE = new UserCapeDocument(); 17 | 18 | @Override 19 | public void createContent() { 20 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 21 | String accID = ctx.getPathParameters().get("uuid"); 22 | if(accID.contains("_")) accID = accID.substring(0, accID.indexOf("_")); 23 | Account acc = Webinterface.getAccountStorage().getAccountByConnectionSpecificID(ShittyAuth.ACCOUNT_CONNECTION_NAME, accID); 24 | if(acc == null) { 25 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NOT_FOUND_404); 26 | ctx.getServerHeader().setContent(MimeType.TEXT, "404 Not Found".getBytes(StandardCharsets.UTF_8)); 27 | return; 28 | } 29 | 30 | try { 31 | byte[] bytes = ShittyAuth.loadUserCapeRaw(accID); 32 | ctx.getServerHeader().setContent(MimeType.PNG, bytes); 33 | } catch (IOException e) { 34 | Webinterface.getLogger().error("Failed to load cape", e); 35 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.INTERNAL_SERVER_ERROR_500); 36 | ctx.getServerHeader().setContent(MimeType.TEXT, "500 Internal Server error".getBytes(StandardCharsets.UTF_8)); 37 | return; 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/UserSkinDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | import me.mrletsplay.shittyauth.ShittyAuth; 7 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 8 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 9 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 10 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 11 | import me.mrletsplay.webinterfaceapi.Webinterface; 12 | import me.mrletsplay.webinterfaceapi.auth.Account; 13 | 14 | public class UserSkinDocument implements HttpDocument { 15 | 16 | public static final UserSkinDocument INSTANCE = new UserSkinDocument(); 17 | 18 | @Override 19 | public void createContent() { 20 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 21 | String accID = ctx.getPathParameters().get("uuid"); 22 | if(accID.contains("_")) accID = accID.substring(0, accID.indexOf("_")); 23 | Account acc = Webinterface.getAccountStorage().getAccountByConnectionSpecificID(ShittyAuth.ACCOUNT_CONNECTION_NAME, accID); 24 | if(acc == null) { 25 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NOT_FOUND_404); 26 | ctx.getServerHeader().setContent(MimeType.TEXT, "404 Not Found".getBytes(StandardCharsets.UTF_8)); 27 | return; 28 | } 29 | 30 | try { 31 | byte[] bytes = ShittyAuth.loadUserSkinRaw(accID); 32 | ctx.getServerHeader().setContent(MimeType.PNG, bytes); 33 | ctx.getServerHeader().getFields().set("Content-Type", "image/png"); 34 | } catch (IOException e) { 35 | Webinterface.getLogger().error("Failed to load skin", e); 36 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.INTERNAL_SERVER_ERROR_500); 37 | ctx.getServerHeader().setContent(MimeType.TEXT, "500 Internal Server error".getBytes(StandardCharsets.UTF_8)); 38 | return; 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/authlibinjector/AuthLibInjectorDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.authlibinjector; 2 | 3 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 4 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 5 | import me.mrletsplay.webinterfaceapi.Webinterface; 6 | 7 | public class AuthLibInjectorDocument implements HttpDocument { 8 | 9 | @Override 10 | public void createContent() { 11 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 12 | String page = ctx.getPathParameters().get("page"); 13 | HttpDocument doc = Webinterface.getDocumentProvider().get(ctx.getClientHeader().getMethod(), "/" + page); 14 | if(doc == null) { 15 | Webinterface.getDocumentProvider().getNotFoundDocument().createContent(); 16 | return; 17 | } 18 | 19 | doc.createContent(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/authlibinjector/AuthLibInjectorMetadataDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.authlibinjector; 2 | 3 | import java.net.URI; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | import me.mrletsplay.mrcore.json.JSONArray; 7 | import me.mrletsplay.mrcore.json.JSONObject; 8 | import me.mrletsplay.shittyauth.ShittyAuth; 9 | import me.mrletsplay.shittyauth.util.CryptoHelper; 10 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 11 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 12 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 13 | import me.mrletsplay.webinterfaceapi.Webinterface; 14 | import me.mrletsplay.webinterfaceapi.config.DefaultSettings; 15 | 16 | public class AuthLibInjectorMetadataDocument implements HttpDocument { 17 | 18 | @Override 19 | public void createContent() { 20 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 21 | 22 | JSONObject obj = new JSONObject(); 23 | JSONObject meta = new JSONObject(); 24 | meta.put("implementationName", "ShittyAuthServer"); 25 | meta.put("implementationVersion", "69.420"); 26 | meta.put("serverName", "ShittyAuthServer"); 27 | JSONObject links = new JSONObject(); 28 | links.put("homepage", Webinterface.getConfig().getSetting(DefaultSettings.HTTP_BASE_URL)); 29 | links.put("register", Webinterface.getConfig().getSetting(DefaultSettings.HTTP_BASE_URL)); 30 | meta.put("links", links); 31 | 32 | meta.put("feature.legacy_skin_api", true); 33 | meta.put("feature.no_mojang_namespace", true); 34 | meta.put("feature.enable_profile_key", true); 35 | meta.put("feature.username_check", true); 36 | obj.put("meta", meta); 37 | 38 | JSONArray skinDomains = new JSONArray(); 39 | skinDomains.add(URI.create(Webinterface.getConfig().getSetting(DefaultSettings.HTTP_BASE_URL)).getHost()); 40 | obj.put("skinDomains", skinDomains); 41 | 42 | obj.put("signaturePublicKey", CryptoHelper.encodeRSAPublicKey(ShittyAuth.publicKey)); 43 | ctx.getServerHeader().setContent(MimeType.JSON, obj.toString().getBytes(StandardCharsets.UTF_8)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/legacy/LegacyCheckServerDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.legacy; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import me.mrletsplay.shittyauth.ShittyAuth; 6 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 7 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 8 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 9 | import me.mrletsplay.simplehttpserver.http.request.urlencoded.UrlEncoded; 10 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 11 | import me.mrletsplay.webinterfaceapi.auth.Account; 12 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 13 | 14 | public class LegacyCheckServerDocument implements HttpDocument { 15 | 16 | @Override 17 | public void createContent() { 18 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 19 | UrlEncoded query = ctx.getClientHeader().getPath().getQuery(); 20 | 21 | if(!query.has("user") || !query.has("serverId")) { 22 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.BAD_REQUEST_400); 23 | ctx.getServerHeader().setContent(MimeType.TEXT, "Bad Request".getBytes(StandardCharsets.UTF_8)); 24 | return; 25 | } 26 | 27 | String accName = query.getFirst("user"); 28 | String serverId = query.getFirst("serverId"); 29 | 30 | Account acc = ShittyAuth.getAccountByUsername(accName); 31 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 32 | String joinedServer = ShittyAuth.userServers.get(con.getUserID()); 33 | if(joinedServer == null || !joinedServer.equals(serverId)) { 34 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.UNAUTHORIZED_401); 35 | return; 36 | } 37 | 38 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.OK_200); 39 | ctx.getServerHeader().setContent(MimeType.TEXT, "YES".getBytes(StandardCharsets.UTF_8)); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/legacy/LegacyJoinServerDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.legacy; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import me.mrletsplay.shittyauth.ShittyAuth; 6 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 7 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 8 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 9 | import me.mrletsplay.simplehttpserver.http.request.urlencoded.UrlEncoded; 10 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 11 | import me.mrletsplay.webinterfaceapi.auth.Account; 12 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 13 | 14 | public class LegacyJoinServerDocument implements HttpDocument { 15 | 16 | @Override 17 | public void createContent() { 18 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 19 | UrlEncoded query = ctx.getClientHeader().getPath().getQuery(); 20 | 21 | if(!query.has("sessionId") || !query.has("user") || !query.has("serverId")) { 22 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.BAD_REQUEST_400); 23 | ctx.getServerHeader().setContent(MimeType.TEXT, "Bad Request".getBytes(StandardCharsets.UTF_8)); 24 | return; 25 | } 26 | 27 | String accessToken = query.getFirst("sessionId"); 28 | String accName = query.getFirst("user"); 29 | String serverId = query.getFirst("serverId"); 30 | 31 | String accID = ShittyAuth.tokenStorage.getAccountID(accessToken); 32 | 33 | if(accID == null) { 34 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.UNAUTHORIZED_401); 35 | return; 36 | } 37 | 38 | Account acc = ShittyAuth.getAccountByUsername(accName); 39 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 40 | if(!con.getUserID().equals(accID)) { 41 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.UNAUTHORIZED_401); 42 | return; 43 | } 44 | 45 | ShittyAuth.userServers.put(con.getUserID(), serverId); 46 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.OK_200); 47 | ctx.getServerHeader().setContent(MimeType.TEXT, "ok".getBytes(StandardCharsets.UTF_8)); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/legacy/LegacyUserCapeDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.legacy; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | import me.mrletsplay.shittyauth.ShittyAuth; 7 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 8 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 9 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 10 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 11 | import me.mrletsplay.webinterfaceapi.Webinterface; 12 | import me.mrletsplay.webinterfaceapi.auth.Account; 13 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 14 | 15 | public class LegacyUserCapeDocument implements HttpDocument { 16 | 17 | public static final String PATH_PREFIX = "/MinecraftCloaks/"; 18 | public static final LegacyUserCapeDocument INSTANCE = new LegacyUserCapeDocument(); 19 | 20 | @Override 21 | public void createContent() { 22 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 23 | String username = ctx.getPathParameters().get("name"); 24 | if(username.endsWith(".png")) username = username.substring(0, username.length() - ".png".length()); 25 | Account acc = ShittyAuth.getAccountByUsername(username); 26 | if(acc == null) { 27 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NOT_FOUND_404); 28 | ctx.getServerHeader().setContent(MimeType.TEXT, "404 Not Found".getBytes(StandardCharsets.UTF_8)); 29 | return; 30 | } 31 | 32 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 33 | 34 | try { 35 | byte[] bytes = ShittyAuth.loadUserCapeRaw(con.getUserID()); 36 | ctx.getServerHeader().setContent(bytes); 37 | ctx.getServerHeader().getFields().set("Content-Type", "image/png"); 38 | } catch (IOException e) { 39 | Webinterface.getLogger().error("Failed to load cape", e); 40 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.INTERNAL_SERVER_ERROR_500); 41 | ctx.getServerHeader().setContent(MimeType.TEXT, "500 Internal Server error".getBytes(StandardCharsets.UTF_8)); 42 | return; 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/legacy/LegacyUserSkinDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.legacy; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | 6 | import me.mrletsplay.shittyauth.ShittyAuth; 7 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 8 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 9 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 10 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 11 | import me.mrletsplay.webinterfaceapi.Webinterface; 12 | import me.mrletsplay.webinterfaceapi.auth.Account; 13 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 14 | 15 | public class LegacyUserSkinDocument implements HttpDocument { 16 | 17 | public static final String PATH_PREFIX = "/MinecraftSkins/"; 18 | public static final LegacyUserSkinDocument INSTANCE = new LegacyUserSkinDocument(); 19 | 20 | @Override 21 | public void createContent() { 22 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 23 | String username = ctx.getPathParameters().get("name"); 24 | if(username.endsWith(".png")) username = username.substring(0, username.length() - ".png".length()); 25 | Account acc = ShittyAuth.getAccountByUsername(username); 26 | if(acc == null) { 27 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NOT_FOUND_404); 28 | ctx.getServerHeader().setContent(MimeType.TEXT, "404 Not Found".getBytes(StandardCharsets.UTF_8)); 29 | return; 30 | } 31 | 32 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 33 | 34 | try { 35 | byte[] bytes = ShittyAuth.loadUserSkinRaw(con.getUserID()); 36 | ctx.getServerHeader().setContent(bytes); 37 | ctx.getServerHeader().getFields().set("Content-Type", "image/png"); 38 | } catch (IOException e) { 39 | Webinterface.getLogger().error("Failed to load skin", e); 40 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.INTERNAL_SERVER_ERROR_500); 41 | ctx.getServerHeader().setContent(MimeType.TEXT, "500 Internal Server error".getBytes(StandardCharsets.UTF_8)); 42 | return; 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/services/PlayerAttributesDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.services; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import me.mrletsplay.mrcore.json.JSONObject; 6 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 7 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 8 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 9 | 10 | public class PlayerAttributesDocument implements HttpDocument { 11 | 12 | @Override 13 | public void createContent() { 14 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 15 | // String auth = ctx.getClientHeader().getFields().getFieldValue("Authorization"); TODO: ignored for now 16 | 17 | JSONObject obj = new JSONObject(); 18 | JSONObject privileges = new JSONObject(); 19 | JSONObject enabled = new JSONObject(); 20 | enabled.set("enabled", true); 21 | JSONObject disabled = new JSONObject(); 22 | disabled.set("enabled", false); 23 | privileges.set("onlineChat", enabled); 24 | privileges.set("multiplayerServer", enabled); 25 | privileges.set("multiplayerRealms", enabled); 26 | privileges.set("telemetry", disabled); 27 | obj.put("privileges", privileges); 28 | 29 | JSONObject prof = new JSONObject(); 30 | prof.put("profanityFilterOn", false); 31 | obj.put("profanityFilterPreferences", prof); 32 | ctx.getServerHeader().setContent(MimeType.JSON, obj.toString().getBytes(StandardCharsets.UTF_8)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/services/PlayerCertificatesDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.services; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.DataOutputStream; 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | import java.security.PublicKey; 8 | import java.security.Signature; 9 | import java.time.Instant; 10 | import java.time.temporal.ChronoUnit; 11 | import java.util.Base64; 12 | import java.util.UUID; 13 | 14 | import me.mrletsplay.mrcore.json.JSONObject; 15 | import me.mrletsplay.shittyauth.ShittyAuth; 16 | import me.mrletsplay.shittyauth.auth.StoredAccessToken; 17 | import me.mrletsplay.shittyauth.user.UserData; 18 | import me.mrletsplay.shittyauth.util.CryptoHelper; 19 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 20 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 21 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 22 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 23 | 24 | public class PlayerCertificatesDocument implements HttpDocument { 25 | 26 | @Override 27 | public void createContent() { 28 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 29 | String auth = ctx.getClientHeader().getFields().getFirst("Authorization"); 30 | if(!auth.startsWith("Bearer ")) { 31 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.ACCESS_DENIED_403); 32 | return; 33 | } 34 | 35 | auth = auth.substring("Bearer ".length()); 36 | StoredAccessToken tok = ShittyAuth.tokenStorage.getStoredToken(auth); 37 | if(tok == null) { 38 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.ACCESS_DENIED_403); 39 | return; 40 | } 41 | 42 | Instant expAt = Instant.now().plus(7, ChronoUnit.DAYS); // TODO: Use correct timestamps 43 | UserData userData = ShittyAuth.dataStorage.getUserData(tok.getAccountID()); 44 | 45 | JSONObject obj = new JSONObject(); 46 | JSONObject keyPair = new JSONObject(); 47 | keyPair.put("privateKey", CryptoHelper.encodeRSAPrivateKey(userData.getPrivateKey())); 48 | keyPair.put("publicKey", CryptoHelper.encodeRSAPublicKey(userData.getPublicKey())); 49 | obj.put("keyPair", keyPair); 50 | 51 | try { 52 | Signature sig = Signature.getInstance("SHA1withRSA"); 53 | sig.initSign(ShittyAuth.privateKey); 54 | sig.update(getSignaturePayload(UUID.fromString(tok.getAccountID()), expAt, userData.getPublicKey())); 55 | byte[] sign = sig.sign(); 56 | // obj.put("publicKeySignature", Base64.getEncoder().encodeToString(sign)); TODO: v1 signature? 57 | obj.put("publicKeySignatureV2", Base64.getEncoder().encodeToString(sign)); 58 | }catch (Exception e) { 59 | e.printStackTrace(); 60 | } 61 | 62 | obj.put("refreshedAfter", Instant.now().plus(7, ChronoUnit.DAYS).toString()); 63 | obj.put("expiresAt", expAt.toString()); 64 | 65 | ctx.getServerHeader().setContent(MimeType.JSON, obj.toString().getBytes(StandardCharsets.UTF_8)); 66 | } 67 | 68 | private byte[] getSignaturePayload(UUID playerUUID, Instant expiresAt, PublicKey key) { 69 | ByteArrayOutputStream bOut = new ByteArrayOutputStream(); 70 | DataOutputStream dOut = new DataOutputStream(bOut); 71 | try { 72 | dOut.writeLong(playerUUID.getMostSignificantBits()); 73 | dOut.writeLong(playerUUID.getLeastSignificantBits()); 74 | dOut.writeLong(expiresAt.toEpochMilli()); 75 | dOut.write(key.getEncoded()); 76 | } catch (IOException ignored) {} 77 | return bOut.toByteArray(); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/services/PlayerReportDocument.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.services; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import me.mrletsplay.mrcore.json.JSONObject; 6 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 7 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 8 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 9 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 10 | 11 | public class PlayerReportDocument implements HttpDocument { 12 | 13 | @Override 14 | public void createContent() { 15 | // TODO: currently not supported 16 | HttpRequestContext.getCurrentContext().getServerHeader().setStatusCode(HttpStatusCodes.BAD_REQUEST_400); 17 | JSONObject error = new JSONObject(); 18 | error.put("errorMessage", "Chat reporting is not supported by ShittyAuthServer"); 19 | HttpRequestContext.getCurrentContext().getServerHeader().setContent(MimeType.JSON, error.toString().getBytes(StandardCharsets.UTF_8)); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/shittyauth/ShittyAuthAPI.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.shittyauth; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.ByteArrayInputStream; 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.IOException; 7 | import java.util.Base64; 8 | 9 | import javax.imageio.ImageIO; 10 | 11 | import me.mrletsplay.mrcore.json.JSONArray; 12 | import me.mrletsplay.mrcore.json.JSONObject; 13 | import me.mrletsplay.mrcore.json.JSONType; 14 | import me.mrletsplay.shittyauth.ShittyAuth; 15 | import me.mrletsplay.shittyauth.config.ShittyAuthSettings; 16 | import me.mrletsplay.shittyauth.textures.SkinType; 17 | import me.mrletsplay.shittyauth.textures.TexturesHelper; 18 | import me.mrletsplay.shittyauth.user.UserData; 19 | import me.mrletsplay.shittyauth.util.DefaultTexture; 20 | import me.mrletsplay.shittyauth.util.InvalidSkinException; 21 | import me.mrletsplay.shittyauth.util.InvalidUsernameException; 22 | import me.mrletsplay.shittyauth.webinterface.ShittyAuthWIHandler; 23 | import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; 24 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 25 | import me.mrletsplay.simplehttpserver.http.endpoint.Endpoint; 26 | import me.mrletsplay.simplehttpserver.http.endpoint.EndpointCollection; 27 | import me.mrletsplay.simplehttpserver.http.endpoint.RequestParameter; 28 | import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; 29 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 30 | import me.mrletsplay.simplehttpserver.http.response.HttpResponse; 31 | import me.mrletsplay.simplehttpserver.http.response.JsonResponse; 32 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 33 | import me.mrletsplay.simplehttpserver.http.validation.JsonObjectValidator; 34 | import me.mrletsplay.simplehttpserver.http.validation.result.ValidationResult; 35 | import me.mrletsplay.webinterfaceapi.Webinterface; 36 | import me.mrletsplay.webinterfaceapi.auth.Account; 37 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 38 | import me.mrletsplay.webinterfaceapi.session.Session; 39 | 40 | public class ShittyAuthAPI implements EndpointCollection { 41 | 42 | private static final JsonObjectValidator LOGIN_VALIDATOR = new JsonObjectValidator() 43 | .require("username", JSONType.STRING) 44 | .require("password", JSONType.STRING); 45 | 46 | private static final JsonObjectValidator CHANGE_USERNAME_VALIDATOR = new JsonObjectValidator() 47 | .require("newUsername", JSONType.STRING); 48 | 49 | private static final JsonObjectValidator CHANGE_PASSWORD_VALIDATOR = new JsonObjectValidator() 50 | .require("oldPassword", JSONType.STRING) 51 | .require("newPassword", JSONType.STRING); 52 | 53 | private static final JsonObjectValidator UPDATE_SKIN_SETTINGS_VALIDATOR = new JsonObjectValidator() 54 | .optional("skinType", JSONType.STRING) 55 | .optional("capeEnabled", JSONType.BOOLEAN); 56 | 57 | private static final JsonObjectValidator CHANGE_RESET_SKIN_VALIDATOR = new JsonObjectValidator() 58 | .require("skin", JSONType.STRING); 59 | 60 | private static final JsonObjectValidator CHANGE_RESET_CAPE_VALIDATOR = new JsonObjectValidator() 61 | .require("cape", JSONType.STRING); 62 | 63 | static JSONObject error(String message) { 64 | JSONObject error = new JSONObject(); 65 | error.put("error", message); 66 | return error; 67 | } 68 | 69 | static Account requireAuthorization(HttpRequestContext ctx) { 70 | String sessionID = ctx.getClientHeader().getFields().getFirst("Authorization"); 71 | if(sessionID == null || !sessionID.startsWith("Bearer ")) { 72 | ctx.respond(HttpStatusCodes.ACCESS_DENIED_403, new JsonResponse(error("Unauthorized"))); 73 | return null; 74 | } 75 | 76 | Session session = Session.getSession(sessionID.substring("Bearer ".length())); 77 | if(session == null) { 78 | ctx.respond(HttpStatusCodes.ACCESS_DENIED_403, new JsonResponse(error("Unauthorized"))); 79 | return null; 80 | } 81 | 82 | Account account = session.getAccount(); 83 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 84 | if(connection == null) { 85 | ctx.respond(HttpStatusCodes.NOT_FOUND_404, new JsonResponse(error("No account"))); 86 | return null; 87 | } 88 | 89 | return account; 90 | } 91 | 92 | @Endpoint(method = HttpRequestMethod.GET, path = "/meta") 93 | public void version() { 94 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 95 | 96 | JSONObject meta = new JSONObject(); 97 | meta.put("version", 1); 98 | 99 | ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(meta)); 100 | } 101 | 102 | @Endpoint(method = HttpRequestMethod.POST, path = "/login") 103 | public void login() { 104 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 105 | 106 | JSONObject object; 107 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 108 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 109 | return; 110 | } 111 | 112 | ValidationResult result = LOGIN_VALIDATOR.validate(object); 113 | if(!result.isOk()) { 114 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 115 | return; 116 | } 117 | 118 | String username = object.getString("username"); 119 | String password = object.getString("password"); 120 | Account account = ShittyAuth.getAccountByUsername(username); 121 | if(account == null) { 122 | ctx.respond(HttpStatusCodes.ACCESS_DENIED_403, new JsonResponse(error("Invalid credentials"))); 123 | return; 124 | } 125 | 126 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 127 | if(!Webinterface.getCredentialsStorage().checkCredentials(ShittyAuth.ACCOUNT_CONNECTION_NAME, connection.getUserID(), password)) { 128 | ctx.respond(HttpStatusCodes.ACCESS_DENIED_403, new JsonResponse(error("Invalid credentials"))); 129 | return; 130 | } 131 | 132 | Session session = Session.startSession(account); 133 | JSONObject response = new JSONObject(); 134 | response.put("token", session.getSessionID()); 135 | ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(response)); 136 | } 137 | 138 | @Endpoint(method = HttpRequestMethod.POST, path = "/register") 139 | public void register() { 140 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 141 | 142 | if(!ShittyAuth.config.getSetting(ShittyAuthSettings.ALLOW_REGISTRATION)) { 143 | ctx.respond(HttpStatusCodes.ACCESS_DENIED_403, new JsonResponse(error("Creation of Minecraft accounts disabled"))); 144 | return; 145 | } 146 | 147 | JSONObject object; 148 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 149 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 150 | return; 151 | } 152 | 153 | ValidationResult result = LOGIN_VALIDATOR.validate(object); 154 | if(!result.isOk()) { 155 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 156 | return; 157 | } 158 | 159 | String username = object.getString("username"); 160 | String password = object.getString("password"); 161 | if(ShittyAuth.getAccountByUsername(username) != null) { 162 | ctx.respond(HttpStatusCodes.ACCESS_DENIED_403, new JsonResponse(error("A user with that username already exists"))); 163 | return; 164 | } 165 | 166 | try { 167 | Account account = ShittyAuth.createAccount(username, password); 168 | Session session = Session.startSession(account); 169 | JSONObject response = new JSONObject(); 170 | response.put("token", session.getSessionID()); 171 | ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(response)); 172 | } catch (InvalidUsernameException e) { 173 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Invalid username"))); 174 | } 175 | } 176 | 177 | @Endpoint(method = HttpRequestMethod.GET, path = "/me") 178 | public void me() { 179 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 180 | 181 | Account account = requireAuthorization(ctx); 182 | if(account == null) return; 183 | 184 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 185 | 186 | JSONObject accountInfo = new JSONObject(); 187 | accountInfo.put("id", connection.getUserID()); 188 | accountInfo.put("username", connection.getUserName()); 189 | accountInfo.put("isAdmin", account.hasPermission("*")); 190 | ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(accountInfo)); 191 | } 192 | 193 | @Endpoint(method = HttpRequestMethod.GET, path = "/skin") 194 | public void skin() { 195 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 196 | 197 | Account account = requireAuthorization(ctx); 198 | if(account == null) return; 199 | 200 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 201 | 202 | UserData data = ShittyAuth.dataStorage.getUserData(connection.getUserID()); 203 | 204 | JSONObject skinInfo = new JSONObject(); 205 | skinInfo.put("skinURL", String.format(TexturesHelper.getSkinBaseURL() + TexturesHelper.SKIN_PATH, connection.getUserID(), data.getSkinLastChanged())); 206 | skinInfo.put("skinType", data.getSkinType().name().toLowerCase()); 207 | skinInfo.put("capeURL", String.format(TexturesHelper.getSkinBaseURL() + TexturesHelper.CAPE_PATH, connection.getUserID(), data.getCapeLastChanged())); 208 | skinInfo.put("capeEnabled", data.hasCape()); 209 | ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(skinInfo)); 210 | } 211 | 212 | @Endpoint(method = HttpRequestMethod.POST, path = "/skin") 213 | public void changeSkin() { 214 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 215 | 216 | JSONObject object; 217 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 218 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 219 | return; 220 | } 221 | 222 | ValidationResult result = CHANGE_RESET_SKIN_VALIDATOR.validate(object); 223 | if(!result.isOk()) { 224 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 225 | return; 226 | } 227 | 228 | Account account = requireAuthorization(ctx); 229 | if(account == null) return; 230 | 231 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 232 | 233 | byte[] skinBytes; 234 | try { 235 | skinBytes = Base64.getDecoder().decode(object.getString("skin")); 236 | }catch(IllegalArgumentException e) { 237 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Invalid base64"))); 238 | return; 239 | } 240 | 241 | try { 242 | ShittyAuth.updateUserSkin(connection.getUserID(), ImageIO.read(new ByteArrayInputStream(skinBytes))); 243 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 244 | } catch (IOException e) { 245 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Invalid skin file"))); 246 | }catch(InvalidSkinException e) { 247 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error(e.getMessage()))); 248 | } 249 | } 250 | 251 | @Endpoint(method = HttpRequestMethod.POST, path = "/resetSkin") 252 | public void resetSkin() { 253 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 254 | 255 | JSONObject object; 256 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 257 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 258 | return; 259 | } 260 | 261 | ValidationResult result = CHANGE_RESET_SKIN_VALIDATOR.validate(object); 262 | if(!result.isOk()) { 263 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 264 | return; 265 | } 266 | 267 | Account account = requireAuthorization(ctx); 268 | if(account == null) return; 269 | 270 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 271 | 272 | DefaultTexture texture; 273 | boolean slim = false; 274 | try { 275 | texture = DefaultTexture.valueOf(object.getString("skin").toUpperCase()); 276 | if(!DefaultTexture.getSkins().contains(texture) && !(slim = DefaultTexture.getSlimSkins().contains(texture))) { 277 | throw new IllegalArgumentException(); 278 | } 279 | }catch(IllegalArgumentException e) { 280 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Not a valid skin"))); 281 | return; 282 | } 283 | 284 | try { 285 | ShittyAuth.updateUserSkin(connection.getUserID(), texture); 286 | UserData d = ShittyAuth.dataStorage.getUserData(connection.getUserID()); 287 | d.setSkinType(slim ? SkinType.ALEX : SkinType.STEVE); 288 | ShittyAuth.dataStorage.updateUserData(connection.getUserID(), d); 289 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 290 | }catch(IOException e) { 291 | ctx.respond(HttpStatusCodes.INTERNAL_SERVER_ERROR_500, new JsonResponse(error("Failed to update skin"))); 292 | return; 293 | } 294 | } 295 | 296 | @Endpoint(method = HttpRequestMethod.POST, path = "/cape") 297 | public void changeCape() { 298 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 299 | 300 | JSONObject object; 301 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 302 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 303 | return; 304 | } 305 | 306 | ValidationResult result = CHANGE_RESET_CAPE_VALIDATOR.validate(object); 307 | if(!result.isOk()) { 308 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 309 | return; 310 | } 311 | 312 | Account account = requireAuthorization(ctx); 313 | if(account == null) return; 314 | 315 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 316 | 317 | byte[] skinBytes; 318 | try { 319 | skinBytes = Base64.getDecoder().decode(object.getString("cape")); 320 | }catch(IllegalArgumentException e) { 321 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Invalid base64"))); 322 | return; 323 | } 324 | 325 | try { 326 | ShittyAuth.updateUserCape(connection.getUserID(), ImageIO.read(new ByteArrayInputStream(skinBytes))); 327 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 328 | } catch (IOException e) { 329 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Invalid cape file"))); 330 | }catch(InvalidSkinException e) { 331 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error(e.getMessage()))); 332 | } 333 | } 334 | 335 | @Endpoint(method = HttpRequestMethod.POST, path = "/resetCape") 336 | public void resetCape() { 337 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 338 | 339 | JSONObject object; 340 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 341 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 342 | return; 343 | } 344 | 345 | ValidationResult result = CHANGE_RESET_CAPE_VALIDATOR.validate(object); 346 | if(!result.isOk()) { 347 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 348 | return; 349 | } 350 | 351 | Account account = requireAuthorization(ctx); 352 | if(account == null) return; 353 | 354 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 355 | 356 | DefaultTexture texture; 357 | try { 358 | texture = DefaultTexture.valueOf(object.getString("cape").toUpperCase()); 359 | if(!DefaultTexture.getCapes().contains(texture)) { 360 | throw new IllegalArgumentException(); 361 | } 362 | }catch(IllegalArgumentException e) { 363 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Not a valid cape"))); 364 | return; 365 | } 366 | 367 | try { 368 | ShittyAuth.updateUserCape(connection.getUserID(), texture); 369 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 370 | }catch(IOException e) { 371 | ctx.respond(HttpStatusCodes.INTERNAL_SERVER_ERROR_500, new JsonResponse(error("Failed to update cape"))); 372 | return; 373 | } 374 | } 375 | 376 | @Endpoint(method = HttpRequestMethod.POST, path = "/changeUsername") 377 | public void changeUsername() { 378 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 379 | 380 | JSONObject object; 381 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 382 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 383 | return; 384 | } 385 | 386 | ValidationResult result = CHANGE_USERNAME_VALIDATOR.validate(object); 387 | if(!result.isOk()) { 388 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 389 | return; 390 | } 391 | 392 | Account account = requireAuthorization(ctx); 393 | if(account == null) return; 394 | 395 | String newUsername = object.getString("newUsername"); 396 | if(!ShittyAuthWIHandler.USERNAME_PATTERN.matcher(newUsername).matches()) { 397 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Invalid username"))); 398 | return; 399 | } 400 | 401 | if(ShittyAuth.getAccountByUsername(newUsername) != null) { 402 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Username is already taken"))); 403 | return; 404 | } 405 | 406 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 407 | AccountConnection newConnection = new AccountConnection(connection.getConnectionName(), connection.getUserID(), newUsername, connection.getUserEmail(), connection.getUserAvatar()); 408 | account.removeConnection(connection); 409 | account.addConnection(newConnection); 410 | 411 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 412 | } 413 | 414 | @Endpoint(method = HttpRequestMethod.POST, path = "/changePassword") 415 | public void changePassword() { 416 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 417 | 418 | JSONObject object; 419 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 420 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 421 | return; 422 | } 423 | 424 | ValidationResult result = CHANGE_PASSWORD_VALIDATOR.validate(object); 425 | if(!result.isOk()) { 426 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 427 | return; 428 | } 429 | 430 | Account account = requireAuthorization(ctx); 431 | if(account == null) return; 432 | 433 | String oldPassword = object.getString("oldPassword"); 434 | String newPassword = object.getString("newPassword"); 435 | 436 | if(newPassword.isEmpty()) { 437 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("New password can't be empty"))); 438 | return; 439 | } 440 | 441 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 442 | if(!Webinterface.getCredentialsStorage().checkCredentials(ShittyAuth.ACCOUNT_CONNECTION_NAME, connection.getUserID(), oldPassword)) { 443 | ctx.respond(HttpStatusCodes.ACCESS_DENIED_403, new JsonResponse(error("Invalid credentials"))); 444 | return; 445 | } 446 | 447 | Webinterface.getCredentialsStorage().storeCredentials(ShittyAuth.ACCOUNT_CONNECTION_NAME, connection.getUserID(), newPassword); 448 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 449 | } 450 | 451 | @Endpoint(method = HttpRequestMethod.PUT, path = "/updateSkinSettings") 452 | public void updateSkinSettings() { 453 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 454 | 455 | JSONObject object; 456 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 457 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 458 | return; 459 | } 460 | 461 | ValidationResult result = UPDATE_SKIN_SETTINGS_VALIDATOR.validate(object); 462 | if(!result.isOk()) { 463 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 464 | return; 465 | } 466 | 467 | Account account = requireAuthorization(ctx); 468 | if(account == null) return; 469 | 470 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 471 | 472 | SkinType skinType = null; 473 | 474 | if(object.has("skinType")) { 475 | try { 476 | skinType = SkinType.valueOf(object.getString("skinType").toUpperCase()); 477 | }catch(IllegalArgumentException e) { 478 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Invalid skin type"))); 479 | return; 480 | } 481 | } 482 | 483 | Boolean capeEnabled = object.optBoolean("capeEnabled").orElse(null); 484 | 485 | if(skinType != null || capeEnabled != null) { 486 | UserData data = ShittyAuth.dataStorage.getUserData(connection.getUserID()); 487 | if(skinType != null) data.setSkinType(skinType); 488 | if(capeEnabled != null) data.setHasCape(capeEnabled); 489 | ShittyAuth.dataStorage.updateUserData(connection.getUserID(), data); 490 | } 491 | 492 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 493 | } 494 | 495 | @Endpoint(method = HttpRequestMethod.GET, path = "/avatar/{userID}", pathPattern = true) 496 | public void avatar(@RequestParameter("userID") String userID) { 497 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 498 | 499 | Account account = Webinterface.getAccountStorage().getAccountByConnectionSpecificID(ShittyAuth.ACCOUNT_CONNECTION_NAME, userID); 500 | if(account == null) { 501 | ctx.respond(HttpStatusCodes.NOT_FOUND_404, new JsonResponse(error("Account not found"))); 502 | return; 503 | } 504 | 505 | byte[] headBytes; 506 | try { 507 | BufferedImage skinImage = ShittyAuth.loadUserSkin(userID); 508 | BufferedImage headImage = skinImage.getSubimage(8, 8, 8, 8); 509 | ByteArrayOutputStream bOut = new ByteArrayOutputStream(); 510 | ImageIO.write(headImage, "PNG", bOut); 511 | headBytes = bOut.toByteArray(); 512 | } catch (IOException e) { 513 | ctx.respond(HttpStatusCodes.INTERNAL_SERVER_ERROR_500, new JsonResponse(error("Failed to load skin"))); 514 | return; 515 | } 516 | 517 | ctx.respond(HttpStatusCodes.OK_200, new HttpResponse() { 518 | 519 | @Override 520 | public MimeType getContentType() { 521 | return MimeType.PNG; 522 | } 523 | 524 | @Override 525 | public byte[] getContent() { 526 | return headBytes; 527 | } 528 | }); 529 | } 530 | 531 | @Endpoint(method = HttpRequestMethod.GET, path = "/defaultSkins") 532 | public void defaultSkins() { 533 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 534 | 535 | JSONObject res = new JSONObject(); 536 | 537 | JSONArray skins = new JSONArray(); 538 | for(DefaultTexture t : DefaultTexture.getSkins()) { 539 | JSONObject skin = new JSONObject(); 540 | skin.put("id", t.name().toLowerCase()); 541 | skin.put("name", t.getName()); 542 | skin.put("url", t.getURL()); // TODO: provide url on ShittyAuthServer 543 | skins.add(skin); 544 | } 545 | res.put("skins", skins); 546 | 547 | JSONArray slimSkins = new JSONArray(); 548 | for(DefaultTexture t : DefaultTexture.getSlimSkins()) { 549 | JSONObject skin = new JSONObject(); 550 | skin.put("id", t.name().toLowerCase()); 551 | skin.put("name", t.getName()); 552 | skin.put("url", t.getURL()); // TODO: provide url on ShittyAuthServer 553 | slimSkins.add(skin); 554 | } 555 | res.put("slimSkins", slimSkins); 556 | 557 | ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(res)); 558 | } 559 | 560 | @Endpoint(method = HttpRequestMethod.GET, path = "/defaultCapes") 561 | public void defaultCapes() { 562 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 563 | 564 | JSONArray capes = new JSONArray(); 565 | for(DefaultTexture t : DefaultTexture.getCapes()) { 566 | JSONObject cape = new JSONObject(); 567 | cape.put("id", t.name().toLowerCase()); 568 | cape.put("name", t.getName()); 569 | cape.put("url", t.getURL()); // TODO: provide url on ShittyAuthServer 570 | capes.add(cape); 571 | } 572 | 573 | ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(capes)); 574 | } 575 | 576 | @Override 577 | public String getBasePath() { 578 | return "/api/shittyauth"; 579 | } 580 | 581 | } 582 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/shittyauth/ShittyAuthAdminAPI.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.shittyauth; 2 | 3 | import static me.mrletsplay.shittyauth.page.api.shittyauth.ShittyAuthAPI.error; 4 | import static me.mrletsplay.shittyauth.page.api.shittyauth.ShittyAuthAPI.requireAuthorization; 5 | 6 | import java.util.List; 7 | 8 | import me.mrletsplay.mrcore.json.JSONArray; 9 | import me.mrletsplay.mrcore.json.JSONObject; 10 | import me.mrletsplay.mrcore.json.JSONType; 11 | import me.mrletsplay.shittyauth.ShittyAuth; 12 | import me.mrletsplay.shittyauth.config.ShittyAuthSettings; 13 | import me.mrletsplay.shittyauth.webinterface.ShittyAuthWIHandler; 14 | import me.mrletsplay.simplehttpserver.http.HttpRequestMethod; 15 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 16 | import me.mrletsplay.simplehttpserver.http.endpoint.Endpoint; 17 | import me.mrletsplay.simplehttpserver.http.endpoint.EndpointCollection; 18 | import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; 19 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 20 | import me.mrletsplay.simplehttpserver.http.response.JsonResponse; 21 | import me.mrletsplay.simplehttpserver.http.validation.JsonObjectValidator; 22 | import me.mrletsplay.simplehttpserver.http.validation.result.ValidationResult; 23 | import me.mrletsplay.webinterfaceapi.Webinterface; 24 | import me.mrletsplay.webinterfaceapi.auth.Account; 25 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 26 | 27 | public class ShittyAuthAdminAPI implements EndpointCollection { 28 | 29 | private static final JsonObjectValidator CHANGE_USERNAME_VALIDATOR = new JsonObjectValidator() 30 | .require("userID", JSONType.STRING) 31 | .require("newUsername", JSONType.STRING); 32 | 33 | private static final JsonObjectValidator CHANGE_PASSWORD_VALIDATOR = new JsonObjectValidator() 34 | .require("userID", JSONType.STRING) 35 | .require("newPassword", JSONType.STRING); 36 | 37 | private static final JsonObjectValidator DELETE_ACCOUNT_VALIDATOR = new JsonObjectValidator() 38 | .require("userID", JSONType.STRING); 39 | 40 | private static final JsonObjectValidator CHANGE_ADMIN_VALIDATOR = new JsonObjectValidator() 41 | .require("userID", JSONType.STRING) 42 | .require("admin", JSONType.BOOLEAN); 43 | 44 | private static final JsonObjectValidator GLOBAL_SETTINGS_VALIDATOR = new JsonObjectValidator() 45 | .optional("allowRegistration", JSONType.BOOLEAN) 46 | .optional("authlibCompat", JSONType.BOOLEAN); 47 | 48 | private static Account requireAdmin(HttpRequestContext ctx) { 49 | Account account = requireAuthorization(ctx); 50 | if(account == null) return null; 51 | 52 | if(!account.hasPermission("*")) { 53 | ctx.respond(HttpStatusCodes.ACCESS_DENIED_403, new JsonResponse(error("Unauthorized"))); 54 | return null; 55 | } 56 | 57 | return account; 58 | } 59 | 60 | @Endpoint(method = HttpRequestMethod.GET, path = "/accounts") 61 | public void accounts() { 62 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 63 | 64 | Account account = requireAdmin(ctx); 65 | if(account == null) return; 66 | 67 | List allAccounts = Webinterface.getAccountStorage().getAccounts(); 68 | JSONArray accounts = new JSONArray(); 69 | for(Account a : allAccounts) { 70 | AccountConnection connection = a.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 71 | if(connection == null) continue; 72 | 73 | JSONObject obj = new JSONObject(); 74 | obj.put("id", connection.getUserID()); 75 | obj.put("username", connection.getUserName()); 76 | obj.put("isAdmin", a.hasPermission("*")); 77 | accounts.add(obj); 78 | } 79 | 80 | ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(accounts)); 81 | } 82 | 83 | @Endpoint(method = HttpRequestMethod.POST, path = "/changeUsername") 84 | public void changeUsername() { 85 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 86 | 87 | JSONObject object; 88 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 89 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 90 | return; 91 | } 92 | 93 | ValidationResult result = CHANGE_USERNAME_VALIDATOR.validate(object); 94 | if(!result.isOk()) { 95 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 96 | return; 97 | } 98 | 99 | String newUsername = object.getString("newUsername"); 100 | if(!ShittyAuthWIHandler.USERNAME_PATTERN.matcher(newUsername).matches()) { 101 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("New username is invalid"))); 102 | return; 103 | } 104 | 105 | Account account = requireAdmin(ctx); 106 | if(account == null) return; 107 | 108 | Account otherAccount = Webinterface.getAccountStorage().getAccountByConnectionSpecificID(ShittyAuth.ACCOUNT_CONNECTION_NAME, object.getString("userID")); 109 | if(otherAccount == null) { 110 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Account doesn't exist"))); 111 | return; 112 | } 113 | 114 | if(account.getID().equals(otherAccount.getID())) { 115 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Can't edit own account via admin API"))); 116 | return; 117 | } 118 | 119 | if(ShittyAuth.getAccountByUsername(newUsername) != null) { 120 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Username is already taken"))); 121 | return; 122 | } 123 | 124 | AccountConnection connection = otherAccount.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 125 | AccountConnection newConnection = new AccountConnection(connection.getConnectionName(), connection.getUserID(), newUsername, connection.getUserEmail(), connection.getUserAvatar()); 126 | otherAccount.removeConnection(connection); 127 | otherAccount.addConnection(newConnection); 128 | 129 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 130 | } 131 | 132 | @Endpoint(method = HttpRequestMethod.POST, path = "/changePassword") 133 | public void changePassword() { 134 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 135 | 136 | JSONObject object; 137 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 138 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 139 | return; 140 | } 141 | 142 | ValidationResult result = CHANGE_PASSWORD_VALIDATOR.validate(object); 143 | if(!result.isOk()) { 144 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 145 | return; 146 | } 147 | 148 | String newPassword = object.getString("newPassword"); 149 | if(newPassword.isEmpty()) { 150 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("New password can't be empty"))); 151 | return; 152 | } 153 | 154 | Account account = requireAdmin(ctx); 155 | if(account == null) return; 156 | 157 | String userID = object.getString("userID"); 158 | Account otherAccount = Webinterface.getAccountStorage().getAccountByConnectionSpecificID(ShittyAuth.ACCOUNT_CONNECTION_NAME, userID); 159 | if(otherAccount == null) { 160 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Account doesn't exist"))); 161 | return; 162 | } 163 | 164 | if(account.getID().equals(otherAccount.getID())) { 165 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Can't edit own account via admin API"))); 166 | return; 167 | } 168 | 169 | Webinterface.getCredentialsStorage().storeCredentials(ShittyAuth.ACCOUNT_CONNECTION_NAME, userID, newPassword); 170 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 171 | } 172 | 173 | @Endpoint(method = HttpRequestMethod.POST, path = "/deleteAccount") 174 | public void deleteAccount() { 175 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 176 | 177 | JSONObject object; 178 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 179 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 180 | return; 181 | } 182 | 183 | ValidationResult result = DELETE_ACCOUNT_VALIDATOR.validate(object); 184 | if(!result.isOk()) { 185 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 186 | return; 187 | } 188 | 189 | Account account = requireAdmin(ctx); 190 | if(account == null) return; 191 | 192 | Account otherAccount = Webinterface.getAccountStorage().getAccountByConnectionSpecificID(ShittyAuth.ACCOUNT_CONNECTION_NAME, object.getString("userID")); 193 | if(otherAccount == null) { 194 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Account doesn't exist"))); 195 | return; 196 | } 197 | 198 | if(account.getID().equals(otherAccount.getID())) { 199 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Can't edit own account via admin API"))); 200 | return; 201 | } 202 | 203 | Webinterface.getAccountStorage().deleteAccount(otherAccount.getID()); 204 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 205 | } 206 | 207 | @Endpoint(method = HttpRequestMethod.POST, path = "/changeAdmin") 208 | public void changeAdmin() { 209 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 210 | 211 | JSONObject object; 212 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 213 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 214 | return; 215 | } 216 | 217 | ValidationResult result = CHANGE_ADMIN_VALIDATOR.validate(object); 218 | if(!result.isOk()) { 219 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 220 | return; 221 | } 222 | 223 | Account account = requireAdmin(ctx); 224 | if(account == null) return; 225 | 226 | Account otherAccount = Webinterface.getAccountStorage().getAccountByConnectionSpecificID(ShittyAuth.ACCOUNT_CONNECTION_NAME, object.getString("userID")); 227 | if(otherAccount == null) { 228 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Account doesn't exist"))); 229 | return; 230 | } 231 | 232 | if(account.getID().equals(otherAccount.getID())) { 233 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Can't edit own account via admin API"))); 234 | return; 235 | } 236 | 237 | boolean admin = object.getBoolean("admin"); 238 | if(admin) { 239 | if(!otherAccount.hasPermission("*")) otherAccount.addPermission("*"); 240 | }else { 241 | otherAccount.removePermission("*"); 242 | } 243 | 244 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 245 | } 246 | 247 | @Endpoint(method = HttpRequestMethod.GET, path = "/globalSettings") 248 | public void globalSettings() { 249 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 250 | 251 | Account account = requireAdmin(ctx); 252 | if(account == null) return; 253 | 254 | JSONObject obj = new JSONObject(); 255 | obj.put("allowRegistration", ShittyAuth.config.getSetting(ShittyAuthSettings.ALLOW_REGISTRATION)); 256 | obj.put("authlibCompat", ShittyAuth.config.getSetting(ShittyAuthSettings.AUTHLIB_INJECTOR_COMPAT)); 257 | 258 | ctx.respond(HttpStatusCodes.OK_200, new JsonResponse(obj)); 259 | } 260 | 261 | @Endpoint(method = HttpRequestMethod.PATCH, path = "/updateGlobalSettings") 262 | public void updateGlobalSettings() { 263 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 264 | 265 | JSONObject object; 266 | if((object = ctx.expectContent(DefaultClientContentTypes.JSON_OBJECT)) == null){ 267 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, new JsonResponse(error("Bad JSON"))); 268 | return; 269 | } 270 | 271 | ValidationResult result = GLOBAL_SETTINGS_VALIDATOR.validate(object); 272 | if(!result.isOk()) { 273 | ctx.respond(HttpStatusCodes.BAD_REQUEST_400, result.asJsonResponse()); 274 | return; 275 | } 276 | 277 | Account account = requireAdmin(ctx); 278 | if(account == null) return; 279 | 280 | if(object.has("allowRegistration")) { 281 | boolean allowRegistration = object.getBoolean("allowRegistration"); 282 | ShittyAuth.config.setSetting(ShittyAuthSettings.ALLOW_REGISTRATION, allowRegistration); 283 | } 284 | 285 | if(object.has("authlibCompat")) { 286 | boolean authlibCompat = object.getBoolean("authlibCompat"); 287 | ShittyAuth.config.setSetting(ShittyAuthSettings.AUTHLIB_INJECTOR_COMPAT, authlibCompat); 288 | } 289 | 290 | ctx.respond(HttpStatusCodes.OK_200, JsonResponse.EMPTY_OBJECT); 291 | } 292 | 293 | @Override 294 | public String getBasePath() { 295 | return "/api/shittyauth/admin"; 296 | } 297 | 298 | } 299 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/yggdrasil/AuthenticatePage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.yggdrasil; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.UUID; 5 | 6 | import me.mrletsplay.mrcore.json.JSONArray; 7 | import me.mrletsplay.mrcore.json.JSONObject; 8 | import me.mrletsplay.mrcore.json.JSONParseException; 9 | import me.mrletsplay.shittyauth.ShittyAuth; 10 | import me.mrletsplay.shittyauth.auth.AccessToken; 11 | import me.mrletsplay.shittyauth.util.UUIDHelper; 12 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 13 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 14 | import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; 15 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 16 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 17 | import me.mrletsplay.webinterfaceapi.Webinterface; 18 | import me.mrletsplay.webinterfaceapi.auth.Account; 19 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 20 | 21 | public class AuthenticatePage implements HttpDocument { 22 | 23 | // https://wiki.vg/Authentication 24 | 25 | @Override 26 | public void createContent() { 27 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 28 | 29 | JSONObject obj; 30 | try { 31 | obj = ctx.getClientHeader().getPostData().getParsedAs(DefaultClientContentTypes.JSON_OBJECT); 32 | }catch(JSONParseException e) { 33 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.UNAUTHORIZED_401); 34 | JSONObject response = new JSONObject(); 35 | response.put("error", "ForbiddenOperationException"); 36 | response.put("errorMessage", "Forbidden"); 37 | ctx.getServerHeader().setContent(MimeType.JSON, response.toString().getBytes(StandardCharsets.UTF_8)); 38 | return; 39 | } 40 | 41 | if(!obj.has("username") || !obj.has("password")) { 42 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.UNAUTHORIZED_401); 43 | JSONObject response = new JSONObject(); 44 | response.put("error", "ForbiddenOperationException"); 45 | response.put("errorMessage", "Forbidden"); 46 | ctx.getServerHeader().setContent(MimeType.JSON, response.toString().getBytes(StandardCharsets.UTF_8)); 47 | return; 48 | } 49 | 50 | String 51 | username = obj.getString("username"), 52 | password = obj.getString("password"), 53 | clientToken = obj.optString("clientToken").orElse(null); 54 | 55 | Account acc = ShittyAuth.getAccountByUsername(username); 56 | AccountConnection con; 57 | if(acc == null 58 | || (con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME)) == null 59 | || !Webinterface.getCredentialsStorage().checkCredentials(ShittyAuth.ACCOUNT_CONNECTION_NAME, con.getUserID(), password)) { 60 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.UNAUTHORIZED_401); 61 | JSONObject response = new JSONObject(); 62 | response.put("error", "ForbiddenOperationException"); 63 | response.put("errorMessage", "Invalid credentials. Invalid username or password."); 64 | ctx.getServerHeader().setContent(MimeType.JSON, response.toString().getBytes(StandardCharsets.UTF_8)); 65 | return; 66 | } 67 | 68 | JSONObject response = new JSONObject(); 69 | 70 | if(obj.optBoolean("requestUser").orElse(false)) { 71 | JSONObject user = new JSONObject(); 72 | user.put("username", username); 73 | JSONArray properties = new JSONArray(); 74 | user.put("properties", properties); 75 | user.put("id", con.getUserID()); 76 | response.put("user", user); 77 | } 78 | 79 | JSONObject profile = new JSONObject(); 80 | profile.put("name", username); 81 | profile.put("id", UUIDHelper.toShortUUID(UUID.fromString(con.getUserID()))); 82 | 83 | JSONArray availableProfiles = new JSONArray(); 84 | availableProfiles.add(profile); 85 | response.put("availableProfiles", availableProfiles); 86 | 87 | response.put("selectedProfile", profile); 88 | 89 | AccessToken tok = ShittyAuth.tokenStorage.generateToken(con.getUserID(), clientToken); 90 | response.put("accessToken", tok.getAccessToken()); 91 | response.put("clientToken", tok.getClientToken()); 92 | 93 | ctx.getServerHeader().setContent(MimeType.JSON, response.toString().getBytes(StandardCharsets.UTF_8)); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/yggdrasil/HasJoinedPage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.yggdrasil; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.security.Signature; 5 | import java.util.Base64; 6 | import java.util.UUID; 7 | 8 | import me.mrletsplay.mrcore.json.JSONArray; 9 | import me.mrletsplay.mrcore.json.JSONObject; 10 | import me.mrletsplay.shittyauth.ShittyAuth; 11 | import me.mrletsplay.shittyauth.textures.TexturesHelper; 12 | import me.mrletsplay.shittyauth.user.UserData; 13 | import me.mrletsplay.shittyauth.util.UUIDHelper; 14 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 15 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 16 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 17 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 18 | import me.mrletsplay.webinterfaceapi.auth.Account; 19 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 20 | 21 | public class HasJoinedPage implements HttpDocument { 22 | 23 | @Override 24 | public void createContent() { 25 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 26 | 27 | String username = ctx.getClientHeader().getPath().getQuery().getFirst("username"); 28 | if(username == null) { 29 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NOT_FOUND_404); 30 | return; 31 | } 32 | 33 | Account acc = ShittyAuth.getAccountByUsername(username); 34 | if(acc == null) { 35 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NOT_FOUND_404); 36 | return; 37 | } 38 | 39 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 40 | 41 | String serverHash = ShittyAuth.userServers.get(con.getUserID()); 42 | String hash = ctx.getClientHeader().getPath().getQuery().getFirst("serverId"); 43 | if(hash == null || serverHash == null || !serverHash.equals(hash)) { 44 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NO_CONTENT_204); 45 | return; 46 | } 47 | 48 | ShittyAuth.userServers.remove(con.getUserID()); 49 | 50 | JSONObject obj = new JSONObject(); 51 | obj.put("id", UUIDHelper.toShortUUID(UUID.fromString(con.getUserID()))); 52 | obj.put("name", con.getUserName()); 53 | 54 | JSONArray a = new JSONArray(); 55 | JSONObject b = new JSONObject(); 56 | b.put("name", "textures"); 57 | 58 | JSONObject textures = new JSONObject(); 59 | textures.put("timestamp", System.currentTimeMillis()); 60 | textures.put("profileId", UUIDHelper.toShortUUID(UUID.fromString(con.getUserID()))); 61 | textures.put("profileName", con.getUserName()); 62 | 63 | UserData d = ShittyAuth.dataStorage.getUserData(con.getUserID()); 64 | textures.put("textures", TexturesHelper.getTexturesObject(con.getUserID(), d)); 65 | 66 | String b64Value = Base64.getEncoder().encodeToString(textures.toString().getBytes()); 67 | b.put("value", b64Value); 68 | 69 | try { 70 | Signature sig = Signature.getInstance("SHA1withRSA"); 71 | sig.initSign(ShittyAuth.privateKey); 72 | sig.update(b64Value.getBytes(StandardCharsets.UTF_8)); 73 | b.put("signature", Base64.getEncoder().encodeToString(sig.sign())); 74 | }catch (Exception e) { 75 | e.printStackTrace(); 76 | } 77 | a.add(b); 78 | obj.put("properties", a); 79 | 80 | ctx.getServerHeader().setContent(MimeType.JSON, obj.toString().getBytes(StandardCharsets.UTF_8)); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/yggdrasil/InvalidatePage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.yggdrasil; 2 | 3 | import me.mrletsplay.mrcore.json.JSONObject; 4 | import me.mrletsplay.shittyauth.ShittyAuth; 5 | import me.mrletsplay.shittyauth.auth.StoredAccessToken; 6 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 7 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 8 | import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; 9 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 10 | import me.mrletsplay.webinterfaceapi.Webinterface; 11 | import me.mrletsplay.webinterfaceapi.auth.Account; 12 | 13 | public class InvalidatePage implements HttpDocument { 14 | 15 | @Override 16 | public void createContent() { 17 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 18 | JSONObject obj = ctx.getClientHeader().getPostData().getParsedAs(DefaultClientContentTypes.JSON_OBJECT); 19 | String 20 | accessToken = obj.getString("accessToken"), 21 | clientToken = obj.optString("clientToken").orElse(null); 22 | 23 | // Check the token 24 | StoredAccessToken tok = ShittyAuth.tokenStorage.getStoredToken(accessToken); 25 | if(tok == null || (clientToken != null && !clientToken.equals(tok.getClientToken()))) { 26 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.ACCESS_DENIED_403); 27 | JSONObject err = new JSONObject(); 28 | err.put("error", "ForbiddenOperationException"); 29 | err.put("errorMessage", "Invalid token."); 30 | return; 31 | } 32 | 33 | Account acc = Webinterface.getAccountStorage().getAccountByID(tok.getAccountID()); 34 | if(acc == null) { 35 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.ACCESS_DENIED_403); 36 | JSONObject err = new JSONObject(); 37 | err.put("error", "ForbiddenOperationException"); 38 | err.put("errorMessage", "Invalid token."); 39 | return; 40 | } 41 | 42 | // Invalidate the token 43 | ShittyAuth.tokenStorage.removeToken(accessToken); 44 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NO_CONTENT_204); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/yggdrasil/JoinPage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.yggdrasil; 2 | 3 | import java.util.UUID; 4 | 5 | import me.mrletsplay.mrcore.json.JSONObject; 6 | import me.mrletsplay.shittyauth.ShittyAuth; 7 | import me.mrletsplay.shittyauth.util.UUIDHelper; 8 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 9 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 10 | import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; 11 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 12 | 13 | public class JoinPage implements HttpDocument { 14 | 15 | @Override 16 | public void createContent() { 17 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 18 | 19 | JSONObject authData = ctx.getClientHeader().getPostData().getParsedAs(DefaultClientContentTypes.JSON_OBJECT); 20 | String accessToken = authData.getString("accessToken"); 21 | 22 | String accID = ShittyAuth.tokenStorage.getAccountID(accessToken); 23 | String shortUUID = accID == null ? null : UUIDHelper.toShortUUID(UUID.fromString(accID)); 24 | if(accID == null || !shortUUID.equals(authData.getString("selectedProfile"))) { 25 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.UNAUTHORIZED_401); 26 | return; 27 | } 28 | 29 | ShittyAuth.userServers.put(accID, authData.getString("serverId")); 30 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NO_CONTENT_204); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/yggdrasil/ProfilePage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.yggdrasil; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.security.Signature; 5 | import java.util.Base64; 6 | import java.util.UUID; 7 | 8 | import me.mrletsplay.mrcore.json.JSONArray; 9 | import me.mrletsplay.mrcore.json.JSONObject; 10 | import me.mrletsplay.shittyauth.ShittyAuth; 11 | import me.mrletsplay.shittyauth.textures.TexturesHelper; 12 | import me.mrletsplay.shittyauth.user.UserData; 13 | import me.mrletsplay.shittyauth.util.UUIDHelper; 14 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 15 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 16 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 17 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 18 | import me.mrletsplay.webinterfaceapi.Webinterface; 19 | import me.mrletsplay.webinterfaceapi.auth.Account; 20 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 21 | 22 | public class ProfilePage implements HttpDocument { 23 | 24 | public static final ProfilePage INSTANCE = new ProfilePage(); 25 | 26 | @Override 27 | public void createContent() { 28 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 29 | String uuid = ctx.getPathParameters().get("uuid"); 30 | UUID uuidU = UUIDHelper.parseShortUUID(uuid); 31 | if(uuidU == null) { 32 | ctx.getServerHeader().setContent(MimeType.TEXT, "404 Not Found".getBytes(StandardCharsets.UTF_8)); 33 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NOT_FOUND_404); 34 | return; 35 | } 36 | 37 | Account acc = Webinterface.getAccountStorage().getAccountByConnectionSpecificID(ShittyAuth.ACCOUNT_CONNECTION_NAME, uuidU.toString()); 38 | if(acc == null) { 39 | ctx.getServerHeader().setContent(MimeType.TEXT, "404 Not Found".getBytes(StandardCharsets.UTF_8)); 40 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NOT_FOUND_404); 41 | return; 42 | } 43 | 44 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 45 | 46 | JSONObject obj = new JSONObject(); 47 | obj.put("id", UUIDHelper.toShortUUID(UUID.fromString(con.getUserID()))); 48 | obj.put("name", con.getUserName()); 49 | 50 | JSONArray a = new JSONArray(); 51 | JSONObject b = new JSONObject(); 52 | b.put("name", "textures"); 53 | 54 | JSONObject textures = new JSONObject(); 55 | textures.put("timestamp", System.currentTimeMillis()); 56 | textures.put("profileId", UUIDHelper.toShortUUID(UUID.fromString(con.getUserID()))); 57 | textures.put("profileName", con.getUserName()); 58 | 59 | String unsigned = ctx.getRequestedPath().getQuery().getFirst("unsigned"); 60 | boolean signed = "false".equals(unsigned) || "0".equals(unsigned); 61 | if(signed) { 62 | textures.put("signatureRequired", true); 63 | } 64 | 65 | UserData d = ShittyAuth.dataStorage.getUserData(con.getUserID()); 66 | textures.put("textures", TexturesHelper.getTexturesObject(con.getUserID(), d)); 67 | 68 | String value = Base64.getEncoder().encodeToString(textures.toString().getBytes(StandardCharsets.UTF_8)); 69 | b.put("value", value); 70 | 71 | if(signed) { 72 | try { 73 | Signature sig = Signature.getInstance("SHA1withRSA"); 74 | sig.initSign(ShittyAuth.privateKey); 75 | sig.update(value.getBytes(StandardCharsets.US_ASCII)); 76 | byte[] sign = sig.sign(); 77 | b.put("signature", Base64.getEncoder().encodeToString(sign)); 78 | }catch (Exception e) { 79 | e.printStackTrace(); 80 | } 81 | } 82 | 83 | a.add(b); 84 | obj.put("properties", a); 85 | 86 | ctx.getServerHeader().setContent(MimeType.JSON, obj.toString().getBytes(StandardCharsets.UTF_8)); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/yggdrasil/PublicKeysPage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.yggdrasil; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.Base64; 5 | 6 | import me.mrletsplay.mrcore.json.JSONArray; 7 | import me.mrletsplay.mrcore.json.JSONObject; 8 | import me.mrletsplay.shittyauth.ShittyAuth; 9 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 10 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 11 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 12 | 13 | public class PublicKeysPage implements HttpDocument { 14 | 15 | @Override 16 | public void createContent() { 17 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 18 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.OK_200); 19 | 20 | JSONObject obj = new JSONObject(); 21 | 22 | // playerCertificateKeys is not currently used by clients using the ShittyAuthServer. It might be used for signed chat messages and the connected player certificates 23 | obj.put("playerCertificateKeys", new JSONArray()); 24 | 25 | // profilePropertyKeys is used for signing properties on the profile page. ShittyAuthServer just uses the normal private key for that 26 | JSONArray profileKeys = new JSONArray(); 27 | JSONObject key = new JSONObject(); 28 | key.put("publicKey", Base64.getEncoder().encodeToString(ShittyAuth.publicKey.getEncoded())); 29 | profileKeys.add(key); 30 | obj.put("profilePropertyKeys", profileKeys); 31 | 32 | ctx.getServerHeader().setContent(obj.toString().getBytes(StandardCharsets.UTF_8)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/yggdrasil/RefreshPage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.yggdrasil; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.UUID; 5 | 6 | import me.mrletsplay.mrcore.json.JSONArray; 7 | import me.mrletsplay.mrcore.json.JSONObject; 8 | import me.mrletsplay.shittyauth.ShittyAuth; 9 | import me.mrletsplay.shittyauth.auth.AccessToken; 10 | import me.mrletsplay.shittyauth.auth.StoredAccessToken; 11 | import me.mrletsplay.shittyauth.util.UUIDHelper; 12 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 13 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 14 | import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; 15 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 16 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 17 | import me.mrletsplay.webinterfaceapi.Webinterface; 18 | import me.mrletsplay.webinterfaceapi.auth.Account; 19 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 20 | 21 | public class RefreshPage implements HttpDocument { 22 | 23 | @Override 24 | public void createContent() { 25 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 26 | JSONObject obj = ctx.getClientHeader().getPostData().getParsedAs(DefaultClientContentTypes.JSON_OBJECT); 27 | String 28 | accessToken = obj.getString("accessToken"), 29 | clientToken = obj.optString("clientToken").orElse(null); 30 | boolean requestUser = obj.optBoolean("requestUser").orElse(false); 31 | 32 | // Check the old token 33 | StoredAccessToken tok = ShittyAuth.tokenStorage.getStoredToken(accessToken); 34 | if(tok == null || (clientToken != null && !clientToken.equals(tok.getClientToken()))) { 35 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.ACCESS_DENIED_403); 36 | JSONObject err = new JSONObject(); 37 | err.put("error", "ForbiddenOperationException"); 38 | err.put("errorMessage", "Invalid token."); 39 | return; 40 | } 41 | 42 | Account acc = Webinterface.getAccountStorage().getAccountByConnectionSpecificID(ShittyAuth.ACCOUNT_CONNECTION_NAME, tok.getAccountID()); 43 | if(acc == null) { 44 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.ACCESS_DENIED_403); 45 | JSONObject err = new JSONObject(); 46 | err.put("error", "ForbiddenOperationException"); 47 | err.put("errorMessage", "Invalid token."); 48 | return; 49 | } 50 | 51 | // Invalidate the old token 52 | ShittyAuth.tokenStorage.removeToken(accessToken); 53 | 54 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 55 | 56 | // Then create a new token 57 | AccessToken newTok = ShittyAuth.tokenStorage.generateToken(con.getUserID(), clientToken); 58 | 59 | JSONObject r = new JSONObject(); 60 | r.put("accessToken", newTok.getAccessToken()); 61 | r.put("clientToken", newTok.getClientToken()); 62 | 63 | JSONObject selectedProfile = new JSONObject(); 64 | selectedProfile.put("id", UUIDHelper.toShortUUID(UUID.fromString(con.getUserID()))); 65 | selectedProfile.put("name", con.getUserName()); 66 | r.put("selectedProfile", selectedProfile); 67 | 68 | if(requestUser) { // To be compliant with Yggdrasil (https://wiki.vg/Authentication#Refresh) 69 | JSONObject user = new JSONObject(); 70 | user.put("username", con.getUserName()); 71 | user.put("id", acc.getID()); 72 | 73 | user.put("properties", new JSONArray()); // TODO: Maybe add preferredLanguage? 74 | } 75 | 76 | ctx.getServerHeader().setContent(MimeType.JSON, r.toString().getBytes(StandardCharsets.UTF_8)); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/yggdrasil/SignoutPage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.yggdrasil; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import me.mrletsplay.mrcore.json.JSONObject; 6 | import me.mrletsplay.shittyauth.ShittyAuth; 7 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 8 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 9 | import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; 10 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 11 | import me.mrletsplay.simplehttpserver.http.util.MimeType; 12 | import me.mrletsplay.webinterfaceapi.Webinterface; 13 | import me.mrletsplay.webinterfaceapi.auth.Account; 14 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 15 | 16 | public class SignoutPage implements HttpDocument { 17 | 18 | // https://wiki.vg/Authentication 19 | 20 | @Override 21 | public void createContent() { 22 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 23 | JSONObject obj = ctx.getClientHeader().getPostData().getParsedAs(DefaultClientContentTypes.JSON_OBJECT); 24 | 25 | if(!obj.has("username") || !obj.has("password")) { 26 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.UNAUTHORIZED_401); 27 | JSONObject response = new JSONObject(); 28 | response.put("error", "ForbiddenOperationException"); 29 | response.put("errorMessage", "Forbidden"); 30 | ctx.getServerHeader().setContent(MimeType.JSON, response.toString().getBytes(StandardCharsets.UTF_8)); 31 | return; 32 | } 33 | 34 | String 35 | username = obj.getString("username"), 36 | password = obj.getString("password"); 37 | 38 | Account acc = ShittyAuth.getAccountByUsername(username); 39 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 40 | if(acc == null || !Webinterface.getCredentialsStorage().checkCredentials(ShittyAuth.ACCOUNT_CONNECTION_NAME, con.getUserID(), password)) { 41 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.UNAUTHORIZED_401); 42 | JSONObject response = new JSONObject(); 43 | response.put("error", "ForbiddenOperationException"); 44 | response.put("errorMessage", "Invalid credentials. Invalid username or password."); 45 | ctx.getServerHeader().setContent(MimeType.JSON, response.toString().getBytes(StandardCharsets.UTF_8)); 46 | return; 47 | } 48 | 49 | ShittyAuth.tokenStorage.removeTokensByAccountID(con.getUserID()); 50 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NO_CONTENT_204); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/page/api/yggdrasil/ValidatePage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.page.api.yggdrasil; 2 | 3 | import me.mrletsplay.mrcore.json.JSONObject; 4 | import me.mrletsplay.shittyauth.ShittyAuth; 5 | import me.mrletsplay.shittyauth.auth.StoredAccessToken; 6 | import me.mrletsplay.simplehttpserver.http.HttpStatusCodes; 7 | import me.mrletsplay.simplehttpserver.http.document.HttpDocument; 8 | import me.mrletsplay.simplehttpserver.http.header.DefaultClientContentTypes; 9 | import me.mrletsplay.simplehttpserver.http.request.HttpRequestContext; 10 | 11 | public class ValidatePage implements HttpDocument { 12 | 13 | @Override 14 | public void createContent() { 15 | HttpRequestContext ctx = HttpRequestContext.getCurrentContext(); 16 | JSONObject obj = ctx.getClientHeader().getPostData().getParsedAs(DefaultClientContentTypes.JSON_OBJECT); 17 | String 18 | accessToken = obj.getString("accessToken"), 19 | clientToken = obj.optString("clientToken").orElse(null); 20 | 21 | 22 | StoredAccessToken tok = ShittyAuth.tokenStorage.getStoredToken(accessToken); 23 | if(tok == null || (clientToken != null && !clientToken.equals(tok.getClientToken()))) { 24 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.ACCESS_DENIED_403); 25 | JSONObject err = new JSONObject(); 26 | err.put("error", "ForbiddenOperationException"); 27 | err.put("errorMessage", "Invalid token."); 28 | return; 29 | } 30 | 31 | ctx.getServerHeader().setStatusCode(HttpStatusCodes.NO_CONTENT_204); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/textures/SkinType.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.textures; 2 | 3 | import me.mrletsplay.mrcore.json.converter.JSONEnum; 4 | 5 | public enum SkinType implements JSONEnum { 6 | 7 | STEVE, 8 | ALEX; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/textures/TexturesHelper.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.textures; 2 | 3 | import me.mrletsplay.mrcore.json.JSONObject; 4 | import me.mrletsplay.shittyauth.user.UserData; 5 | import me.mrletsplay.webinterfaceapi.Webinterface; 6 | import me.mrletsplay.webinterfaceapi.config.DefaultSettings; 7 | 8 | public class TexturesHelper { 9 | 10 | public static final String 11 | SKIN_PATH = "/skin/%s_%s", 12 | CAPE_PATH = "/cape/%s_%s"; 13 | 14 | public static String getSkinBaseURL() { 15 | return Webinterface.getConfig().getSetting(DefaultSettings.HTTP_BASE_URL); 16 | } 17 | 18 | public static JSONObject getTexturesObject(String accID, UserData userData) { 19 | JSONObject textures2 = new JSONObject(); 20 | JSONObject skin = new JSONObject(); 21 | skin.put("url", String.format(getSkinBaseURL() + SKIN_PATH, accID, userData.getSkinLastChanged())); 22 | if(userData.getSkinType() == SkinType.ALEX) { 23 | JSONObject meta = new JSONObject(); 24 | meta.put("model", "slim"); 25 | skin.put("metadata", meta); 26 | } 27 | textures2.put("SKIN", skin); 28 | if(userData.hasCape()) { 29 | JSONObject cape = new JSONObject(); 30 | cape.put("url", String.format(getSkinBaseURL() + CAPE_PATH, accID, userData.getCapeLastChanged())); 31 | textures2.put("CAPE", cape); 32 | } 33 | return textures2; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/user/FileUserDataStorage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.user; 2 | 3 | import java.io.File; 4 | 5 | import me.mrletsplay.mrcore.config.ConfigLoader; 6 | import me.mrletsplay.mrcore.config.FileCustomConfig; 7 | import me.mrletsplay.mrcore.config.mapper.JSONObjectMapper; 8 | import me.mrletsplay.webinterfaceapi.Webinterface; 9 | 10 | public class FileUserDataStorage implements UserDataStorage { 11 | 12 | private FileCustomConfig config; 13 | 14 | @Override 15 | public void initialize() { 16 | config = ConfigLoader.loadFileConfig(new File(Webinterface.getDataDirectory(), "shittyauth/user-data.yml")); 17 | config.registerMapper(JSONObjectMapper.create(UserData.class)); 18 | } 19 | 20 | @Override 21 | public void updateUserData(String accID, UserData userData) { 22 | config.set(accID, userData); 23 | config.saveToFile(); 24 | } 25 | 26 | @Override 27 | public UserData getUserData(String accID) { 28 | return config.getGeneric(accID, UserData.class, null, false); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/user/SQLUserDataStorage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.user; 2 | 3 | import java.sql.PreparedStatement; 4 | import java.sql.ResultSet; 5 | 6 | import me.mrletsplay.shittyauth.textures.SkinType; 7 | import me.mrletsplay.shittyauth.util.CryptoHelper; 8 | import me.mrletsplay.webinterfaceapi.sql.SQLHelper; 9 | 10 | public class SQLUserDataStorage implements UserDataStorage { 11 | 12 | @Override 13 | public void initialize() { 14 | SQLHelper.run(c -> { 15 | try(PreparedStatement st = c.prepareStatement("CREATE TABLE IF NOT EXISTS " + SQLHelper.tableName("shittyauth_user_data") + "(UserId VARCHAR(255) PRIMARY KEY, HasCape BOOLEAN, SkinType VARCHAR(255), SkinLastChanged BIGINT, CapeLastChanged BIGINT, PublicKey BLOB, PrivateKey BLOB)")) { 16 | st.execute(); 17 | } 18 | }); 19 | } 20 | 21 | @Override 22 | public void updateUserData(String accID, UserData userData) { 23 | SQLHelper.run(c -> { 24 | try(PreparedStatement st = c.prepareStatement("INSERT INTO " + SQLHelper.tableName("shittyauth_user_data") + "(UserId, HasCape, SkinType, SkinLastChanged, CapeLastChanged, PublicKey, PrivateKey) VALUES(?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE HasCape = VALUES(HasCape), SkinType = VALUES(SkinType), SkinLastChanged = VALUES(SkinLastChanged), CapeLastChanged = VALUES(CapeLastChanged), PublicKey = VALUES(PublicKey), PrivateKey = VALUES(PrivateKey)")) { 25 | st.setString(1, accID); 26 | st.setBoolean(2, userData.hasCape()); 27 | st.setString(3, userData.getSkinType().name()); 28 | st.setLong(4, userData.getSkinLastChanged()); 29 | st.setLong(5, userData.getCapeLastChanged()); 30 | st.setBytes(6, userData.getPublicKey().getEncoded()); 31 | st.setBytes(7, userData.getPrivateKey().getEncoded()); 32 | st.execute(); 33 | } 34 | }); 35 | } 36 | 37 | @Override 38 | public UserData getUserData(String accID) { 39 | return SQLHelper.run(c -> { 40 | try(PreparedStatement st = c.prepareStatement("SELECT * FROM " + SQLHelper.tableName("shittyauth_user_data") + " WHERE UserId = ?")) { 41 | st.setString(1, accID); 42 | try(ResultSet r = st.executeQuery()) { 43 | if(!r.next()) return null; 44 | return new UserData( 45 | r.getBoolean("HasCape"), 46 | SkinType.valueOf(r.getString("SkinType")), 47 | r.getLong("SkinLastChanged"), 48 | r.getLong("CapeLastChanged"), 49 | CryptoHelper.parseRSAPublicKey(r.getBytes("PublicKey")), 50 | CryptoHelper.parseRSAPrivateKey(r.getBytes("PrivateKey")) 51 | ); 52 | } 53 | } 54 | }); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/user/UserData.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.user; 2 | 3 | import java.security.KeyPair; 4 | import java.security.PrivateKey; 5 | import java.security.PublicKey; 6 | import java.util.Base64; 7 | 8 | import me.mrletsplay.mrcore.json.JSONObject; 9 | import me.mrletsplay.mrcore.json.converter.JSONConstructor; 10 | import me.mrletsplay.mrcore.json.converter.JSONConvertible; 11 | import me.mrletsplay.mrcore.json.converter.JSONValue; 12 | import me.mrletsplay.shittyauth.textures.SkinType; 13 | import me.mrletsplay.shittyauth.util.CryptoHelper; 14 | 15 | public class UserData implements JSONConvertible { 16 | 17 | @JSONValue 18 | private boolean hasCape; 19 | 20 | @JSONValue 21 | private SkinType skinType; 22 | 23 | @JSONValue 24 | private long skinLastChanged; // To update skin cache in client after change 25 | 26 | @JSONValue 27 | private long capeLastChanged; // To update cape cache in client after change 28 | 29 | private PublicKey publicKey; 30 | 31 | private PrivateKey privateKey; 32 | 33 | @JSONConstructor 34 | private UserData() {} 35 | 36 | public UserData(boolean hasCape, SkinType skinType, long skinLastChanged, long capeLastChanged, PublicKey publicKey, PrivateKey privateKey) { 37 | this.hasCape = hasCape; 38 | this.skinType = skinType; 39 | this.skinLastChanged = skinLastChanged; 40 | this.capeLastChanged = capeLastChanged; 41 | this.publicKey = publicKey; 42 | this.privateKey = privateKey; 43 | } 44 | 45 | public void setHasCape(boolean hasCape) { 46 | this.hasCape = hasCape; 47 | } 48 | 49 | public boolean hasCape() { 50 | return hasCape; 51 | } 52 | 53 | public void setSkinType(SkinType skinType) { 54 | this.skinType = skinType; 55 | } 56 | 57 | public SkinType getSkinType() { 58 | return skinType; 59 | } 60 | 61 | public void setSkinLastChanged(long skinLastChanged) { 62 | this.skinLastChanged = skinLastChanged; 63 | } 64 | 65 | public long getSkinLastChanged() { 66 | return skinLastChanged; 67 | } 68 | 69 | public void setCapeLastChanged(long capeLastChanged) { 70 | this.capeLastChanged = capeLastChanged; 71 | } 72 | 73 | public long getCapeLastChanged() { 74 | return capeLastChanged; 75 | } 76 | 77 | public PublicKey getPublicKey() { 78 | return publicKey; 79 | } 80 | 81 | public PrivateKey getPrivateKey() { 82 | return privateKey; 83 | } 84 | 85 | public boolean hasKeyPair() { 86 | return publicKey != null && privateKey != null; 87 | } 88 | 89 | public void generateNewKeyPair() { 90 | KeyPair pair = CryptoHelper.generateRSAKeyPair(); 91 | this.publicKey = pair.getPublic(); 92 | this.privateKey = pair.getPrivate(); 93 | } 94 | 95 | @Override 96 | public void preSerialize(JSONObject object) { 97 | object.put("publicKey" ,Base64.getEncoder().encodeToString(publicKey.getEncoded())); 98 | object.put("privateKey" ,Base64.getEncoder().encodeToString(privateKey.getEncoded())); 99 | } 100 | 101 | @Override 102 | public void preDeserialize(JSONObject object) { 103 | publicKey = CryptoHelper.parseRSAPublicKey(Base64.getDecoder().decode(object.getString("publicKey"))); 104 | privateKey = CryptoHelper.parseRSAPrivateKey(Base64.getDecoder().decode(object.getString("privateKey"))); 105 | } 106 | 107 | public static UserData createNew() { 108 | UserData ud = new UserData(); 109 | ud.setSkinType(SkinType.STEVE); 110 | ud.generateNewKeyPair(); 111 | return ud; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/user/UserDataStorage.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.user; 2 | 3 | public interface UserDataStorage { 4 | 5 | public void initialize(); 6 | 7 | public void updateUserData(String accID, UserData userData); 8 | 9 | public UserData getUserData(String accID); 10 | 11 | } -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/util/CryptoHelper.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.util; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.security.KeyFactory; 5 | import java.security.KeyPair; 6 | import java.security.KeyPairGenerator; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.security.PrivateKey; 9 | import java.security.PublicKey; 10 | import java.security.spec.InvalidKeySpecException; 11 | import java.security.spec.PKCS8EncodedKeySpec; 12 | import java.security.spec.X509EncodedKeySpec; 13 | import java.util.Base64; 14 | 15 | import me.mrletsplay.mrcore.misc.FriendlyException; 16 | 17 | public class CryptoHelper { 18 | 19 | private static final String 20 | PUBKEY_HEADER = "-----BEGIN RSA PUBLIC KEY-----", 21 | PUBKEY_FOOTER = "-----END RSA PUBLIC KEY-----", 22 | PRIVKEY_HEADER = "-----BEGIN RSA PRIVATE KEY-----", 23 | PRIVKEY_FOOTER = "-----END RSA PRIVATE KEY-----"; 24 | 25 | private static final KeyFactory RSA; 26 | private static final Base64.Encoder ENCODER = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8)); 27 | 28 | static { 29 | try { 30 | RSA = KeyFactory.getInstance("RSA"); 31 | } catch (NoSuchAlgorithmException e) { 32 | throw new FriendlyException(e); 33 | } 34 | } 35 | 36 | public static KeyPair generateRSAKeyPair() { 37 | try { 38 | return KeyPairGenerator.getInstance("RSA").generateKeyPair(); 39 | } catch (NoSuchAlgorithmException e) { 40 | throw new FriendlyException(e); 41 | } 42 | } 43 | 44 | public static PublicKey parseRSAPublicKey(byte[] bytes) { 45 | X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes); 46 | try { 47 | return RSA.generatePublic(spec); 48 | } catch (InvalidKeySpecException e) { 49 | throw new FriendlyException(e); 50 | } 51 | } 52 | 53 | public static PrivateKey parseRSAPrivateKey(byte[] bytes) { 54 | PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes); 55 | try { 56 | return RSA.generatePrivate(spec); 57 | } catch (InvalidKeySpecException e) { 58 | throw new FriendlyException(e); 59 | } 60 | } 61 | 62 | public static PublicKey parseRSAPublicKey(String str) { 63 | byte[] bs = Base64.getMimeDecoder().decode(str.substring(PUBKEY_HEADER.length(), str.length() - PUBKEY_FOOTER.length())); 64 | return parseRSAPublicKey(bs); 65 | } 66 | 67 | public static PrivateKey parseRSAPrivateKey(String str) { 68 | byte[] bs = Base64.getMimeDecoder().decode(str.substring(PRIVKEY_HEADER.length(), str.length() - PRIVKEY_FOOTER.length())); 69 | return parseRSAPrivateKey(bs); 70 | } 71 | 72 | public static String encodeRSAPublicKey(PublicKey key) { 73 | byte[] bs = key.getEncoded(); 74 | return PUBKEY_HEADER + "\n" + ENCODER.encodeToString(bs) + "\n" + PUBKEY_FOOTER; 75 | } 76 | 77 | public static String encodeRSAPrivateKey(PrivateKey key) { 78 | byte[] bs = key.getEncoded(); 79 | return PRIVKEY_HEADER + "\n" + ENCODER.encodeToString(bs) + "\n" + PRIVKEY_FOOTER; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/util/DefaultTexture.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.util; 2 | 3 | import java.nio.file.Path; 4 | import java.nio.file.Paths; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | public enum DefaultTexture { 10 | 11 | SKIN_STEVE_CLASSIC("Steve (Classic)", "1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"), 12 | 13 | SKIN_STEVE("Steve", "31f477eb1a7beee631c2ca64d06f8f68fa93a3386d04452ab27f43acdf1b60cb"), 14 | SKIN_ALEX("Alex", "1abc803022d8300ab7578b189294cce39622d9a404cdc00d3feacfdf45be6981"), 15 | SKIN_ZURI("Zuri", "f5dddb41dcafef616e959c2817808e0be741c89ffbfed39134a13e75b811863d"), 16 | SKIN_SUNNY("Sunny", "a3bd16079f764cd541e072e888fe43885e711f98658323db0f9a6045da91ee7a"), 17 | SKIN_NOOR("Noor", "90e75cd429ba6331cd210b9bd19399527ee3bab467b5a9f61cb8a27b177f6789"), 18 | SKIN_MAKENA("Makena", "dc0fcfaf2aa040a83dc0de4e56058d1bbb2ea40157501f3e7d15dc245e493095"), 19 | SKIN_KAI("Kai", "e5cdc3243b2153ab28a159861be643a4fc1e3c17d291cdd3e57a7f370ad676f3"), 20 | SKIN_EFE("Efe", "daf3d88ccb38f11f74814e92053d92f7728ddb1a7955652a60e30cb27ae6659f"), 21 | SKIN_ARI("Ari", "4c05ab9e07b3505dc3ec11370c3bdce5570ad2fb2b562e9b9dd9cf271f81aa44"), 22 | 23 | SKIN_STEVE_SLIM("Steve", "d5c4ee5ce20aed9e33e866c66caa37178606234b3721084bf01d13320fb2eb3f"), 24 | SKIN_ALEX_SLIM("Alex", "46acd06e8483b176e8ea39fc12fe105eb3a2a4970f5100057e9d84d4b60bdfa7"), 25 | SKIN_ZURI_SLIM("Zuri", "eee522611005acf256dbd152e992c60c0bb7978cb0f3127807700e478ad97664"), 26 | SKIN_SUNNY_SLIM("Sunny", "b66bc80f002b10371e2fa23de6f230dd5e2f3affc2e15786f65bc9be4c6eb71a"), 27 | SKIN_NOOR_SLIM("Noor", "6c160fbd16adbc4bff2409e70180d911002aebcfa811eb6ec3d1040761aea6dd"), 28 | SKIN_MAKENA_SLIM("Makena", "7cb3ba52ddd5cc82c0b050c3f920f87da36add80165846f479079663805433db"), 29 | SKIN_KAI_SLIM("Kai", "226c617fde5b1ba569aa08bd2cb6fd84c93337532a872b3eb7bf66bdd5b395f8"), 30 | SKIN_EFE_SLIM("Efe", "fece7017b1bb13926d1158864b283b8b930271f80a90482f174cca6a17e88236"), 31 | SKIN_ARI_SLIM("Ari", "6ac6ca262d67bcfb3dbc924ba8215a18195497c780058a5749de674217721892"), 32 | 33 | CAPE_MOJANG_OLD("Mojang (Classic)", "8f120319222a9f4a104e2f5cb97b2cda93199a2ee9e1585cb8d09d6f687cb761"), 34 | CAPE_MOJANG("Mojang", "5786fe99be377dfb6858859f926c4dbc995751e91cee373468c5fbf4865e7151"), 35 | CAPE_MOJANG_NEW("Mojang Studios", "9e507afc56359978a3eb3e32367042b853cddd0995d17d0da995662913fb00f7"), 36 | 37 | CAPE_MINECON_2011("MINECON 2011", "953cac8b779fe41383e675ee2b86071a71658f2180f56fbce8aa315ea70e2ed6"), 38 | CAPE_MINECON_2012("MINECON 2012", "a2e8d97ec79100e90a75d369d1b3ba81273c4f82bc1b737e934eed4a854be1b6"), 39 | CAPE_MINECON_2013("MINECON 2013", "153b1a0dfcbae953cdeb6f2c2bf6bf79943239b1372780da44bcbb29273131da"), 40 | CAPE_MINECON_2015("MINECON 2015", "b0cc08840700447322d953a02b965f1d65a13a603bf64b17c803c21446fe1635"), 41 | CAPE_MINECON_2016("MINECON 2016", "e7dfea16dc83c97df01a12fabbd1216359c0cd0ea42f9999b6e97c584963e980"), 42 | 43 | CAPE_MILLIONTH_SALE("Millionth Customer", "70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"), 44 | CAPE_DANNYBSTYLE("dB (dannyBstyle)", "bcfbe84c6542a4a5c213c1cacf8979b5e913dcb4ad783a8b80e3c4a7d5c8bdac"), 45 | CAPE_JULIANCLARK("Snowman (JulianClark)", "23ec737f18bfe4b547c95935fc297dd767bb84ee55bfd855144d279ac9bfd9fe"), 46 | CAPE_CHEAPSH0T("Japanese Translator (cheapsh0t)", "ca29f5dd9e94fb1748203b92e36b66fda80750c87ebc18d6eafdb0e28cc1d05f"), 47 | CAPE_MRMESSIAH("Spade (MrMessiah)", "2e002d5e1758e79ba51d08d92a0f3a95119f2f435ae7704916507b6c565a7da8"), 48 | CAPE_PRISMARINE("Prismarine (Drullkus)", "d8f8d13a1adf9636a16c31d47f3ecc9bb8d8533108aa5ad2a01b13b1a0c55eac"), 49 | CAPE_BIRTHDAY("Birthday (Gr8Bizzo)", "2056f2eebd759cce93460907186ef44e9192954ae12b227d817eb4b55627a7fc"), 50 | CAPE_VALENTINE("Valentine", "6a7cf0eb5cfe7e7c508b364e32916dfd28d164e7bf6d92c6ea7811b82451e760"), 51 | 52 | CAPE_TRANSLATOR("Translator", "1bf91499701404e21bd46b0191d63239a4ef76ebde88d27e4d430ac211df681e"), 53 | CAPE_TRANSLATOR_CHINESE("Chinese Translator", "2262fb1d24912209490586ecae98aca8500df3eff91f2a07da37ee524e7e3cb6"), 54 | CAPE_SCROLLS_CHAMP("Scrolls Champion", "3efadf6510961830f9fcc077f19b4daf286d502b5f5aafbd807c7bbffcaca245"), 55 | CAPE_COBALT("Cobalt", "ca35c56efe71ed290385f4ab5346a1826b546a54d519e6a3ff01efa01acce81"), 56 | CAPE_MODERATOR("Mojira Moderator", "ae677f7d98ac70a533713518416df4452fe5700365c09cf45d0d156ea9396551"), 57 | CAPE_MAP_MAKER("Realms MapMaker", "17912790ff164b93196f08ba71d0e62129304776d0f347334f8a6eae509f8a56"), 58 | CAPE_TURTLE("Turtle", "5048ea61566353397247d2b7d946034de926b997d5e66c86483dfb1e031aee95"), 59 | 60 | CAPE_MIGRATOR("Migrator", "2340c0e03dd24a11b15a8b33c2a7e9e32abb2051b2481d0ba7defd635ca7a933"), 61 | CAPE_VANILLA("Vanilla", "f9a76537647989f9a0b6d001e320dac591c359e9e61a31f4ce11c88f207f0ad4"), 62 | 63 | CAPE_15_YEAR_ANNIVERSARY("15 Year Anniversary", "cd9d82ab17fd92022dbd4a86cde4c382a7540e117fae7b9a2853658505a80625"), 64 | CAPE_CHERRY_BLOSSOM("Cherry Blossom", "afd553b39358a24edfe3b8a9a939fa5fa4faa4d9a9c3d6af8eafb377fa05c2bb"), 65 | CAPE_PURPLE_HEART("Purple Heart", "cb40a92e32b57fd732a00fc325e7afb00a7ca74936ad50d8e860152e482cfbde"), 66 | CAPE_FOLLOWERS("Follower's", "569b7f2a1d00d26f30efe3f9ab9ac817b1e6d35f4f3cfb0324ef2d328223d350"), 67 | 68 | // Christmas 2010 69 | // New Year 2011 70 | // Sniffer 71 | // Snail 72 | // ICU 73 | // ICU2 74 | // Frog 75 | // Pancape 76 | // Founder's 77 | // Pride 78 | // MCC 15th Year 79 | 80 | // Microsoft Xbox 81 | // 1st birthday 82 | 83 | // Awesom 84 | // Blonk 85 | // No-Circle 86 | // Nyan 87 | // Squid 88 | // Veterinarian 89 | 90 | // Minecon 2011 alternatives 91 | 92 | // BE skin packs 93 | ; 94 | 95 | public static final String TEXTURE_URL = "https://textures.minecraft.net/texture/%s"; 96 | public static final Path DEFAULT_TEXTURES_PATH = Paths.get("shittyauth/textures"); 97 | 98 | private static final List 99 | SKINS, 100 | SLIM_SKINS, 101 | CAPES; 102 | 103 | static { 104 | List 105 | skins = new ArrayList<>(), 106 | slimSkins = new ArrayList<>(), 107 | capes = new ArrayList<>(); 108 | 109 | for(DefaultTexture texture : values()) { 110 | if(texture.name().startsWith("SKIN_")) { 111 | (texture.name().endsWith("_SLIM") ? slimSkins : skins).add(texture); 112 | }else { 113 | capes.add(texture); 114 | } 115 | } 116 | 117 | SKINS = Collections.unmodifiableList(skins); 118 | SLIM_SKINS = Collections.unmodifiableList(slimSkins); 119 | CAPES = Collections.unmodifiableList(capes); 120 | } 121 | 122 | private final String name; 123 | private final String textureID; 124 | 125 | private DefaultTexture(String name, String textureID) { 126 | this.name = name; 127 | this.textureID = textureID; 128 | } 129 | 130 | public String getName() { 131 | return name; 132 | } 133 | 134 | public String getTextureID() { 135 | return textureID; 136 | } 137 | 138 | public String getURL() { 139 | return String.format(TEXTURE_URL, textureID); 140 | } 141 | 142 | public Path getPath() { 143 | return DEFAULT_TEXTURES_PATH.resolve(name().toLowerCase() + ".png"); 144 | } 145 | 146 | public static List getSkins() { 147 | return SKINS; 148 | } 149 | 150 | public static List getSlimSkins() { 151 | return SLIM_SKINS; 152 | } 153 | 154 | public static List getCapes() { 155 | return CAPES; 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/util/InvalidSkinException.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.util; 2 | 3 | public class InvalidSkinException extends Exception { 4 | 5 | private static final long serialVersionUID = 2581532239295743608L; 6 | 7 | public InvalidSkinException() { 8 | super(); 9 | } 10 | 11 | public InvalidSkinException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public InvalidSkinException(String message) { 16 | super(message); 17 | } 18 | 19 | public InvalidSkinException(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/util/InvalidUsernameException.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.util; 2 | 3 | public class InvalidUsernameException extends Exception { 4 | 5 | private static final long serialVersionUID = -827594972986252869L; 6 | 7 | public InvalidUsernameException() { 8 | super(); 9 | } 10 | 11 | public InvalidUsernameException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public InvalidUsernameException(String message) { 16 | super(message); 17 | } 18 | 19 | public InvalidUsernameException(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/util/UUIDHelper.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.util; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.UUID; 5 | 6 | import me.mrletsplay.mrcore.misc.ByteUtils; 7 | 8 | public class UUIDHelper { 9 | 10 | public static String toShortUUID(UUID uuid) { 11 | return ByteUtils.bytesToHex(getBytesFromUUID(uuid)); 12 | } 13 | 14 | public static UUID parseShortUUID(String shortUUID) { 15 | try { 16 | return getUUIDFromBytes(ByteUtils.hexToBytes(shortUUID)); 17 | }catch(IllegalArgumentException e) { 18 | return null; 19 | } 20 | } 21 | 22 | private static byte[] getBytesFromUUID(UUID uuid) { 23 | ByteBuffer bb = ByteBuffer.wrap(new byte[16]); 24 | bb.putLong(uuid.getMostSignificantBits()); 25 | bb.putLong(uuid.getLeastSignificantBits()); 26 | return bb.array(); 27 | } 28 | 29 | private static UUID getUUIDFromBytes(byte[] bytes) { 30 | ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); 31 | Long high = byteBuffer.getLong(); 32 | Long low = byteBuffer.getLong(); 33 | return new UUID(high, low); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/me/mrletsplay/shittyauth/webinterface/ShittyAuthWIHandler.java: -------------------------------------------------------------------------------- 1 | package me.mrletsplay.shittyauth.webinterface; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.ByteArrayInputStream; 5 | import java.io.IOException; 6 | import java.util.UUID; 7 | import java.util.regex.Pattern; 8 | 9 | import javax.imageio.ImageIO; 10 | 11 | import me.mrletsplay.shittyauth.ShittyAuth; 12 | import me.mrletsplay.shittyauth.config.ShittyAuthSettings; 13 | import me.mrletsplay.shittyauth.textures.SkinType; 14 | import me.mrletsplay.shittyauth.user.UserData; 15 | import me.mrletsplay.shittyauth.util.InvalidSkinException; 16 | import me.mrletsplay.webinterfaceapi.DefaultPermissions; 17 | import me.mrletsplay.webinterfaceapi.Webinterface; 18 | import me.mrletsplay.webinterfaceapi.auth.Account; 19 | import me.mrletsplay.webinterfaceapi.auth.AccountConnection; 20 | import me.mrletsplay.webinterfaceapi.auth.impl.PasswordAuth; 21 | import me.mrletsplay.webinterfaceapi.page.SettingsPage; 22 | import me.mrletsplay.webinterfaceapi.page.action.ActionEvent; 23 | import me.mrletsplay.webinterfaceapi.page.action.ActionHandler; 24 | import me.mrletsplay.webinterfaceapi.page.action.ActionResponse; 25 | import me.mrletsplay.webinterfaceapi.page.action.WebinterfaceHandler; 26 | import me.mrletsplay.webinterfaceapi.page.element.FileUpload; 27 | 28 | public class ShittyAuthWIHandler implements ActionHandler { 29 | 30 | public static final Pattern USERNAME_PATTERN = Pattern.compile("[a-zA-Z0-9_]{3,16}"); 31 | 32 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "createAccount") 33 | public ActionResponse createAccount(ActionEvent event) { 34 | if(ShittyAuth.config.getSetting(ShittyAuthSettings.ALLOW_REGISTRATION)) return ActionResponse.error("Creation of Minecraft accounts disabled"); 35 | 36 | Account acc = event.getAccount(); 37 | if(acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME) != null) return ActionResponse.error("Account already has a Minecraft account"); 38 | 39 | String username = event.getData().getString("username"); 40 | String password = event.getData().getString("password"); 41 | 42 | if(!USERNAME_PATTERN.matcher(username).matches()) return ActionResponse.error("Invalid username"); 43 | 44 | if(ShittyAuth.getAccountByUsername(username) != null) return ActionResponse.error("An account with that username already exists"); 45 | 46 | UUID uuid = UUID.randomUUID(); 47 | AccountConnection con = new AccountConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME, uuid.toString(), username, null, null); 48 | acc.addConnection(con); 49 | Webinterface.getCredentialsStorage().storeCredentials(ShittyAuth.ACCOUNT_CONNECTION_NAME, uuid.toString(), password); 50 | ShittyAuth.dataStorage.updateUserData(con.getUserID(), UserData.createNew()); 51 | 52 | return ActionResponse.success(); 53 | } 54 | 55 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "uploadSkin") 56 | public ActionResponse uploadSkin(ActionEvent event) { 57 | Account acc = event.getAccount(); 58 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 59 | if(con == null) return ActionResponse.error("No Minecraft account"); 60 | byte[] skinBytes = FileUpload.getUploadedFileBytes(event); 61 | if(skinBytes.length == 0) return ActionResponse.error("No file provided"); 62 | try { 63 | BufferedImage img = ImageIO.read(new ByteArrayInputStream(skinBytes)); 64 | if(img == null) return ActionResponse.error("Invalid image file"); 65 | ShittyAuth.updateUserSkin(con.getUserID(), img); 66 | return ActionResponse.success(); 67 | }catch(IOException e) { 68 | return ActionResponse.error("Invalid skin file"); 69 | } catch (InvalidSkinException e) { 70 | return ActionResponse.error(e.getMessage()); 71 | } 72 | } 73 | 74 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "uploadCape") 75 | public ActionResponse uploadCape(ActionEvent event) { 76 | Account acc = event.getAccount(); 77 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 78 | if(con == null) return ActionResponse.error("No MC account"); 79 | byte[] capeBytes = FileUpload.getUploadedFileBytes(event); 80 | if(capeBytes.length == 0) return ActionResponse.error("No file provided"); 81 | try { 82 | BufferedImage img = ImageIO.read(new ByteArrayInputStream(capeBytes)); 83 | if(img == null) return ActionResponse.error("Invalid image file"); 84 | ShittyAuth.updateUserCape(con.getUserID(), img); 85 | return ActionResponse.success(); 86 | }catch(IOException e) { 87 | return ActionResponse.error("Invalid cape file"); 88 | } catch (InvalidSkinException e) { 89 | return ActionResponse.error(e.getMessage()); 90 | } 91 | } 92 | 93 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "setSkinType") 94 | public ActionResponse setSkinType(ActionEvent event) { 95 | Account acc = event.getAccount(); 96 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 97 | if(con == null) return ActionResponse.error("No Minecraft account"); 98 | SkinType type = SkinType.valueOf(event.getData().getString("type")); 99 | UserData d = ShittyAuth.dataStorage.getUserData(con.getUserID()); 100 | d.setSkinType(type); 101 | ShittyAuth.dataStorage.updateUserData(con.getUserID(), d); 102 | return ActionResponse.success(); 103 | } 104 | 105 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "setEnableCape") 106 | public ActionResponse setEnableCape(ActionEvent event) { 107 | Account acc = event.getAccount(); 108 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 109 | if(con == null) return ActionResponse.error("No Minecraft account"); 110 | boolean enable = event.getData().getBoolean("enable"); 111 | UserData d = ShittyAuth.dataStorage.getUserData(con.getUserID()); 112 | d.setHasCape(enable); 113 | ShittyAuth.dataStorage.updateUserData(con.getUserID(), d); 114 | return ActionResponse.success(); 115 | } 116 | 117 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "resetPassword") 118 | public ActionResponse resetPassword(ActionEvent event) { 119 | Account acc = event.getAccount(); 120 | AccountConnection con = acc.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 121 | if(con == null) return ActionResponse.error("No Minecraft account"); 122 | 123 | String password = event.getData().getString("password"); 124 | Webinterface.getCredentialsStorage().storeCredentials(ShittyAuth.ACCOUNT_CONNECTION_NAME, con.getUserID(), password); 125 | ShittyAuth.tokenStorage.removeTokensByAccountID(con.getUserID()); // Invalidate all sessions 126 | 127 | return ActionResponse.success(); 128 | } 129 | 130 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "changeMCUsername", permission = DefaultPermissions.MODIFY_USERS) 131 | public ActionResponse changeMCUsername(ActionEvent event) { 132 | String accountID = event.getData().getString("account"); 133 | String username = event.getData().getString("username"); 134 | 135 | Account account = Webinterface.getAccountStorage().getAccountByID(accountID); 136 | if(account == null) return ActionResponse.error("Account doesn't exist"); 137 | 138 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 139 | if(connection == null) return ActionResponse.error("No Minecraft account"); 140 | 141 | AccountConnection newConnection = new AccountConnection(connection.getConnectionName(), connection.getUserID(), username, connection.getUserEmail(), connection.getUserAvatar()); 142 | account.removeConnection(connection); 143 | account.addConnection(newConnection); 144 | 145 | return ActionResponse.success(); 146 | } 147 | 148 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "changeMCPassword", permission = DefaultPermissions.MODIFY_USERS) 149 | public ActionResponse changeMCPassword(ActionEvent event) { 150 | String accountID = event.getData().getString("account"); 151 | String password = event.getData().getString("password"); 152 | 153 | Account account = Webinterface.getAccountStorage().getAccountByID(accountID); 154 | if(account == null) return ActionResponse.error("Account doesn't exist"); 155 | 156 | AccountConnection connection = account.getConnection(ShittyAuth.ACCOUNT_CONNECTION_NAME); 157 | if(connection == null) return ActionResponse.error("No Minecraft account"); 158 | 159 | Webinterface.getCredentialsStorage().storeCredentials(ShittyAuth.ACCOUNT_CONNECTION_NAME, connection.getUserID(), password); 160 | return ActionResponse.success(); 161 | } 162 | 163 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "changeWIAPIPassword", permission = DefaultPermissions.MODIFY_USERS) 164 | public ActionResponse changeWIAPIPassword(ActionEvent event) { 165 | String accountID = event.getData().getString("account"); 166 | String password = event.getData().getString("password"); 167 | 168 | Account account = Webinterface.getAccountStorage().getAccountByID(accountID); 169 | if(account == null) return ActionResponse.error("Account doesn't exist"); 170 | 171 | AccountConnection connection = account.getConnection(PasswordAuth.ID); 172 | if(connection == null) return ActionResponse.error("Not a password-based account"); 173 | 174 | Webinterface.getCredentialsStorage().storeCredentials(PasswordAuth.ID, connection.getUserID(), password); 175 | return ActionResponse.success(); 176 | } 177 | 178 | @WebinterfaceHandler(requestTarget = "shittyauth", requestTypes = "setSetting", permission = DefaultPermissions.SETTINGS) 179 | public ActionResponse setSetting(ActionEvent event) { 180 | return SettingsPage.handleSetSettingRequest(ShittyAuth.config, event); 181 | } 182 | 183 | } 184 | --------------------------------------------------------------------------------