diff --git a/plugin.yml b/plugin.yml index a6b9d74..1dece11 100644 --- a/plugin.yml +++ b/plugin.yml @@ -3,7 +3,7 @@ main: buttondevteam.website.ButtonWebsiteModule version: 4.0 author: NorbiPeti depend: - - ThorpeCore + - Chroma-Core commands: login: aliases: [web, weblogin, website] \ No newline at end of file diff --git a/pom.xml b/pom.xml index cd09ce1..aa4dce8 100644 --- a/pom.xml +++ b/pom.xml @@ -27,8 +27,8 @@ maven-compiler-plugin 3.3 - 1.8 - 1.8 + 10 + 10 @@ -111,17 +111,10 @@ 3.20.0-GA provided - - - commons-io - commons-io - 1.3.2 - provided - com.github.TBMCPlugins.ChromaCore Chroma-Core - ${branch}-SNAPSHOT + v1.0.0 provided diff --git a/src/buttondevteam/website/AcmeClient.java b/src/buttondevteam/website/AcmeClient.java deleted file mode 100644 index 13a0e09..0000000 --- a/src/buttondevteam/website/AcmeClient.java +++ /dev/null @@ -1,318 +0,0 @@ -/* - * acme4j - Java ACME client - * - * Copyright (C) 2015 Richard "Shred" Körber - * http://acme4j.shredzone.org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ //Modified -package buttondevteam.website; - -import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.website.page.AcmeChallengePage; -import org.shredzone.acme4j.*; -import org.shredzone.acme4j.challenge.Challenge; -import org.shredzone.acme4j.challenge.Http01Challenge; -import org.shredzone.acme4j.exception.AcmeException; -import org.shredzone.acme4j.util.CSRBuilder; -import org.shredzone.acme4j.util.KeyPairUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.*; -import java.net.URI; -import java.security.KeyPair; -import java.util.Arrays; -import java.util.Collection; - -/** - * A simple client test tool. - *

- * Pass the names of the domains as parameters. - */ -public class AcmeClient { - // File name of the User Key Pair - public static final File USER_KEY_FILE = new File("user.key"); - - // File name of the Domain Key Pair - public static final File DOMAIN_KEY_FILE = new File("domain.key"); - - // File name of the CSR - public static final File DOMAIN_CSR_FILE = new File("domain.csr"); - - // File name of the signed certificate - public static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt"); - - // RSA key size of generated key pairs - private static final int KEY_SIZE = 2048; - - private static final Logger LOG = LoggerFactory.getLogger(AcmeClient.class); - - /** - * Generates a certificate for the given domains. Also takes care for the registration process. - * - * @param domains - * Domains to get a common certificate for - */ - public void fetchCertificate(Collection domains) throws IOException, AcmeException { - // Load the user key file. If there is no key file, create a new one. - // Keep this key pair in a safe place! In a production environment, you will not be - // able to access your account again if you should lose the key pair. - KeyPair userKeyPair = loadOrCreateKeyPair(USER_KEY_FILE); - - // Create a session for Let's Encrypt. - // Use "acme://letsencrypt.org" for production server - Session session = new Session("acme://letsencrypt.org" + (TBMCCoreAPI.IsTestServer() ? "/staging" : "")); - - // Get the Registration to the account. - // If there is no account yet, create a new one. - Account acc = findOrRegisterAccount(session, userKeyPair); - - Order order = acc.newOrder().domains(domains).create(); - - // Separately authorize every requested domain. - for (Authorization auth : order.getAuthorizations()) { - authorize(auth); - } - - // Load or create a key pair for the domains. This should not be the userKeyPair! - KeyPair domainKeyPair = loadOrCreateKeyPair(DOMAIN_KEY_FILE); - - // Generate a CSR for all of the domains, and sign it with the domain key pair. - CSRBuilder csrb = new CSRBuilder(); - csrb.addDomains(domains); - csrb.sign(domainKeyPair); - - // Write the CSR to a file, for later use. - try (Writer out = new FileWriter(DOMAIN_CSR_FILE)) { - csrb.write(out); - } - - LOG.info("Ordering certificate..."); - // Order the certificate - order.execute(csrb.getEncoded()); - - // Wait for the order to complete - try { - int attempts = 10; - while (order.getStatus() != Status.VALID && attempts-- > 0) { - // Did the order fail? - if (order.getStatus() == Status.INVALID) { - throw new AcmeException("Order failed... Giving up."); - } - - // Wait for a few seconds - Thread.sleep(3000L); - - // Then update the status - order.update(); - if (order.getStatus() != Status.VALID) - LOG.info("Not yet..."); - } - } catch (InterruptedException ex) { - LOG.error("interrupted", ex); - Thread.currentThread().interrupt(); - } - - // Get the certificate - Certificate certificate = order.getCertificate(); - - if (certificate == null) - throw new AcmeException("Certificate is null. Wot."); - - LOG.info("Success! The certificate for domains " + domains + " has been generated!"); - LOG.info("Certificate URL: " + certificate.getLocation()); - - // Write a combined file containing the certificate and chain. - try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) { - certificate.writeCertificate(fw); - } - - // That's all! Configure your web server to use the DOMAIN_KEY_FILE and - // DOMAIN_CHAIN_FILE for the requested domans. - } - - /** - * Loads a key pair from specified file. If the file does not exist, a new key pair is generated and saved. - * - * @return {@link KeyPair}. - */ - private KeyPair loadOrCreateKeyPair(File file) throws IOException { - if (file.exists()) { - try (FileReader fr = new FileReader(file)) { - return KeyPairUtils.readKeyPair(fr); - } - } else { - KeyPair domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE); - try (FileWriter fw = new FileWriter(file)) { - KeyPairUtils.writeKeyPair(domainKeyPair, fw); - } - return domainKeyPair; - } - } - - /** - * Finds your {@link Account} at the ACME server. It will be found by your user's public key. If your key is not known to the server yet, a new registration will be created. - * - * @param session - * {@link Session} to bind with - * @param kp The user keypair - * @return {@link Account} connected to your account - */ - private Account findOrRegisterAccount(Session session, KeyPair kp) throws AcmeException, IOException { - Account acc; - - URI loc = ButtonWebsiteModule.getRegistration(); - if (loc != null) { - LOG.info("Loading account from file"); - return new Login(loc.toURL(), kp, session).getAccount(); - } - - // Try to create a new Registration. - AccountBuilder ab = new AccountBuilder().useKeyPair(kp); - - // This is a new account. Let the user accept the Terms of Service. - // We won't be able to authorize domains until the ToS is accepted. - URI agreement = session.getMetadata().getTermsOfService(); - acceptAgreement(ab, agreement); - acc = ab.create(session); - LOG.info("Registered a new user, URI: " + acc.getLocation()); - ButtonWebsiteModule.storeRegistration(acc.getLocation()); - - return acc; - } - - /** - * Authorize a domain. It will be associated with your account, so you will be able to retrieve a signed certificate for the domain later. - *

- * You need separate authorizations for subdomains (e.g. "www" subdomain). Wildcard certificates are not currently supported. - * - * @param auth - * {@link Authorization} for the domain - */ - private void authorize(Authorization auth) throws AcmeException { - LOG.info("Authorization for domain " + auth.getDomain()); - - // The authorization is already valid. No need to process a challenge. - if (auth.getStatus() == Status.VALID) { - return; - } - - Challenge challenge = httpChallenge(auth); - - if (challenge == null) { - throw new AcmeException("No challenge found"); - } - - // If the challenge is already verified, there's no need to execute it again. - if (challenge.getStatus() == Status.VALID) { - return; - } - - // Now trigger the challenge. - challenge.trigger(); - - // Poll for the challenge to complete. - try { - int attempts = 10; - while (challenge.getStatus() != Status.VALID && attempts-- > 0) { - // Did the authorization fail? - if (challenge.getStatus() == Status.INVALID) { - throw new AcmeException("Challenge failed... Giving up."); - } - - // Wait for a few seconds - Thread.sleep(3000L); - - // Then update the status - challenge.update(); - } - } catch (InterruptedException ex) { - LOG.error("interrupted", ex); - Thread.currentThread().interrupt(); - } - - // All reattempts are used up and there is still no valid authorization? - if (challenge.getStatus() != Status.VALID) { - throw new AcmeException("Failed to pass the challenge for domain " + auth.getDomain() + ", ... Giving up."); - } - } - - /** - * Prepares a HTTP challenge. - *

- * The verification of this challenge expects a file with a certain content to be reachable at a given path under the domain to be tested. - *

- * This example outputs instructions that need to be executed manually. In a production environment, you would rather generate this file automatically, or maybe use a servlet that returns - * {@link Http01Challenge#getAuthorization()}. - * - * @param auth - * {@link Authorization} to find the challenge in - * @return {@link Challenge} to verify - */ - public Challenge httpChallenge(Authorization auth) throws AcmeException { - // Find a single http-01 challenge - Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE); - if (challenge == null) { - throw new AcmeException("Found no " + Http01Challenge.TYPE + " challenge, don't know what to do..."); - } - LOG.info("Storing the challenge data."); - LOG.info("It should be reachable at: http://" + auth.getDomain() + "/.well-known/acme-challenge/" + challenge.getToken()); - ButtonWebsiteModule.addHttpPage(new AcmeChallengePage(challenge.getToken(), challenge.getAuthorization())); - ButtonWebsiteModule.startHttp(); - try { - Thread.sleep(1000); // Just to make sure - } catch (InterruptedException e) { - } - return challenge; - } - - /** - * Presents the user a link to the Terms of Service, and asks for confirmation. If the user denies confirmation, an exception is thrown. - * - * @param ab - * {@link AccountBuilder} for the user - * @param agreement - * {@link URI} of the Terms of Service - */ - public void acceptAgreement(AccountBuilder ab, URI agreement) throws AcmeException, IOException { - LOG.info("Terms of Service: " + agreement); - BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); - System.out.println("Do you accept the terms? (y/n)"); - if (br.readLine().equalsIgnoreCase("y\n")) { - throw new AcmeException("User did not accept Terms of Service"); - } - - // Motify the Registration and accept the agreement - ab.agreeToTermsOfService(); - LOG.info("Updated user's ToS"); - } - - /** - * Invokes this example. - * - * @param args - * Domains to get a certificate for - */ - public static void main(String... args) { - if (args.length == 0) { - LOG.error("Error while doing ACME!", new Exception("No domains given")); - return; - } - - LOG.info("Starting up..."); - - Collection domains = Arrays.asList(args); - try { - AcmeClient ct = new AcmeClient(); - ct.fetchCertificate(domains); - } catch (Exception ex) { - LOG.error("Failed to get a certificate for domains " + domains, ex); - } - } -} diff --git a/src/buttondevteam/website/ButtonWebsiteModule.java b/src/buttondevteam/website/ButtonWebsiteModule.java index df04447..380b56b 100644 --- a/src/buttondevteam/website/ButtonWebsiteModule.java +++ b/src/buttondevteam/website/ButtonWebsiteModule.java @@ -5,31 +5,12 @@ import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.website.io.IOHelper; import buttondevteam.website.page.*; import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsConfigurator; -import com.sun.net.httpserver.HttpsParameters; -import com.sun.net.httpserver.HttpsServer; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openssl.PEMKeyPair; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bukkit.Bukkit; -import javax.net.ssl.*; import java.io.*; import java.net.*; -import java.security.KeyPair; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.Security; -import java.security.cert.Certificate; -import java.security.cert.CertificateFactory; -import java.util.Calendar; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; public class ButtonWebsiteModule extends ButtonPlugin { - private static HttpsServer server; /** * For ACME validation and user redirection */ @@ -38,75 +19,8 @@ public class ButtonWebsiteModule extends ButtonPlugin { public ButtonWebsiteModule() { try { - int ps = getConfig().getInt("https-port", 443); int p = getConfig().getInt("http-port", 80); - server = HttpsServer.create(new InetSocketAddress((InetAddress) null, ps), 10); httpserver = HttpServer.create(new InetSocketAddress((InetAddress) null, p), 10); - SSLContext sslContext = SSLContext.getInstance("TLS"); - - // initialise the keystore - char[] password = "password".toCharArray(); - KeyStore ks = KeyStore.getInstance("JKS"); - File certfile = AcmeClient.DOMAIN_CHAIN_FILE; /* your cert path */ - File keystoreFile = new File("keystore.keystore"); - - ks.load(keystoreFile.exists() ? new FileInputStream(keystoreFile) : null, password); - - String alias = "chroma"; - - ////// - - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - InputStream certstream = fullStream(certfile); - Certificate[] certs = cf.generateCertificates(certstream).toArray(new Certificate[0]); - - BufferedReader br = new BufferedReader(new FileReader(AcmeClient.DOMAIN_KEY_FILE)); - - Security.addProvider(new BouncyCastleProvider()); - - PEMParser pp = new PEMParser(br); - PEMKeyPair pemKeyPair = (PEMKeyPair) pp.readObject(); - KeyPair kp = new JcaPEMKeyConverter().getKeyPair(pemKeyPair); - pp.close(); - PrivateKey pk = kp.getPrivate(); - - // Add the certificate - ks.setKeyEntry(alias, pk, password, certs); // TODO: Only set if updated - - // Save the new keystore contents - FileOutputStream out = new FileOutputStream(keystoreFile); - ks.store(out, password); - out.close(); - - // setup the key manager factory - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - kmf.init(ks, password); - - // setup the trust manager factory - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); - tmf.init(ks); - - // setup the HTTPS context and parameters - sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - server.setHttpsConfigurator(new HttpsConfigurator(sslContext) { - public void configure(HttpsParameters params) { - try { - // initialise the SSL context - SSLContext c = SSLContext.getDefault(); - SSLEngine engine = c.createSSLEngine(); - params.setNeedClientAuth(false); - params.setCipherSuites(engine.getEnabledCipherSuites()); - params.setProtocols(engine.getEnabledProtocols()); - - // get the default parameters - SSLParameters defaultSSLParameters = c.getDefaultSSLParameters(); - params.setSSLParameters(defaultSSLParameters); - - } catch (Exception ex) { - System.out.println("Failed to create HTTPS port"); - } - } - }); enabled = true; } catch (Exception e) { TBMCCoreAPI.SendException("An error occurred while starting the webserver!", e, this); @@ -125,19 +39,12 @@ public class ButtonWebsiteModule extends ButtonPlugin { addPage(new ProfilePage()); addPage(new BuildNotificationsPage()); addPage(new BridgePage()); + addHttpPage(new BridgePage()); TBMCCoreAPI.RegisterUserClass(WebUser.class, WebUser::new); getCommand2MC().registerCommand(new LoginCommand()); Bukkit.getScheduler().runTaskAsynchronously(this, () -> { this.getLogger().info("Starting webserver..."); - server.setExecutor( - new ThreadPoolExecutor(4, 8, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100))); - httpserver.createContext("/", exchange -> IOHelper.SendResponse(IOHelper.Redirect("https://server.figytuna.com/", exchange))); - final Calendar calendar = Calendar.getInstance(); - if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY && !TBMCCoreAPI.IsTestServer()) { // Only update every week - Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); - AcmeClient.main("server.figytuna.com"); // Task is running async so we don't need an extra thread - } - ((Runnable) server::start).run(); // Totally normal way of calling a method + httpserver.createContext("/", exchange -> IOHelper.SendResponse(IOHelper.Redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ", exchange))); if (!httpstarted) httpserver.start(); this.getLogger().info("Webserver started"); @@ -162,9 +69,7 @@ public class ButtonWebsiteModule extends ButtonPlugin { * Adds a new page/endpoint to the website. This method needs to be called before the server finishes loading (onEnable). */ public static void addPage(Page page) { - if (!enabled) - return; - server.createContext("/" + page.GetName(), page); + // No HTTPS support for now but these pages should be secured (and updated) } /** diff --git a/src/buttondevteam/website/io/IOHelper.java b/src/buttondevteam/website/io/IOHelper.java index 5dfd80a..d70a184 100644 --- a/src/buttondevteam/website/io/IOHelper.java +++ b/src/buttondevteam/website/io/IOHelper.java @@ -6,13 +6,9 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.sun.net.httpserver.HttpExchange; -import org.apache.commons.io.IOUtils; import org.bukkit.Bukkit; -import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.time.ZoneId; @@ -57,7 +53,10 @@ public class IOHelper { try { if (exchange.getRequestBody().available() == 0) return ""; - return IOUtils.toString(exchange.getRequestBody(), "UTF-8"); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + exchange.getRequestBody().transferTo(baos); + return baos.toString(StandardCharsets.UTF_8); + } } catch (Exception e) { e.printStackTrace(); return ""; diff --git a/src/buttondevteam/website/page/Page.java b/src/buttondevteam/website/page/Page.java index 77255ec..67d88a1 100644 --- a/src/buttondevteam/website/page/Page.java +++ b/src/buttondevteam/website/page/Page.java @@ -6,8 +6,8 @@ import buttondevteam.website.io.IOHelper; import buttondevteam.website.io.Response; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; -import org.apache.commons.io.output.ByteArrayOutputStream; +import java.io.ByteArrayOutputStream; import java.io.PrintStream; /**