Compare commits

..

50 commits

Author SHA1 Message Date
9d07c7bf2b
Merge branch 'dev' 2019-02-01 17:11:30 +01:00
52f306ef87
Package change once again 2019-01-30 23:56:23 +01:00
f5827adc07
Merge pull request #13 from TBMCPlugins/dev
Package change & CI branch stuff
2019-01-20 22:47:35 +01:00
a8d6c7ba78
Package change & CI branch stuff 2019-01-14 23:45:14 +01:00
6d21e25bd6
Config & proper disable
#12 and #10
2018-11-01 20:46:00 +01:00
6911df7f60
Added extra log messages 2018-09-15 14:19:18 +02:00
73b02518a4
Silent handling of bridge errors
No longer reporting breaking pipes on bridges
#11
2018-09-15 14:05:12 +02:00
a4cf26556f
Merge branch 'dev' 2018-07-20 22:29:02 +02:00
2e15619b33
Updated Bouncycastle provider 2018-07-20 22:26:50 +02:00
dbf280f941
Merge pull request #9 from TBMCPlugins/dev
Update AcmeClient to fix certificate renewal
2018-07-04 20:36:49 +02:00
173cb4caed
Updated ACME client works!
At least to the point before the challenge heh.

Also did a code analysis.
2018-07-04 19:14:22 +02:00
cca6235179
Updated Acme client, needs migrating 2018-06-12 00:14:07 +02:00
cbd8758356
Fixed HEAD req. handling 2018-05-31 00:48:16 +02:00
78ef0fed4b
Fixes, cookie fixes 2018-05-29 01:16:27 +02:00
919e57a2a9
Slightly bigger fix 2018-03-27 22:10:11 +02:00
aa4a3f02be
Small and quick fix 2018-03-27 16:24:05 +02:00
2b2a5fac50
Added MC login, needs testing
#1
2018-03-24 23:37:27 +01:00
96b200c379
Parts of those debug msgs were sorta important 2018-01-06 03:58:01 +01:00
f6ba2929a4
IT WORKS!!! - Cleanup 2018-01-06 03:46:29 +01:00
7f97683509
Added socket close handlers, fix 2018-01-06 03:32:25 +01:00
0c00c48003
We do need to flush at the right time 2018-01-06 03:05:03 +01:00
bab0fe2938
To answer my comment, yes, keep open
Still not sure it's gonna work, so I'm gonna leave those sysouts
Also I can't test locally easily because of HTTPS
2018-01-06 02:25:50 +01:00
fee4292d3b
Let's try flushing
The bad packet error is gone now, but it times out
2018-01-06 01:17:06 +01:00
c33aee426a
Sending headers to GET, HEAD handling fixed 2018-01-06 00:25:56 +01:00
0f7b7e7096 Added a message for creating a bridge 2018-01-03 20:51:09 +01:00
6f3b102a32 Added not exact page support (/bridge/id) 2018-01-02 14:05:15 +01:00
ad6126f460 Whoops 2018-01-02 03:00:26 +01:00
3d6b4f6e2a Added BridgePage for a HTTP tunnel
@iiegit
2018-01-02 02:58:18 +01:00
72e35d25d5 Added HTTP HEAD handling & better error handling
sendResponseHeaders failed silently which broke even the error message
2017-12-08 23:52:05 +01:00
3377290dfa It's getting late here 2017-11-25 02:56:55 +01:00
4099a54ce8 Fix&Fix - Totally not testing in production 2017-11-25 01:59:35 +01:00
bce6aaa428 Added HTTP server on port 80, fixes & impr.
Also ran out of time, so now it'll update on Saturdays
2017-11-25 01:37:00 +01:00
f1d21df5d8 Merge branch 'master' of GitHub repo
Conflicts:
	src/buttondevteam/website/ButtonWebsiteModule.java
	src/buttondevteam/website/WebUser.java
2017-11-23 23:31:39 +01:00
f67d886000 Fixes, changed to port 443, more fixes
Aaand I forgot to pull the master branch
2017-11-23 23:25:56 +01:00
d780d3fe35 Merge pull request #7 from TBMCPlugins/dev
Removed autoupdate, addded deploy and build notifications
2017-07-04 18:41:28 +02:00
690a7be52e Merge branch 'master' into dev 2017-07-04 18:38:37 +02:00
f0aba479e3 This should work - Fixed build notifications
Although I can't actually test it, just roughly
2017-06-30 23:58:36 +02:00
2aa02a34cb Applied changes 2017-06-30 18:21:03 +02:00
812f70f5e1 Added build notifications, fixes 2017-06-28 22:33:38 +02:00
91b64f9541 [Insert curse here]
I've done this with ChunkArchive...
2017-06-28 18:58:10 +02:00
5be1f9f73b Ofc forgot to remove this 2017-06-28 18:55:14 +02:00
609f309cd1 Removed autoupdate, addded deploy 2017-06-28 18:52:18 +02:00
897372f098 Probably should use TFTP actually 2017-06-27 23:06:51 +02:00
4c4301d2d9 Create .travis.yml 2017-06-18 21:35:42 +02:00
802e27f5f9 Create .travis.yml 2017-06-18 21:26:45 +02:00
aaa1b8e3ba Small fixes 2017-06-16 23:46:10 +02:00
195a208f67 User mgmnt fixes, login preparation 2017-06-15 22:12:02 +02:00
d32940383c Copypasted the multithreaded code
Probably works
@Alisolarflare
StackOverflow ftw
2017-06-13 22:45:07 +02:00
fc39cf6d6a Fixed access rules, added WebUser, login 2017-06-13 22:18:08 +02:00
alisolarflare
8525d1c99e Merge pull request #4 from TBMCPlugins/dev
*I have no clue what I'm doing*
2017-06-10 00:26:14 -07:00
19 changed files with 896 additions and 369 deletions

4
.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
@ -218,3 +218,5 @@ TheButtonAutoFlair/out/artifacts/Autoflair/Autoflair.jar
*.name *.name
.idea/compiler.xml .idea/compiler.xml
*.xml *.xml
upload_key

View file

@ -10,6 +10,7 @@ org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8 org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.APILeak=warning
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
@ -17,7 +18,7 @@ org.eclipse.jdt.core.compiler.problem.deadCode=warning
org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning
org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
org.eclipse.jdt.core.compiler.problem.discouragedReference=ignore org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
@ -69,12 +70,16 @@ org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning
org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning
org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled
org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info
org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore

18
.travis.yml Normal file
View file

@ -0,0 +1,18 @@
language: java
jdk:
- oraclejdk8
deploy:
# deploy develop to the staging environment
- provider: script
script: chmod +x deploy.sh && sh deploy.sh staging
on:
branch: dev
skip_cleanup: true
# deploy master to production
- provider: script
script: chmod +x deploy.sh && sh deploy.sh production
on:
branch: master
skip_cleanup: true
notifications:
webhooks: https://server.figytuna.com:8080/build_notifications

11
deploy.sh Normal file
View file

@ -0,0 +1,11 @@
#!/bin/sh
FILENAME=$(find target/ ! -name '*original*' -name '*.jar')
echo Found file: $FILENAME
if [ $1 = 'production' ]; then
echo Production mode
echo $UPLOAD_KEY > upload_key
chmod 400 upload_key
yes | scp -B -i upload_key -o StrictHostKeyChecking=no $FILENAME travis@server.figytuna.com:/minecraft/main/plugins
fi

View file

@ -3,4 +3,7 @@ main: buttondevteam.website.ButtonWebsiteModule
version: 4.0 version: 4.0
author: NorbiPeti author: NorbiPeti
depend: depend:
- ButtonCore - ThorpeCore
commands:
login:
aliases: [web, weblogin, website]

36
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>
@ -68,6 +63,9 @@
</build> </build>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<branch>
master
</branch>
</properties> </properties>
<repositories> <repositories>
@ -89,7 +87,7 @@
<dependency> <dependency>
<groupId>org.spigotmc</groupId> <groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId> <artifactId>spigot-api</artifactId>
<version>1.9.2-R0.1-SNAPSHOT</version> <version>1.12.2-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
@ -115,7 +113,7 @@
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-io --> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-io -->
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>commons-io</groupId>
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
<version>1.3.2</version> <version>1.3.2</version>
<scope>provided</scope> <scope>provided</scope>
@ -123,24 +121,40 @@
<dependency> <dependency>
<groupId>com.github.TBMCPlugins.ButtonCore</groupId> <groupId>com.github.TBMCPlugins.ButtonCore</groupId>
<artifactId>ButtonCore</artifactId> <artifactId>ButtonCore</artifactId>
<version>master-SNAPSHOT</version> <version>${branch}-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>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId> <artifactId>bcprov-jdk15on</artifactId>
<version>1.57</version> <version>1.60</version>
</dependency> </dependency>
</dependencies> </dependencies>
<profiles>
<profile>
<id>ci</id>
<activation>
<property>
<name>env.TRAVIS_BRANCH</name>
</property>
</activation>
<properties>
<!-- Override only if necessary -->
<branch>${env.TRAVIS_BRANCH}</branch>
</properties>
</profile>
</profiles>
</project> </project>

View file

@ -13,34 +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.Security;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
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.
@ -49,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;
@ -79,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!
@ -104,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
@ -143,37 +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();
if (loc != null) {
LOG.info("Loading account from file");
return new Login(loc.toURL(), kp, session).getAccount();
}
try {
// Try to create a new Registration. // Try to create a new Registration.
reg = new RegistrationBuilder().create(session); AccountBuilder ab = new AccountBuilder().useKeyPair(kp);
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);
}
return reg;
} }
/** /**
@ -181,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");
@ -228,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.");
} }
} }
@ -242,44 +253,35 @@ 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
*/ */
@SuppressWarnings("unused") public Challenge httpChallenge(Authorization auth) throws AcmeException {
public Challenge httpChallenge(Authorization auth, String domain) 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) {
throw new AcmeException("Found no " + Http01Challenge.TYPE + " challenge, don't know what to do..."); throw new AcmeException("Found no " + Http01Challenge.TYPE + " challenge, don't know what to do...");
} }
if (ButtonWebsiteModule.PORT == 80)
LOG.info("Storing the challenge data."); LOG.info("Storing the challenge data.");
else LOG.info("It should be reachable at: http://" + auth.getDomain() + "/.well-known/acme-challenge/" + challenge.getToken());
LOG.info("Store the challenge data! Can't do automatically."); ButtonWebsiteModule.addHttpPage(new AcmeChallengePage(challenge.getToken(), challenge.getAuthorization()));
LOG.info("It should be reachable at: http://" + domain + "/.well-known/acme-challenge/" + challenge.getToken()); ButtonWebsiteModule.startHttp();
LOG.info("File name: " + challenge.getToken());
LOG.info("Content: " + challenge.getAuthorization());
LOG.info("Press any key to continue...");
if (ButtonWebsiteModule.PORT != 80)
try { try {
System.in.read(); Thread.sleep(1000); // Just to make sure
} catch (IOException e) { } catch (InterruptedException e) {
e.printStackTrace();
} }
ButtonWebsiteModule.addPage(new AcmeChallengePage(challenge.getToken(), challenge.getAuthorization()));
return challenge; 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. * 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")) {
@ -287,7 +289,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

@ -1,18 +1,13 @@
package buttondevteam.website; package buttondevteam.website;
import java.io.*; import buttondevteam.lib.TBMCCoreAPI;
import java.net.InetAddress; import buttondevteam.lib.chat.TBMCChatAPI;
import java.net.InetSocketAddress; import buttondevteam.website.io.IOHelper;
import java.security.KeyPair; import buttondevteam.website.page.*;
import java.security.KeyStore; import com.sun.net.httpserver.HttpServer;
import java.security.PrivateKey; import com.sun.net.httpserver.HttpsConfigurator;
import java.security.Security; import com.sun.net.httpserver.HttpsParameters;
import java.security.cert.CertificateFactory; import com.sun.net.httpserver.HttpsServer;
import java.util.Calendar;
import javax.net.ssl.*;
import java.security.cert.Certificate;
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;
@ -20,26 +15,40 @@ import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import com.sun.net.httpserver.HttpsConfigurator; import javax.net.ssl.*;
import com.sun.net.httpserver.HttpsParameters; import java.io.*;
import com.sun.net.httpserver.HttpsServer; import java.net.*;
import java.security.KeyPair;
import buttondevteam.lib.TBMCCoreAPI; import java.security.KeyStore;
import buttondevteam.website.page.*; 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 JavaPlugin { public class ButtonWebsiteModule extends JavaPlugin {
public static final int PORT = 8080;
private static HttpsServer server; private static HttpsServer server;
/**
* For ACME validation and user redirection
*/
private static HttpServer httpserver;
private static boolean enabled;
public ButtonWebsiteModule() { public ButtonWebsiteModule() {
try { try {
server = HttpsServer.create(new InetSocketAddress((InetAddress) null, PORT), 10); 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"); SSLContext sslContext = SSLContext.getInstance("TLS");
// 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);
@ -50,9 +59,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());
@ -99,42 +108,93 @@ public class ButtonWebsiteModule extends JavaPlugin {
} }
} }
}); });
enabled = true;
} 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); enabled = false; //It's not even enabled yet, so we need a variable
} }
} }
@Override @Override
public void onEnable() { public void onEnable() {
if (!enabled) {
getServer().getPluginManager().disablePlugin(this);
return;
}
addPage(new IndexPage()); addPage(new IndexPage());
addPage(new LoginPage());
addPage(new ProfilePage());
addPage(new BuildNotificationsPage());
addPage(new BridgePage());
TBMCCoreAPI.RegisterUserClass(WebUser.class);
TBMCChatAPI.AddCommand(this, LoginCommand.class);
Bukkit.getScheduler().runTaskAsynchronously(this, () -> { Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
this.getLogger().info("Starting webserver..."); this.getLogger().info("Starting webserver...");
((Runnable) server::start).run(); // Totally normal way of calling a method server.setExecutor(
this.getLogger().info("Webserver started"); 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(); final Calendar calendar = Calendar.getInstance();
if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) { // Only update every week if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY && !TBMCCoreAPI.IsTestServer()) { // Only update every week
Thread t = new Thread(() -> AcmeClient.main("server.figytuna.com")); Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
t.setContextClassLoader(getClass().getClassLoader()); AcmeClient.main("server.figytuna.com"); // Task is running async so we don't need an extra thread
t.start();
} }
((Runnable) server::start).run(); // Totally normal way of calling a method
if (!httpstarted)
httpserver.start();
this.getLogger().info("Webserver started");
}); });
} }
private static boolean httpstarted = false;
/**
* Used to start the server when the ACME client needs it
*/
static void startHttp() {
httpserver.start();
httpstarted = true;
}
/** /**
* Adds a new page/endpoint to the website. This method needs to be called before the server finishes loading (onEnable). * 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) { public static void addPage(Page page) {
if (!enabled)
return;
server.createContext("/" + page.GetName(), page); server.createContext("/" + page.GetName(), page);
} }
private static InputStream fullStream(String fname) throws IOException { /**
FileInputStream fis = new FileInputStream(fname); * Adds an <b>insecure</b> endpoint to the website. This should be avoided when possible.
*/
public static void addHttpPage(Page page) {
if (!enabled)
return;
httpserver.createContext("/" + page.GetName(), page);
}
static void storeRegistration(URL location) {
final ButtonWebsiteModule plugin = getPlugin(ButtonWebsiteModule.class);
plugin.getConfig().set("registration", location.toString());
plugin.saveConfig();
}
static URI getRegistration() {
try {
String str = getPlugin(ButtonWebsiteModule.class).getConfig().getString("registration");
return str == null ? null : new URI(str);
} catch (URISyntaxException e) {
e.printStackTrace();
return null;
}
}
private static InputStream fullStream(File f) throws IOException {
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

@ -0,0 +1,27 @@
package buttondevteam.website;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.PlayerCommandBase;
import buttondevteam.website.page.LoginPage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@CommandClass
public class LoginCommand extends PlayerCommandBase {
@Override //TODO: Ask about linking already existing accounts, to prevent linking someone else's
public boolean OnCommand(Player player, String s, String[] strings) {
String state = LoginPage.generateState("minecraft", player.getUniqueId().toString()).toString();
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "tellraw " + player.getName() + " [\"\",{\"text\":\"Please \",\"color\":\"aqua\"},{\"text\":\"Click Here\",\"color\":\"aqua\",\"bold\":true,\"underlined\":true,\"clickEvent\":{\"action\":\"open_url\",\"value\":\"https://server.figytuna.com/login?type=minecraft&state=" + state + "\"}},{\"text\":\" to log in to our site using your Minecraft account.\",\"color\":\"aqua\",\"bold\":false,\"underlined\":false}]");
return true;
}
@Override
public String[] GetHelpText(String s) {
return new String[]{//
"§6---- Login ----", //
"This command allows you to log in to our website using your Minecraft account.", //
"If you are already logged in to the site, you can connect your MC account to it.", //
"This is good for getting Minecraft rewards if you're a patron for example." //
};
}
}

View file

@ -0,0 +1,22 @@
package buttondevteam.website;
import java.util.UUID;
import buttondevteam.lib.player.ChromaGamerBase;
import buttondevteam.lib.player.PlayerData;
import buttondevteam.lib.player.UserClass;
@UserClass(foldername = "web")
public class WebUser extends ChromaGamerBase {
private UUID uuid;
public UUID getUUID() {
if (uuid == null)
uuid = UUID.fromString(getFileName());
return uuid;
}
public PlayerData<UUID> sessionID() {
return data(new UUID(0, 0)); //It's used with toString() directly, so can't be null
}
}

View file

@ -1,5 +1,7 @@
package buttondevteam.website.io; package buttondevteam.website.io;
import com.sun.net.httpserver.HttpExchange;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.Period; import java.time.Period;
import java.time.ZoneId; import java.time.ZoneId;
@ -7,8 +9,6 @@ import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.HashMap; import java.util.HashMap;
import com.sun.net.httpserver.HttpExchange;
public class Cookies extends HashMap<String, Cookie> { public class Cookies extends HashMap<String, Cookie> {
private static final long serialVersionUID = -328053564170765287L; private static final long serialVersionUID = -328053564170765287L;
@ -30,11 +30,11 @@ public class Cookies extends HashMap<String, Cookie> {
this.expiretime = ZonedDateTime.now(ZoneId.of("GMT")).format(DateTimeFormatter.RFC_1123_DATE_TIME); this.expiretime = ZonedDateTime.now(ZoneId.of("GMT")).format(DateTimeFormatter.RFC_1123_DATE_TIME);
} }
public void SendHeaders(HttpExchange exchange) { public void AddHeaders(HttpExchange exchange) {
for (Entry<String, Cookie> item : entrySet()) for (Entry<String, Cookie> item : entrySet())
exchange.getResponseHeaders().add("Set-Cookie", exchange.getResponseHeaders().add("Set-Cookie",
item.getKey() + "=" + item.getValue().getValue() + "; expires=" + expiretime); item.getKey() + "=" + item.getValue().getValue() + "; expires=" + expiretime + "; Secure; HttpOnly; Domain=figytuna.com"); //Allow for frontend
exchange.getResponseHeaders().add("Set-Cookie", "expiretime=" + expiretime + "; expires=" + expiretime); exchange.getResponseHeaders().add("Set-Cookie", "expiretime=" + expiretime + "; expires=" + expiretime + "; Secure; HttpOnly; Domain=figytuna.com");
} }
public Cookies add(Cookie cookie) { public Cookies add(Cookie cookie) {

View file

@ -1,32 +1,48 @@
package buttondevteam.website.io; package buttondevteam.website.io;
import java.io.BufferedOutputStream; import buttondevteam.lib.player.ChromaGamerBase;
import java.io.ByteArrayInputStream; import buttondevteam.website.WebUser;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.logging.Level;
import org.apache.commons.io.IOUtils;
import org.bukkit.Bukkit;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.sun.net.httpserver.HttpExchange; 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.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
public class IOHelper { public class IOHelper {
public static void SendResponse(Response resp) throws IOException { public static void SendResponse(Response resp) throws IOException {
if (resp == null)
return; // Response is already sent
SendResponse(resp.code, resp.content, resp.exchange); SendResponse(resp.code, resp.content, resp.exchange);
} }
public static void SendResponse(int code, String content, HttpExchange exchange) throws IOException { public static void SendResponse(int code, String content, HttpExchange exchange) throws IOException {
if (exchange.getRequestMethod().equalsIgnoreCase("HEAD")) {
exchange.sendResponseHeaders(200, -1); // -1 indicates no data
//exchange.getResponseBody().close(); - No stream is created for HEAD requests
return;
}
try (BufferedOutputStream out = new BufferedOutputStream(exchange.getResponseBody())) { try (BufferedOutputStream out = new BufferedOutputStream(exchange.getResponseBody())) {
try (ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { try (ByteArrayInputStream bis = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
try {
exchange.sendResponseHeaders(code, bis.available()); exchange.sendResponseHeaders(code, bis.available());
} catch (IOException e) {
if (!e.getMessage().equals("headers already sent"))
throw e; // If an error occurs after sending the response headers send the error page even if the headers are for the original
} // This code will send *some page* (most likely an error page) with the original headers instead of failing to do anything
byte[] buffer = new byte[512]; byte[] buffer = new byte[512];
int count; int count;
while ((count = bis.read(buffer)) != -1) { while ((count = bis.read(buffer)) != -1) {
@ -41,8 +57,7 @@ public class IOHelper {
try { try {
if (exchange.getRequestBody().available() == 0) if (exchange.getRequestBody().available() == 0)
return ""; return "";
String content = IOUtils.toString(exchange.getRequestBody(), "UTF-8"); return IOUtils.toString(exchange.getRequestBody(), "UTF-8");
return content;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return ""; return "";
@ -57,8 +72,7 @@ public class IOHelper {
JsonElement e = new JsonParser().parse(content); JsonElement e = new JsonParser().parse(content);
if (e == null) if (e == null)
return null; return null;
JsonObject obj = e.getAsJsonObject(); return e.getAsJsonObject();
return obj;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return null; return null;
@ -67,34 +81,31 @@ public class IOHelper {
/** /**
* Sends login headers and sets the session id on the user * Sends login headers and sets the session id on the user
*
* @param exchange
* @param user
*/ */
/*public static void LoginUser(HttpExchange exchange, User user) { public static void LoginUser(HttpExchange exchange, WebUser user) {
Bukkit.getLogger().fine("Logging in user: " + user); Bukkit.getLogger().fine("Logging in user: " + user);
// provider.SetValues(() -> user.sessionID().set(UUID.randomUUID());
// user.setSessionid(UUID.randomUUID().toString())); user.save();
user.setSessionid(UUID.randomUUID().toString()); new Cookies(2).add(new Cookie("user_id", user.getUUID() + ""))
new Cookies(2).add(new Cookie("user_id", user.getId() + "")).add(new Cookie("session_id", user.getSessionid())) .add(new Cookie("session_id", user.sessionID().get().toString())).AddHeaders(exchange);
.SendHeaders(exchange);
Bukkit.getLogger().fine("Logged in user."); Bukkit.getLogger().fine("Logged in user.");
} }
public static void LogoutUser(HttpExchange exchange, User user) { public static void LogoutUser(HttpExchange exchange, WebUser user) {
user.setSessionid(new UUID(0, 0).toString()); user.sessionID().set(new UUID(0, 0));
user.save();
SendLogoutHeaders(exchange); SendLogoutHeaders(exchange);
} }
private static void SendLogoutHeaders(HttpExchange exchange) { private static void SendLogoutHeaders(HttpExchange exchange) {
String expiretime = "Sat, 19 Mar 2016 23:33:00 GMT"; String expiretime = "Sat, 19 Mar 2016 23:33:00 GMT";
new Cookies(expiretime).add(new Cookie("user_id", "del")).add(new Cookie("session_id", "del")) new Cookies(expiretime).add(new Cookie("user_id", "del")).add(new Cookie("session_id", "del"))
.SendHeaders(exchange); .AddHeaders(exchange);
} }
public static void 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);
IOHelper.SendResponse(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);
} }
public static Cookies GetCookies(HttpExchange exchange) { public static Cookies GetCookies(HttpExchange exchange) {
@ -102,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]);
@ -112,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())
@ -122,27 +133,73 @@ public class IOHelper {
return new Cookies(); return new Cookies();
} }
return cookies; return cookies;
}*/ }
/** /**
* Get logged in user. It may also send logout headers if the cookies are invalid, or login headers to keep the user logged in. * 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 User 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;
User user = DataManager.load(User.class, Long.parseLong(cookies.get("user_id").getValue()), false); WebUser user = ChromaGamerBase.getUser(cookies.get("user_id").getValue(), WebUser.class);
if (user != null && cookies.get("session_id") != null if (user != null && cookies.get("session_id") != null
&& cookies.get("session_id").getValue().equals(user.getSessionid())) { && cookies.get("session_id").getValue().equals(user.sessionID().get().toString())) {
if (cookies.getExpireTimeParsed().minusYears(1).isBefore(ZonedDateTime.now(ZoneId.of("GMT")))) if (cookies.getExpireTimeParsed().minusYears(1).isBefore(ZonedDateTime.now(ZoneId.of("GMT"))))
LoginUser(exchange, user); LoginUser(exchange, user);
return user; return user;
} else } else
SendLogoutHeaders(exchange); SendLogoutHeaders(exchange);
return null; return null;
}*/ }
public static Map<String, String> parseQueryString(HttpExchange exchange) {
String qs = exchange.getRequestURI().getRawQuery();
Map<String, String> result = new HashMap<>();
if (qs == null)
return result;
int last = 0, next, l = qs.length();
while (last < l) {
next = qs.indexOf('&', last);
if (next == -1)
next = l;
if (next > last) {
int eqPos = qs.indexOf('=', last);
try {
if (eqPos < 0 || eqPos > next)
result.put(URLDecoder.decode(qs.substring(last, next), "utf-8"), "");
else
result.put(URLDecoder.decode(qs.substring(last, eqPos), "utf-8"),
URLDecoder.decode(qs.substring(eqPos + 1, next), "utf-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // will never happen, utf-8 support is mandatory for java
}
}
last = next + 1;
}
return result;
}
public static HashMap<String, String> GetPOSTKeyValues(HttpExchange exchange) {
try {
String[] content = GetPOST(exchange).split("&");
HashMap<String, String> vars = new HashMap<>();
for (String var : content) {
String[] spl = var.split("=");
if (spl.length == 1)
vars.put(spl[0], "");
else
vars.put(spl[0], URLDecoder.decode(spl[1], "utf-8"));
}
return vars;
} catch (Exception e) {
e.printStackTrace();
return new HashMap<>();
}
}
} }

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

@ -6,9 +6,9 @@ import buttondevteam.website.io.Response;
public class AcmeChallengePage extends Page { public class AcmeChallengePage extends Page {
public AcmeChallengePage(String token, String content) { public AcmeChallengePage(String token, String content) { // The page name needs to be known before server start
this.token = token; AcmeChallengePage.token = token;
this.content = content; AcmeChallengePage.content = content;
} }
@Override @Override
@ -23,7 +23,7 @@ public class AcmeChallengePage extends Page {
return new Response(200, content, exchange); return new Response(200, content, exchange);
} }
private String token; private static String token;
private String content; private static String content;
} }

View file

@ -0,0 +1,128 @@
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.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class BridgePage extends Page {
private final Map<String, Socket> connections = new HashMap<>();
@Override
public String GetName() {
return "bridge";
}
@Override
public Response handlePage(HttpExchange exchange) {
String method = exchange.getRequestMethod().toUpperCase();
String id = getConnID(exchange);
if (id == null)
return new Response(400, "No ID", exchange);
try {
Socket s;
switch (method) {
case "POST":
if (connections.containsKey(id))
connections.get(id).close();
Socket socket = new Socket("localhost", Bukkit.getPort());
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
connections.put(id, socket);
System.out.println("[BWM] Created a bridge: " + id);
return new Response(201, "You know what you created. A bridge.", exchange);
case "PUT":
s = getSocket(exchange);
if (s == null)
return new Response(400, "No connection", exchange);
if (s.isClosed())
return new Response(410, "Socket Gone", exchange);
copyStream(exchange.getRequestBody(), s.getOutputStream());
// Don't close the socket, PUT messages are sent individually
return new Response(200, "OK", exchange);
case "GET":
s = getSocket(exchange);
if (s == null)
return new Response(400, "No connection", exchange);
if (s.isClosed())
return new Response(410, "Socket Gone", exchange);
try {
exchange.sendResponseHeaders(200, 0); // Chunked transfer, any amount of data
copyStream(s.getInputStream(), exchange.getResponseBody());
exchange.getResponseBody().close(); // It'll only get here when the communication is already done
} catch (IOException ex) { //Failed to send it over HTTP, GET connection closed
closeSocket(exchange); //We only have one GET, connection over
System.out.println("[BWM] [" + id + "] over (GET): " + ex.toString());
}
return null; // Response already sent
case "DELETE":
System.out.println("[BWM] [" + id + "] delet this");
closeSocket(exchange);
return new Response(200, "OK", exchange);
default:
return new Response(403, "Unknown request", exchange);
}
} catch (IOException e) {
if (e instanceof SocketException) {
closeSocket(exchange);
System.out.println("[BWM] [" + id + "] closed: " + e.toString());
return new Response(410, "Socket Gone because of error: " + e, exchange);
}
e.printStackTrace();
return new Response(500, "Internal Server Error: " + e, exchange);
}
}
private Socket getSocket(HttpExchange exchange) {
String id = getConnID(exchange);
if (id == null)
return null;
return connections.get(id);
}
private String getConnID(HttpExchange exchange) {
String path = exchange.getRequestURI().getPath();
if (path == null)
return null;
String[] spl = path.split("/");
if (spl.length < 2)
return null;
return spl[spl.length - 1];
}
private void closeSocket(HttpExchange exchange) {
Socket socket = getSocket(exchange);
if (socket == null)
return;
try {
socket.close();
} catch (IOException ignored) {
}
connections.values().remove(socket);
}
private void copyStream(InputStream is, OutputStream os) throws IOException { // Based on IOUtils.copy()
byte[] buffer = new byte[4096];
int n;
try {
while (-1 != (n = is.read(buffer))) { // Read is blocking
os.write(buffer, 0, n);
os.flush();
}
} catch (SocketException e) { // Conection closed
os.flush();
}
}
@Override
public boolean exactPage() {
return false;
}
}

View file

@ -0,0 +1,100 @@
package buttondevteam.website.page;
import buttondevteam.core.component.updater.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.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.function.Supplier;
public class BuildNotificationsPage extends Page {
@Override
public String GetName() {
return "build_notifications";
}
private static final Gson gson = new Gson();
private static final String publickey = ((Supplier<String>) () -> {
try {
JsonElement pubkey = fromString(TBMCCoreAPI.DownloadString("https://api.travis-ci.org/config"),
"config.notifications.webhook.public_key");
if (pubkey == null)
return null;
return pubkey.getAsString().replace("-----BEGIN PUBLIC KEY-----", "")
.replaceAll("\n", "").replace("-----END PUBLIC KEY-----", "");
} catch (Exception e) {
throw new RuntimeException(e);
}
}).get();
@Override
public Response handlePage(HttpExchange exchange) {
HashMap<String, String> post = IOHelper.GetPOSTKeyValues(exchange);
try {
final List<String> signatures = exchange.getRequestHeaders().get("Signature");
final String payload = post.get("payload");
if (signatures != null && signatures.size() > 0 && post.containsKey("payload")
&& verifySignature(payload.getBytes(StandardCharsets.UTF_8),
Base64.getDecoder().decode(signatures.get(0)))) {
Bukkit.getPluginManager()
.callEvent(new PluginUpdater.UpdatedEvent(gson.fromJson(payload, JsonObject.class)));
return new Response(200, "All right", exchange);
}
} catch (Exception e) {
return new Response(400,
"Invalid data, error: " + e + " If you're messing with this, stop messing with this.", exchange); // Blame the user
}
return new Response(400, "Verification failed", exchange);
}
// Method for signature verification that initializes with the Public Key,
// updates the data to be verified and then verifies them using the signature
private boolean verifySignature(byte[] data, byte[] signature) throws Exception {
Signature sig = Signature.getInstance("SHA1withRSA");
sig.initVerify(getPublic(BuildNotificationsPage.publickey));
sig.update(data);
return sig.verify(signature);
}
// Method to retrieve the Public Key from a file
public PublicKey getPublic(String keystr) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(keystr);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(spec);
}
public static JsonElement fromString(String json, String path) throws JsonSyntaxException {
JsonObject obj = gson.fromJson(json, JsonObject.class);
String[] seg = path.split("\\.");
for (String element : seg) {
if (obj != null) {
JsonElement ele = obj.get(element);
if (!ele.isJsonObject())
return ele;
else
obj = ele.getAsJsonObject();
} else {
return null;
}
}
return obj;
}
}

View file

@ -0,0 +1,68 @@
package buttondevteam.website.page;
import buttondevteam.lib.player.TBMCPlayer;
import buttondevteam.website.WebUser;
import buttondevteam.website.io.IOHelper;
import buttondevteam.website.io.Response;
import com.google.common.collect.HashBiMap;
import com.sun.net.httpserver.HttpExchange;
import java.util.Map;
import java.util.UUID;
public class LoginPage extends Page {
@Override
public String GetName() {
return "login";
}
@Override
public Response handlePage(HttpExchange exchange) {
Map<String, String> q = IOHelper.parseQueryString(exchange);
if (q == null || !q.containsKey("type"))
return new Response(400, "400 Bad request", exchange);
String type = q.get("type");
/*if (type.equalsIgnoreCase("getstate"))
return new Response(200, "TODO", exchange); // TO!DO: Store and return a random state and check on other types
String state = q.get("state"), code = q.get("code");*/
Response nope = new Response(401, "401 Nope", exchange);
if (type.equalsIgnoreCase("minecraft")) {
//In case of Minecraft, we don't need the full OAuth2 flow, we only need to ensure the state matches
if (q.containsKey("state")) {
UUID state = UUID.fromString(q.get("state"));
if (!states.containsKey(state))
return nope;
String[] folder_id = states.get(state).split(" ");
if (!folder_id[0].equalsIgnoreCase(type)) //TODO: Use for other OAuth stuff as well
return nope;
TBMCPlayer cp = TBMCPlayer.getPlayer(UUID.fromString(folder_id[1]), TBMCPlayer.class);
WebUser wu = cp.getAs(WebUser.class);
if (wu == null) //getAs return Optional?
cp.connectWith(wu = WebUser.getUser(UUID.randomUUID().toString(), WebUser.class)); //Create new user with random UUID
IOHelper.LoginUser(exchange, wu);
states.remove(state);
return IOHelper.Redirect("https://chromagaming.figytuna.com/", exchange);
} else return new Response(418, "Now what", exchange); //Minecraft doesn't have full OAuth
}
return new Response(400, "Wut", exchange);
}
/**
* Value: Folder ID (don't use dashes as a separator... UUIDs contain them)
*/
private static final HashBiMap<UUID, String> states = HashBiMap.create();
/**
* Generates a temporary state data that can be used to authenticate a user.
*
* @param type The service type. Only used to separate in temporary storage.
* @param id The user id in the service. Only used to separate in temporary storage.
* @return A unique state that can be used to authenticate a user.
*/
public static UUID generateState(String type, String id) {
UUID state = UUID.randomUUID();
states.forcePut(state, type + " " + id); //Replace existing for an user
return state;
}
}

View file

@ -1,44 +1,28 @@
package buttondevteam.website.page; package buttondevteam.website.page;
import java.io.PrintStream;
import org.apache.commons.io.output.ByteArrayOutputStream;
import com.sun.net.httpserver.*;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.website.io.IOHelper; import buttondevteam.website.io.IOHelper;
import buttondevteam.website.io.Response; 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.PrintStream;
/** /**
* Add to {@link Main}.Pages * Add using {@link buttondevteam.website.ButtonWebsiteModule#addPage(Page)}
*/ */
public abstract class Page implements HttpHandler { public abstract class Page implements HttpHandler {
public abstract String GetName(); public abstract String GetName();
@Override @Override
public void handle(HttpExchange exchange) { public final void handle(HttpExchange exchange) {
//Creates a new thread to handle the request
Handler handler = new Handler(exchange);
handler.start();
}
class Handler extends Thread{
HttpExchange exchange;
public Handler(HttpExchange exchange){
this.exchange = exchange;
}
@Override
public void run(){
try { try {
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "https://tbmcplugins.github.io"); exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "https://chromagaming.figytuna.com");
if (exchange.getRequestURI().getPath().equals("/" + GetName())) if (!exactPage() || exchange.getRequestURI().getPath().equals("/" + GetName()))
IOHelper.SendResponse(handlePage(exchange)); IOHelper.SendResponse(handlePage(exchange));
else { else {
IOHelper.SendResponse(404, "404 Not found", exchange); IOHelper.SendResponse(404, "404 Not found: " + exchange.getRequestURI().getPath(), exchange);
} }
} catch (Exception e) { } catch (Exception e) {
TBMCCoreAPI.SendException("Internal Server Error in ButtonWebsiteModule!", e); TBMCCoreAPI.SendException("Internal Server Error in ButtonWebsiteModule!", e);
@ -50,15 +34,22 @@ public abstract class Page implements HttpHandler {
str.print("</pre>"); str.print("</pre>");
IOHelper.SendResponse(500, baos.toString("UTF-8"), exchange); IOHelper.SendResponse(500, baos.toString("UTF-8"), exchange);
} catch (Exception e1) { } catch (Exception e1) {
e1.printStackTrace(); TBMCCoreAPI.SendException("Exception while sending Internal Server Error in ButtonWebsiteModule!", e1);
} }
} }
} }
}
/** /**
* The main logic of the endpoint. Use IOHelper to retrieve the message sent and other things. * The main logic of the endpoint. Use IOHelper to retrieve the message sent and other things.
*/ */
public abstract Response handlePage(HttpExchange exchange); public abstract Response handlePage(HttpExchange exchange);
/**
* Whether to return 404 when the URL doesn't match the exact path
*
* @return Whether it should only match the page path
*/
public boolean exactPage() {
return true;
}
} }

View file

@ -0,0 +1,19 @@
package buttondevteam.website.page;
import com.sun.net.httpserver.HttpExchange;
import buttondevteam.website.io.Response;
public class ProfilePage extends Page {
@Override
public String GetName() {
return "profile";
}
@Override
public Response handlePage(HttpExchange exchange) {
return new Response(200, "Under construction", exchange);
}
}