Merge pull request #9 from TBMCPlugins/dev

Update AcmeClient to fix certificate renewal
This commit is contained in:
Norbi Peti 2018-07-04 20:36:49 +02:00 committed by GitHub
commit dbf280f941
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 146 additions and 159 deletions

2
.gitignore vendored
View file

@ -128,7 +128,7 @@ publish/
*.publishproj *.publishproj
# NuGet Packages Directory # NuGet Packages Directory
## TODO: If you have NuGet Package Restore enabled, uncomment the next line ## TO!DO: If you have NuGet Package Restore enabled, uncomment the next line
#packages/ #packages/
# Windows Azure Build Output # Windows Azure Build Output

10
pom.xml
View file

@ -45,11 +45,6 @@
<!-- <artifactSet> <includes> <include>org.shredzone.acme4j:acme4j-client</include> <!-- <artifactSet> <includes> <include>org.shredzone.acme4j:acme4j-client</include>
<include>org.shredzone.acme4j:acme4j-utils</include> <include>org.bouncycastle:bcprov-jdk15on</include> <include>org.shredzone.acme4j:acme4j-utils</include> <include>org.bouncycastle:bcprov-jdk15on</include>
</includes> </artifactSet> --> </includes> </artifactSet> -->
<pluginExecution>
<action>
<execute />
</action>
</pluginExecution>
<filters> <filters>
<filter> <filter>
<artifact>*:*</artifact> <artifact>*:*</artifact>
@ -126,15 +121,16 @@
<version>master-SNAPSHOT</version> <version>master-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.shredzone.acme4j/acme4j-client -->
<dependency> <dependency>
<groupId>org.shredzone.acme4j</groupId> <groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-client</artifactId> <artifactId>acme4j-client</artifactId>
<version>0.10</version> <version>2.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.shredzone.acme4j</groupId> <groupId>org.shredzone.acme4j</groupId>
<artifactId>acme4j-utils</artifactId> <artifactId>acme4j-utils</artifactId>
<version>0.10</version> <version>2.1</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on --> <!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<dependency> <dependency>

View file

@ -13,32 +13,22 @@
*/ //Modified */ //Modified
package buttondevteam.website; package buttondevteam.website;
import java.io.BufferedReader; import buttondevteam.lib.TBMCCoreAPI;
import java.io.File; import buttondevteam.website.page.AcmeChallengePage;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;
import java.net.URI;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import org.shredzone.acme4j.*; import org.shredzone.acme4j.*;
import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.challenge.Http01Challenge; import org.shredzone.acme4j.challenge.Http01Challenge;
import org.shredzone.acme4j.exception.AcmeConflictException;
import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.util.CSRBuilder; import org.shredzone.acme4j.util.CSRBuilder;
import org.shredzone.acme4j.util.CertificateUtils;
import org.shredzone.acme4j.util.KeyPairUtils; import org.shredzone.acme4j.util.KeyPairUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import buttondevteam.lib.TBMCCoreAPI; import java.io.*;
import buttondevteam.website.page.AcmeChallengePage; import java.net.URI;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.Collection;
/** /**
* A simple client test tool. * A simple client test tool.
@ -47,16 +37,16 @@ import buttondevteam.website.page.AcmeChallengePage;
*/ */
public class AcmeClient { public class AcmeClient {
// File name of the User Key Pair // File name of the User Key Pair
private static final File USER_KEY_FILE = new File("user.key"); public static final File USER_KEY_FILE = new File("user.key");
// File name of the Domain Key Pair // File name of the Domain Key Pair
private static final File DOMAIN_KEY_FILE = new File("domain.key"); public static final File DOMAIN_KEY_FILE = new File("domain.key");
// File name of the CSR // File name of the CSR
private static final File DOMAIN_CSR_FILE = new File("domain.csr"); public static final File DOMAIN_CSR_FILE = new File("domain.csr");
// File name of the signed certificate // File name of the signed certificate
private static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt"); public static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt");
// RSA key size of generated key pairs // RSA key size of generated key pairs
private static final int KEY_SIZE = 2048; private static final int KEY_SIZE = 2048;
@ -77,16 +67,17 @@ public class AcmeClient {
// Create a session for Let's Encrypt. // Create a session for Let's Encrypt.
// Use "acme://letsencrypt.org" for production server // Use "acme://letsencrypt.org" for production server
Session session = new Session("acme://letsencrypt.org" + (TBMCCoreAPI.IsTestServer() ? "/staging" : ""), Session session = new Session("acme://letsencrypt.org" + (TBMCCoreAPI.IsTestServer() ? "/staging" : ""));
userKeyPair);
// Get the Registration to the account. // Get the Registration to the account.
// If there is no account yet, create a new one. // If there is no account yet, create a new one.
Registration reg = findOrRegisterAccount(session); Account acc = findOrRegisterAccount(session, userKeyPair);
Order order = acc.newOrder().domains(domains).create();
// Separately authorize every requested domain. // Separately authorize every requested domain.
for (String domain : domains) { for (Authorization auth : order.getAuthorizations()) {
authorize(reg, domain); authorize(auth);
} }
// Load or create a key pair for the domains. This should not be the userKeyPair! // Load or create a key pair for the domains. This should not be the userKeyPair!
@ -102,19 +93,44 @@ public class AcmeClient {
csrb.write(out); csrb.write(out);
} }
// Now request a signed certificate. LOG.info("Ordering certificate...");
Certificate certificate = reg.requestCertificate(csrb.getEncoded()); // 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("Success! The certificate for domains " + domains + " has been generated!");
LOG.info("Certificate URI: " + certificate.getLocation()); LOG.info("Certificate URL: " + certificate.getLocation());
// Download the leaf certificate and certificate chain.
X509Certificate cert = certificate.download();
X509Certificate[] chain = certificate.downloadChain();
// Write a combined file containing the certificate and chain. // Write a combined file containing the certificate and chain.
try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) { try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) {
CertificateUtils.writeX509CertificateChain(fw, cert, chain); certificate.writeCertificate(fw);
} }
// That's all! Configure your web server to use the DOMAIN_KEY_FILE and // That's all! Configure your web server to use the DOMAIN_KEY_FILE and
@ -141,44 +157,34 @@ public class AcmeClient {
} }
/** /**
* Finds your {@link Registration} 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. * 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.
* <p>
* This is a simple way of finding your {@link Registration}. A better way is to get the URI of your new registration with {@link Registration#getLocation()} and store it somewhere. If you need to
* get access to your account later, reconnect to it via {@link Registration#bind(Session, URI)} by using the stored location.
* *
* @param session * @param session
* {@link Session} to bind with * {@link Session} to bind with
* @return {@link Registration} connected to your account * @param kp The user keypair
* @return {@link Account} connected to your account
*/ */
private Registration findOrRegisterAccount(Session session) throws AcmeException, IOException { private Account findOrRegisterAccount(Session session, KeyPair kp) throws AcmeException, IOException {
Registration reg; Account acc;
URI loc = ButtonWebsiteModule.getRegistration(); URI loc = ButtonWebsiteModule.getRegistration();
if (loc != null) { if (loc != null) {
LOG.info("Loading account from file"); LOG.info("Loading account from file");
return Registration.bind(session, loc); return new Login(loc.toURL(), kp, session).getAccount();
} }
try { // Try to create a new Registration.
// Try to create a new Registration. AccountBuilder ab = new AccountBuilder().useKeyPair(kp);
reg = new RegistrationBuilder().create(session);
LOG.info("Registered a new user, URI: " + reg.getLocation());
// This is a new account. Let the user accept the Terms of Service. // 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. // We won't be able to authorize domains until the ToS is accepted.
URI agreement = reg.getAgreement(); URI agreement = session.getMetadata().getTermsOfService();
LOG.info("Terms of Service: " + agreement); acceptAgreement(ab, agreement);
acceptAgreement(reg, agreement); acc = ab.create(session);
LOG.info("Registered a new user, URI: " + acc.getLocation());
ButtonWebsiteModule.storeRegistration(acc.getLocation());
} catch (AcmeConflictException ex) { return acc;
// The Key Pair is already registered. getLocation() contains the
// URL of the existing registration's location. Bind it to the session.
reg = Registration.bind(session, ex.getLocation());
LOG.info("Account does already exist, URI: " + reg.getLocation(), ex);
ButtonWebsiteModule.storeRegistration(ex.getLocation());
}
return reg;
} }
/** /**
@ -186,18 +192,18 @@ public class AcmeClient {
* <p> * <p>
* You need separate authorizations for subdomains (e.g. "www" subdomain). Wildcard certificates are not currently supported. * You need separate authorizations for subdomains (e.g. "www" subdomain). Wildcard certificates are not currently supported.
* *
* @param reg * @param auth
* {@link Registration} of your account * {@link Authorization} for the domain
* @param domain
* Name of the domain to authorize
*/ */
private void authorize(Registration reg, String domain) throws AcmeException { private void authorize(Authorization auth) throws AcmeException {
// Authorize the domain. LOG.info("Authorization for domain " + auth.getDomain());
Authorization auth = reg.authorizeDomain(domain);
LOG.info("Authorization for domain " + domain);
// Find the desired challenge and prepare it. // The authorization is already valid. No need to process a challenge.
Challenge challenge = httpChallenge(auth, domain); if (auth.getStatus() == Status.VALID) {
return;
}
Challenge challenge = httpChallenge(auth);
if (challenge == null) { if (challenge == null) {
throw new AcmeException("No challenge found"); throw new AcmeException("No challenge found");
@ -233,7 +239,7 @@ public class AcmeClient {
// All reattempts are used up and there is still no valid authorization? // All reattempts are used up and there is still no valid authorization?
if (challenge.getStatus() != Status.VALID) { if (challenge.getStatus() != Status.VALID) {
throw new AcmeException("Failed to pass the challenge for domain " + domain + ", ... Giving up."); throw new AcmeException("Failed to pass the challenge for domain " + auth.getDomain() + ", ... Giving up.");
} }
} }
@ -247,11 +253,9 @@ public class AcmeClient {
* *
* @param auth * @param auth
* {@link Authorization} to find the challenge in * {@link Authorization} to find the challenge in
* @param domain
* Domain name to be authorized
* @return {@link Challenge} to verify * @return {@link Challenge} to verify
*/ */
public Challenge httpChallenge(Authorization auth, String domain) throws AcmeException { public Challenge httpChallenge(Authorization auth) throws AcmeException {
// Find a single http-01 challenge // Find a single http-01 challenge
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE); Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
if (challenge == null) { if (challenge == null) {
@ -262,7 +266,7 @@ public class AcmeClient {
/* /*
* else LOG.info("Store the challenge data! Can't do automatically."); * else LOG.info("Store the challenge data! Can't do automatically.");
*/ */
LOG.info("It should be reachable at: http://" + domain + "/.well-known/acme-challenge/" + challenge.getToken()); LOG.info("It should be reachable at: http://" + auth.getDomain() + "/.well-known/acme-challenge/" + challenge.getToken());
// LOG.info("File name: " + challenge.getToken()); // LOG.info("File name: " + challenge.getToken());
// LOG.info("Content: " + challenge.getAuthorization()); // LOG.info("Content: " + challenge.getAuthorization());
/* /*
@ -280,12 +284,13 @@ public class AcmeClient {
/** /**
* Presents the user a link to the Terms of Service, and asks for confirmation. If the user denies confirmation, an exception is thrown. * Presents the user a link to the Terms of Service, and asks for confirmation. If the user denies confirmation, an exception is thrown.
* *
* @param reg * @param ab
* {@link Registration} User's registration * {@link AccountBuilder} for the user
* @param agreement * @param agreement
* {@link URI} of the Terms of Service * {@link URI} of the Terms of Service
*/ */
public void acceptAgreement(Registration reg, URI agreement) throws AcmeException, IOException { public void acceptAgreement(AccountBuilder ab, URI agreement) throws AcmeException, IOException {
LOG.info("Terms of Service: " + agreement);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Do you accept the terms? (y/n)"); System.out.println("Do you accept the terms? (y/n)");
if (br.readLine().equalsIgnoreCase("y\n")) { if (br.readLine().equalsIgnoreCase("y\n")) {
@ -293,7 +298,7 @@ public class AcmeClient {
} }
// Motify the Registration and accept the agreement // Motify the Registration and accept the agreement
reg.modify().setAgreement(agreement).commit(); ab.agreeToTermsOfService();
LOG.info("Updated user's ToS"); LOG.info("Updated user's ToS");
} }

View file

@ -4,7 +4,10 @@ import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.TBMCChatAPI; import buttondevteam.lib.chat.TBMCChatAPI;
import buttondevteam.website.io.IOHelper; import buttondevteam.website.io.IOHelper;
import buttondevteam.website.page.*; import buttondevteam.website.page.*;
import com.sun.net.httpserver.*; 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.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.PEMParser;
@ -14,10 +17,7 @@ import org.bukkit.plugin.java.JavaPlugin;
import javax.net.ssl.*; import javax.net.ssl.*;
import java.io.*; import java.io.*;
import java.net.InetAddress; import java.net.*;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.PrivateKey; import java.security.PrivateKey;
@ -46,7 +46,7 @@ public class ButtonWebsiteModule extends JavaPlugin {
// initialise the keystore // initialise the keystore
char[] password = "password".toCharArray(); char[] password = "password".toCharArray();
KeyStore ks = KeyStore.getInstance("JKS"); KeyStore ks = KeyStore.getInstance("JKS");
String certfile = "domain-chain.crt"; /* your cert path */ File certfile = AcmeClient.DOMAIN_CHAIN_FILE; /* your cert path */
File keystoreFile = new File("keystore.keystore"); File keystoreFile = new File("keystore.keystore");
ks.load(keystoreFile.exists() ? new FileInputStream(keystoreFile) : null, password); ks.load(keystoreFile.exists() ? new FileInputStream(keystoreFile) : null, password);
@ -57,9 +57,9 @@ public class ButtonWebsiteModule extends JavaPlugin {
CertificateFactory cf = CertificateFactory.getInstance("X.509"); CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream certstream = fullStream(certfile); InputStream certstream = fullStream(certfile);
Certificate[] certs = cf.generateCertificates(certstream).stream().toArray(Certificate[]::new); Certificate[] certs = cf.generateCertificates(certstream).toArray(new Certificate[0]);
BufferedReader br = new BufferedReader(new FileReader("domain.key")); BufferedReader br = new BufferedReader(new FileReader(AcmeClient.DOMAIN_KEY_FILE));
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
@ -107,7 +107,7 @@ public class ButtonWebsiteModule extends JavaPlugin {
} }
}); });
} catch (Exception e) { } catch (Exception e) {
TBMCCoreAPI.SendException("An error occured while starting the webserver!", e); TBMCCoreAPI.SendException("An error occurred while starting the webserver!", e);
getServer().getPluginManager().disablePlugin(this); getServer().getPluginManager().disablePlugin(this);
} }
} }
@ -124,15 +124,10 @@ public class ButtonWebsiteModule extends JavaPlugin {
Bukkit.getScheduler().runTaskAsynchronously(this, () -> { Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
this.getLogger().info("Starting webserver..."); this.getLogger().info("Starting webserver...");
server.setExecutor( server.setExecutor(
new ThreadPoolExecutor(4, 8, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100))); new ThreadPoolExecutor(4, 8, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100)));
httpserver.createContext("/", new HttpHandler() { httpserver.createContext("/", exchange -> IOHelper.SendResponse(IOHelper.Redirect("https://server.figytuna.com/", exchange)));
@Override
public void handle(HttpExchange exchange) throws IOException {
IOHelper.SendResponse(IOHelper.Redirect("https://server.figytuna.com/", exchange));
}
});
final Calendar calendar = Calendar.getInstance(); final Calendar calendar = Calendar.getInstance();
if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY && !TBMCCoreAPI.IsTestServer()) { // Only update every week if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY && !TBMCCoreAPI.IsTestServer()) { // Only update every week
Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
AcmeClient.main("server.figytuna.com"); // Task is running async so we don't need an extra thread AcmeClient.main("server.figytuna.com"); // Task is running async so we don't need an extra thread
} }
@ -167,7 +162,7 @@ public class ButtonWebsiteModule extends JavaPlugin {
httpserver.createContext("/" + page.GetName(), page); httpserver.createContext("/" + page.GetName(), page);
} }
static void storeRegistration(URI location) { static void storeRegistration(URL location) {
final ButtonWebsiteModule plugin = getPlugin(ButtonWebsiteModule.class); final ButtonWebsiteModule plugin = getPlugin(ButtonWebsiteModule.class);
plugin.getConfig().set("registration", location.toString()); plugin.getConfig().set("registration", location.toString());
plugin.saveConfig(); plugin.saveConfig();
@ -183,13 +178,12 @@ public class ButtonWebsiteModule extends JavaPlugin {
} }
} }
private static InputStream fullStream(String fname) throws IOException { private static InputStream fullStream(File f) throws IOException {
FileInputStream fis = new FileInputStream(fname); FileInputStream fis = new FileInputStream(f);
DataInputStream dis = new DataInputStream(fis); DataInputStream dis = new DataInputStream(fis);
byte[] bytes = new byte[dis.available()]; byte[] bytes = new byte[dis.available()];
dis.readFully(bytes); dis.readFully(bytes);
dis.close(); dis.close();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes); return new ByteArrayInputStream(bytes);
return bais;
} }
} }

View file

@ -103,7 +103,7 @@ public class IOHelper {
.AddHeaders(exchange); .AddHeaders(exchange);
} }
public static Response Redirect(String url, HttpExchange exchange) throws IOException { public static Response Redirect(String url, HttpExchange exchange) {
exchange.getResponseHeaders().add("Location", url); exchange.getResponseHeaders().add("Location", url);
return new Response(303, "<a href=\"" + url + "\">If you can see this, click here to continue</a>", exchange); return new Response(303, "<a href=\"" + url + "\">If you can see this, click here to continue</a>", exchange);
} }
@ -113,9 +113,9 @@ public class IOHelper {
return new Cookies(); return new Cookies();
Map<String, String> map = new HashMap<>(); Map<String, String> map = new HashMap<>();
for (String cheader : exchange.getRequestHeaders().get("Cookie")) { for (String cheader : exchange.getRequestHeaders().get("Cookie")) {
String[] spl = cheader.split("\\;\\s*"); String[] spl = cheader.split(";\\s*");
for (String s : spl) { for (String s : spl) {
String[] kv = s.split("\\="); String[] kv = s.split("=");
if (kv.length < 2) if (kv.length < 2)
continue; continue;
map.put(kv[0], kv[1]); map.put(kv[0], kv[1]);
@ -123,7 +123,7 @@ public class IOHelper {
} }
if (!map.containsKey("expiretime")) if (!map.containsKey("expiretime"))
return new Cookies(); return new Cookies();
Cookies cookies = null; Cookies cookies;
try { try {
cookies = new Cookies(map.get("expiretime")); cookies = new Cookies(map.get("expiretime"));
for (Entry<String, String> item : map.entrySet()) for (Entry<String, String> item : map.entrySet())
@ -137,12 +137,11 @@ public class IOHelper {
/** /**
* Get logged in user. It may also send logout headers if the cookies are invalid, or login headers to keep the user logged in. <b>Make sure to save the user data.</b> * Get logged in user. It may also send logout headers if the cookies are invalid, or login headers to keep the user logged in. <b>Make sure to save the user data.</b>
* *
* @param exchange * @param exchange The exchange
* @return The logged in user or null if not logged in. * @return The logged in user or null if not logged in.
* @throws IOException
*/ */
public static WebUser GetLoggedInUser(HttpExchange exchange) throws IOException { public static WebUser GetLoggedInUser(HttpExchange exchange) {
Cookies cookies = GetCookies(exchange); Cookies cookies = GetCookies(exchange);
if (!cookies.containsKey("user_id") || !cookies.containsKey("session_id")) if (!cookies.containsKey("user_id") || !cookies.containsKey("session_id"))
return null; return null;
@ -188,10 +187,10 @@ public class IOHelper {
public static HashMap<String, String> GetPOSTKeyValues(HttpExchange exchange) { public static HashMap<String, String> GetPOSTKeyValues(HttpExchange exchange) {
try { try {
String[] content = GetPOST(exchange).split("\\&"); String[] content = GetPOST(exchange).split("&");
HashMap<String, String> vars = new HashMap<>(); HashMap<String, String> vars = new HashMap<>();
for (String var : content) { for (String var : content) {
String[] spl = var.split("\\="); String[] spl = var.split("=");
if (spl.length == 1) if (spl.length == 1)
vars.put(spl[0], ""); vars.put(spl[0], "");
else else

View file

@ -3,9 +3,9 @@ package buttondevteam.website.io;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
public class Response { public class Response {
public int code; public final int code;
public String content; public final String content;
public HttpExchange exchange; public final HttpExchange exchange;
public Response(int code, String content, HttpExchange exchange) { public Response(int code, String content, HttpExchange exchange) {
this.code = code; this.code = code;

View file

@ -1,5 +1,9 @@
package buttondevteam.website.page; package buttondevteam.website.page;
import buttondevteam.website.io.Response;
import com.sun.net.httpserver.HttpExchange;
import org.bukkit.Bukkit;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -8,15 +12,8 @@ import java.net.SocketException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.bukkit.Bukkit;
import com.sun.net.httpserver.HttpExchange;
import buttondevteam.website.io.Response;
public class BridgePage extends Page { public class BridgePage extends Page {
private Map<String, Socket> connections = new HashMap<>(); private final Map<String, Socket> connections = new HashMap<>();
@Override @Override
public String GetName() { public String GetName() {
@ -101,20 +98,17 @@ public class BridgePage extends Page {
connections.values().remove(socket); connections.values().remove(socket);
} }
private int copyStream(InputStream is, OutputStream os) throws IOException { // Based on IOUtils.copy() private void copyStream(InputStream is, OutputStream os) throws IOException { // Based on IOUtils.copy()
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
long count = 0; int n;
int n = 0;
try { try {
while (-1 != (n = is.read(buffer))) { // Read is blocking while (-1 != (n = is.read(buffer))) { // Read is blocking
os.write(buffer, 0, n); os.write(buffer, 0, n);
count += n;
os.flush(); os.flush();
} }
} catch (SocketException e) { // Conection closed } catch (SocketException e) { // Conection closed
os.flush(); os.flush();
} }
return (int) count;
} }
@Override @Override

View file

@ -1,5 +1,16 @@
package buttondevteam.website.page; package buttondevteam.website.page;
import buttondevteam.lib.PluginUpdater;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.website.io.IOHelper;
import buttondevteam.website.io.Response;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.sun.net.httpserver.HttpExchange;
import org.bukkit.Bukkit;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.PublicKey; import java.security.PublicKey;
@ -10,16 +21,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.bukkit.Bukkit;
import com.google.gson.*;
import com.sun.net.httpserver.HttpExchange;
import buttondevteam.lib.PluginUpdater;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.website.io.IOHelper;
import buttondevteam.website.io.Response;
public class BuildNotificationsPage extends Page { public class BuildNotificationsPage extends Page {
@Override @Override
@ -31,8 +32,11 @@ public class BuildNotificationsPage extends Page {
private static final String publickey = ((Supplier<String>) () -> { private static final String publickey = ((Supplier<String>) () -> {
try { try {
return fromString(TBMCCoreAPI.DownloadString("https://api.travis-ci.org/config"), JsonElement pubkey = fromString(TBMCCoreAPI.DownloadString("https://api.travis-ci.org/config"),
"config.notifications.webhook.public_key").getAsString().replace("-----BEGIN PUBLIC KEY-----", "") "config.notifications.webhook.public_key");
if (pubkey == null)
return null;
return pubkey.getAsString().replace("-----BEGIN PUBLIC KEY-----", "")
.replaceAll("\n", "").replace("-----END PUBLIC KEY-----", ""); .replaceAll("\n", "").replace("-----END PUBLIC KEY-----", "");
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -47,7 +51,7 @@ public class BuildNotificationsPage extends Page {
final String payload = post.get("payload"); final String payload = post.get("payload");
if (signatures != null && signatures.size() > 0 && post.containsKey("payload") if (signatures != null && signatures.size() > 0 && post.containsKey("payload")
&& verifySignature(payload.getBytes(StandardCharsets.UTF_8), && verifySignature(payload.getBytes(StandardCharsets.UTF_8),
Base64.getDecoder().decode(signatures.get(0)), publickey)) { Base64.getDecoder().decode(signatures.get(0)))) {
Bukkit.getPluginManager() Bukkit.getPluginManager()
.callEvent(new PluginUpdater.UpdatedEvent(gson.fromJson(payload, JsonObject.class))); .callEvent(new PluginUpdater.UpdatedEvent(gson.fromJson(payload, JsonObject.class)));
return new Response(200, "All right", exchange); return new Response(200, "All right", exchange);
@ -61,9 +65,9 @@ public class BuildNotificationsPage extends Page {
// Method for signature verification that initializes with the Public Key, // Method for signature verification that initializes with the Public Key,
// updates the data to be verified and then verifies them using the signature // updates the data to be verified and then verifies them using the signature
private boolean verifySignature(byte[] data, byte[] signature, String keystr) throws Exception { private boolean verifySignature(byte[] data, byte[] signature) throws Exception {
Signature sig = Signature.getInstance("SHA1withRSA"); Signature sig = Signature.getInstance("SHA1withRSA");
sig.initVerify(getPublic(keystr)); sig.initVerify(getPublic(BuildNotificationsPage.publickey));
sig.update(data); sig.update(data);
return sig.verify(signature); return sig.verify(signature);

View file

@ -7,7 +7,6 @@ import buttondevteam.website.io.Response;
import com.google.common.collect.HashBiMap; import com.google.common.collect.HashBiMap;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -43,11 +42,7 @@ public class LoginPage extends Page {
cp.connectWith(wu = WebUser.getUser(UUID.randomUUID().toString(), WebUser.class)); //Create new user with random UUID cp.connectWith(wu = WebUser.getUser(UUID.randomUUID().toString(), WebUser.class)); //Create new user with random UUID
IOHelper.LoginUser(exchange, wu); IOHelper.LoginUser(exchange, wu);
states.remove(state); states.remove(state);
try { return IOHelper.Redirect("https://chromagaming.figytuna.com/", exchange);
return IOHelper.Redirect("https://chromagaming.figytuna.com/", exchange);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else return new Response(418, "Now what", exchange); //Minecraft doesn't have full OAuth } else return new Response(418, "Now what", exchange); //Minecraft doesn't have full OAuth
} }
return new Response(400, "Wut", exchange); return new Response(400, "Wut", exchange);

View file

@ -19,7 +19,7 @@ public abstract class Page implements HttpHandler {
public final void handle(HttpExchange exchange) { public final void handle(HttpExchange exchange) {
try { try {
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "https://chromagaming.figytuna.com"); exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "https://chromagaming.figytuna.com");
if (exactPage() ? exchange.getRequestURI().getPath().equals("/" + GetName()) : true) if (!exactPage() || exchange.getRequestURI().getPath().equals("/" + GetName()))
IOHelper.SendResponse(handlePage(exchange)); IOHelper.SendResponse(handlePage(exchange));
else { else {
IOHelper.SendResponse(404, "404 Not found: " + exchange.getRequestURI().getPath(), exchange); IOHelper.SendResponse(404, "404 Not found: " + exchange.getRequestURI().getPath(), exchange);
@ -46,8 +46,8 @@ public abstract class Page implements HttpHandler {
/** /**
* Whether to return 404 when the URL doesn't match the exact path * Whether to return 404 when the URL doesn't match the exact path
* *
* @return * @return Whether it should only match the page path
*/ */
public boolean exactPage() { public boolean exactPage() {
return true; return true;