Added ACME client and possibility to use the cert with the HttpServer
This commit is contained in:
parent
2a79a2ba38
commit
a27873bedd
11 changed files with 527 additions and 89 deletions
11
build.gradle
11
build.gradle
|
|
@ -9,14 +9,19 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'commons-fileupload:commons-fileupload:1.4'
|
|
||||||
implementation 'commons-io:commons-io:2.7'
|
|
||||||
implementation 'org.dom4j:dom4j:2.1.3'
|
implementation 'org.dom4j:dom4j:2.1.3'
|
||||||
implementation 'org.xerial:sqlite-jdbc:3.8.11.2'
|
|
||||||
|
|
||||||
compileOnly 'mysql:mysql-connector-java:8.0.16'
|
compileOnly 'mysql:mysql-connector-java:8.0.16'
|
||||||
|
compileOnly 'org.xerial:sqlite-jdbc:3.8.11.2'
|
||||||
|
|
||||||
compileOnly 'javax.servlet:javax.servlet-api:3.1.0'
|
compileOnly 'javax.servlet:javax.servlet-api:3.1.0'
|
||||||
|
|
||||||
|
compileOnly 'org.shredzone.acme4j:acme4j-client:2.12'
|
||||||
|
compileOnly 'org.shredzone.acme4j:acme4j-utils:2.12'
|
||||||
|
|
||||||
|
compileOnly 'commons-fileupload:commons-fileupload:1.4'
|
||||||
|
compileOnly 'commons-io:commons-io:2.7'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.1'
|
testImplementation 'junit:junit:4.13.1'
|
||||||
testImplementation 'org.hamcrest:hamcrest-core:1.3'
|
testImplementation 'org.hamcrest:hamcrest-core:1.3'
|
||||||
testImplementation 'com.carrotsearch:junit-benchmarks:0.7.2'
|
testImplementation 'com.carrotsearch:junit-benchmarks:0.7.2'
|
||||||
|
|
|
||||||
227
src/zutil/net/acme/AcmeClient.java
Normal file
227
src/zutil/net/acme/AcmeClient.java
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
package zutil.net.acme;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.shredzone.acme4j.Account;
|
||||||
|
import org.shredzone.acme4j.AccountBuilder;
|
||||||
|
import org.shredzone.acme4j.Authorization;
|
||||||
|
import org.shredzone.acme4j.Certificate;
|
||||||
|
import org.shredzone.acme4j.Order;
|
||||||
|
import org.shredzone.acme4j.Session;
|
||||||
|
import org.shredzone.acme4j.Status;
|
||||||
|
import org.shredzone.acme4j.challenge.Http01Challenge;
|
||||||
|
import org.shredzone.acme4j.exception.AcmeException;
|
||||||
|
import org.shredzone.acme4j.util.CSRBuilder;
|
||||||
|
import org.shredzone.acme4j.util.KeyPairUtils;
|
||||||
|
import zutil.StringUtil;
|
||||||
|
import zutil.log.LogUtil;
|
||||||
|
import zutil.net.http.HttpServer;
|
||||||
|
import zutil.net.http.page.HttpStaticContentPage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class implementing the ACME protocol (Automatic Certificate Management Environment) used by the LetsEncrypt service.
|
||||||
|
*
|
||||||
|
* Code based on the example from the acme4j project:
|
||||||
|
* https://github.com/shred/acme4j/blob/master/acme4j-example/src/main/java/org/shredzone/acme4j/example/ClientTest.java
|
||||||
|
*/
|
||||||
|
public class AcmeClient {
|
||||||
|
private static final Logger logger = LogUtil.getLogger();
|
||||||
|
|
||||||
|
public static final String ACME_SERVER_LETSENCRYPT_PRODUCTION = "acme://letsencrypt.org";
|
||||||
|
public static final String ACME_SERVER_LETSENCRYPT_STAGING = "acme://letsencrypt.org/staging";
|
||||||
|
|
||||||
|
private static final int KEY_SIZE = 2048; // RSA key size of generated key pairs
|
||||||
|
|
||||||
|
|
||||||
|
private String acmeServerUrl;
|
||||||
|
private AcmeDataStore dataStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of the ACME Client
|
||||||
|
*/
|
||||||
|
public AcmeClient(AcmeDataStore dataStore, String acmeServerUrl) {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
|
||||||
|
this.dataStore = dataStore;
|
||||||
|
this.acmeServerUrl = acmeServerUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AcmeClient(AcmeDataStore dataStore) {
|
||||||
|
this(dataStore, ACME_SERVER_LETSENCRYPT_PRODUCTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a certificate for the given domains. Also takes care for the registration
|
||||||
|
* process.
|
||||||
|
*
|
||||||
|
* @param httpServer the web server where the challenge and response will be performed on and where the certificate will be applied to.
|
||||||
|
* @param domains the domains to get a certificates for
|
||||||
|
* @return a certificate for the given domains.
|
||||||
|
*/
|
||||||
|
public X509Certificate fetchCertificate(HttpServer httpServer, String... domains) throws IOException, AcmeException {
|
||||||
|
// ------------------------------------------------
|
||||||
|
// Read in keys
|
||||||
|
// ------------------------------------------------
|
||||||
|
|
||||||
|
KeyPair userKeyPair = dataStore.loadUserKeyPair(); // Load the user key file. If there is no key file, create a new one.
|
||||||
|
KeyPair domainKeyPair = dataStore.loadDomainKeyPair(); // Load or create a key pair for the domains. This should not be the userKeyPair!
|
||||||
|
|
||||||
|
if (userKeyPair == null) {
|
||||||
|
userKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
|
||||||
|
dataStore.storeUserKeyPair(userKeyPair);
|
||||||
|
}
|
||||||
|
if (domainKeyPair == null) {
|
||||||
|
domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE);
|
||||||
|
dataStore.storeDomainKeyPair(domainKeyPair);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// Start authorization process
|
||||||
|
// ------------------------------------------------
|
||||||
|
|
||||||
|
Session session = new Session(acmeServerUrl);
|
||||||
|
Account acct = getAccount(session, userKeyPair); // Get the Account. If there is no account yet, create a new one.
|
||||||
|
Order order = acct.newOrder().domains(domains).create(); // Order the certificate
|
||||||
|
|
||||||
|
// Perform all required authorizations
|
||||||
|
for (Authorization auth : order.getAuthorizations()) {
|
||||||
|
execHttpChallenge(auth, httpServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a "Certificate Signing Request" for all of the domains, and sign it with the domain key pair.
|
||||||
|
CSRBuilder csrBuilder = new CSRBuilder();
|
||||||
|
csrBuilder.addDomains(domains);
|
||||||
|
csrBuilder.sign(domainKeyPair);
|
||||||
|
|
||||||
|
order.execute(csrBuilder.getEncoded()); // Order the certificate
|
||||||
|
|
||||||
|
// Wait for the order to complete
|
||||||
|
try {
|
||||||
|
for (int attempts = 0; attempts < 10; attempts--) {
|
||||||
|
// Did the order pass or fail?
|
||||||
|
if (order.getStatus() == Status.VALID) {
|
||||||
|
break;
|
||||||
|
} else if (order.getStatus() == Status.INVALID) {
|
||||||
|
throw new AcmeException("Certificate order has failed, reason: " + order.getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a few seconds
|
||||||
|
Thread.sleep(100L + 500L * attempts);
|
||||||
|
|
||||||
|
// Then update the status
|
||||||
|
order.update();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
logger.log(Level.SEVERE, "Interrupted", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the certificate
|
||||||
|
Certificate certificate = order.getCertificate();
|
||||||
|
|
||||||
|
logger.info("The certificate for domains '" + StringUtil.join(",", domains) + "' has been successfully generated.");
|
||||||
|
|
||||||
|
return certificate.getCertificate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 account will be
|
||||||
|
* created.
|
||||||
|
* <p>
|
||||||
|
* This is a simple way of finding your {@link Account}. A better way is to get the
|
||||||
|
* URL of your new account with {@link Account#getLocation()} and store it somewhere.
|
||||||
|
* If you need to get access to your account later, reconnect to it via {@link
|
||||||
|
* Session#login(URL, KeyPair)} by using the stored location.
|
||||||
|
*
|
||||||
|
* @param session {@link Session} to bind with
|
||||||
|
* @return {@link Account}
|
||||||
|
*/
|
||||||
|
private Account getAccount(Session session, KeyPair accountKey) throws AcmeException {
|
||||||
|
// Ask the user to accept the TOS, if server provides us with a link.
|
||||||
|
URI tos = session.getMetadata().getTermsOfService();
|
||||||
|
if (tos != null) {
|
||||||
|
logger.info("By using this service you accept the Terms of Service: " + tos);
|
||||||
|
}
|
||||||
|
|
||||||
|
Account account = new AccountBuilder()
|
||||||
|
.agreeToTermsOfService()
|
||||||
|
.useKeyPair(accountKey)
|
||||||
|
.create(session);
|
||||||
|
logger.info("Registered a new user, URL: " + account.getLocation());
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorize a domain. It will be associated with your account, so you will be able to
|
||||||
|
* retrieve a signed certificate for the domain later.
|
||||||
|
*
|
||||||
|
* @param auth {@link Authorization} to perform
|
||||||
|
*/
|
||||||
|
private void execHttpChallenge(Authorization auth, HttpServer httpServer) throws AcmeException {
|
||||||
|
logger.info("Authorization for domain: " + auth.getIdentifier().getDomain());
|
||||||
|
|
||||||
|
// The authorization is already valid. No need to process a challenge.
|
||||||
|
if (auth.getStatus() == Status.VALID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the desired challenge and prepare it.
|
||||||
|
Http01Challenge challenge = auth.findChallenge(Http01Challenge.class);
|
||||||
|
if (challenge == null) {
|
||||||
|
throw new AcmeException("Found no " + Http01Challenge.TYPE + " challenge.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the challenge is already verified, there's no need to execute it again.
|
||||||
|
if (challenge.getStatus() == Status.VALID)
|
||||||
|
return;
|
||||||
|
|
||||||
|
String url = "http://" + auth.getIdentifier().getDomain();
|
||||||
|
String path = "/.well-known/acme-challenge/" + challenge.getToken();
|
||||||
|
String content = challenge.getAuthorization();
|
||||||
|
|
||||||
|
// Output the challenge, wait for acknowledge...
|
||||||
|
logger.fine("Adding challenge HttpPage at: " + url + path);
|
||||||
|
httpServer.setPage(path, new HttpStaticContentPage(content));
|
||||||
|
|
||||||
|
// Now trigger the challenge.
|
||||||
|
challenge.trigger();
|
||||||
|
|
||||||
|
// Poll for the challenge to complete.
|
||||||
|
try {
|
||||||
|
for (int attempts = 0; attempts < 10; attempts--) {
|
||||||
|
// Did the authorization fail?
|
||||||
|
if (challenge.getStatus() == Status.VALID) {
|
||||||
|
break;
|
||||||
|
} else if (challenge.getStatus() == Status.INVALID) {
|
||||||
|
throw new AcmeException("Certificate challenge failed: " + challenge.getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a few seconds
|
||||||
|
Thread.sleep(100L + 500L * attempts);
|
||||||
|
|
||||||
|
// Then update the status
|
||||||
|
challenge.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// All reattempts are used up and there is still no valid authorization?
|
||||||
|
if (challenge.getStatus() != Status.VALID)
|
||||||
|
throw new AcmeException("Failed to pass the challenge for domain " + auth.getIdentifier().getDomain());
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
logger.log(Level.SEVERE, "Interrupted", ex);
|
||||||
|
} finally {
|
||||||
|
// Cleanup
|
||||||
|
httpServer.removePage(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/zutil/net/acme/AcmeDataStore.java
Normal file
34
src/zutil/net/acme/AcmeDataStore.java
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package zutil.net.acme;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
|
||||||
|
public interface AcmeDataStore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a user key pair.
|
||||||
|
*
|
||||||
|
* @return a KeyPair for the user account, null if no KeyPar was found.
|
||||||
|
*/
|
||||||
|
KeyPair loadUserKeyPair();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a user key pair for later usage.
|
||||||
|
*
|
||||||
|
* @param keyPair the keys for the user account
|
||||||
|
*/
|
||||||
|
void storeUserKeyPair(KeyPair keyPair);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a domain key pair.
|
||||||
|
*
|
||||||
|
* @return a KeyPair for the domains, null if no KeyPar was found.
|
||||||
|
*/
|
||||||
|
KeyPair loadDomainKeyPair();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a domain key pair for later usage.
|
||||||
|
*
|
||||||
|
* @param keyPair the keys for the domain
|
||||||
|
*/
|
||||||
|
void storeDomainKeyPair(KeyPair keyPair);
|
||||||
|
}
|
||||||
64
src/zutil/net/acme/AcmeFileDataStore.java
Normal file
64
src/zutil/net/acme/AcmeFileDataStore.java
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package zutil.net.acme;
|
||||||
|
|
||||||
|
import org.shredzone.acme4j.util.KeyPairUtils;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
|
||||||
|
public class AcmeFileDataStore implements AcmeDataStore {
|
||||||
|
|
||||||
|
private final File userKeyFile;
|
||||||
|
private final File domainKeyFile;
|
||||||
|
private final File domainCsrFile;
|
||||||
|
private final File domainChainFile;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new file based datastore for storing ACME protocol needed data.
|
||||||
|
*
|
||||||
|
* @param folder is the folder there the different files should be stored in.
|
||||||
|
*/
|
||||||
|
public AcmeFileDataStore(File folder) {
|
||||||
|
this.userKeyFile = new File(folder, "user.key");
|
||||||
|
this.domainKeyFile = new File(folder, "domain.key");
|
||||||
|
this.domainCsrFile = new File(folder, "domain.csr");
|
||||||
|
this.domainChainFile = new File(folder, "domain-chain.crt");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public KeyPair loadUserKeyPair(){
|
||||||
|
if (domainKeyFile.exists()) {
|
||||||
|
try (FileReader fr = new FileReader(domainKeyFile)) {
|
||||||
|
return KeyPairUtils.readKeyPair(fr);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public void storeUserKeyPair(KeyPair keyPair){
|
||||||
|
try (FileWriter fw = new FileWriter(userKeyFile)) {
|
||||||
|
KeyPairUtils.writeKeyPair(keyPair, fw);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPair loadDomainKeyPair() {
|
||||||
|
if (userKeyFile.exists()) {
|
||||||
|
try (FileReader fr = new FileReader(userKeyFile)) {
|
||||||
|
return KeyPairUtils.readKeyPair(fr);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public void storeDomainKeyPair(KeyPair keyPair){
|
||||||
|
try (FileWriter fw = new FileWriter(userKeyFile)) {
|
||||||
|
KeyPairUtils.writeKeyPair(keyPair, fw);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -61,21 +61,22 @@ public class HttpServer extends ThreadedTCPNetworkServer{
|
||||||
public static final int SESSION_TTL = 10*60*1000; // in milliseconds
|
public static final int SESSION_TTL = 10*60*1000; // in milliseconds
|
||||||
|
|
||||||
|
|
||||||
private Map<String,HttpPage> pages;
|
private Map<String,HttpPage> pages = new ConcurrentHashMap<>();;
|
||||||
private HttpPage defaultPage;
|
private Map<String,Map<String,Object>> sessions = new ConcurrentHashMap<>();;
|
||||||
private Map<String,Map<String,Object>> sessions;
|
private int nextSessionId = 0;
|
||||||
private int nextSessionId;
|
private HttpPage defaultPage = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the sever
|
* Creates a new instance of the sever
|
||||||
*
|
*
|
||||||
* @param port The port that the server should listen to
|
* @param port The port that the server should listen to
|
||||||
*/
|
*/
|
||||||
public HttpServer(int port) {
|
public HttpServer(int port) throws IOException {
|
||||||
this(port, null, null);
|
super(port);
|
||||||
|
initGarbageCollector();
|
||||||
|
|
||||||
|
logger.info("HTTP Server ready and listening to port: " + port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the sever
|
* Creates a new instance of the sever
|
||||||
*
|
*
|
||||||
|
|
@ -83,19 +84,17 @@ public class HttpServer extends ThreadedTCPNetworkServer{
|
||||||
* @param keyStore If this is not null then the server will use SSL connection with this keyStore file path
|
* @param keyStore If this is not null then the server will use SSL connection with this keyStore file path
|
||||||
* @param keyStorePass If this is not null then the server will use a SSL connection with the given certificate
|
* @param keyStorePass If this is not null then the server will use a SSL connection with the given certificate
|
||||||
*/
|
*/
|
||||||
public HttpServer(int port, File keyStore, String keyStorePass) {
|
public HttpServer(int port, File keyStore, char[] keyStorePass) throws IOException {
|
||||||
super(port, keyStore, keyStorePass);
|
super(port, keyStore, keyStorePass);
|
||||||
|
initGarbageCollector();
|
||||||
|
|
||||||
pages = new ConcurrentHashMap<>();
|
logger.info("HTTPS Server ready and listening to port: " + port);
|
||||||
sessions = new ConcurrentHashMap<>();
|
|
||||||
nextSessionId = 0;
|
|
||||||
|
|
||||||
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
exec.scheduleWithFixedDelay(new SessionGarbageCollector(), 10000, SESSION_TTL / 2, TimeUnit.MILLISECONDS);
|
|
||||||
|
|
||||||
logger.info("HTTP" + (keyStore==null ? "" : "S") + " Server ready and listening to port: " + port);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initGarbageCollector() {
|
||||||
|
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
exec.scheduleWithFixedDelay(new SessionGarbageCollector(), 10000, SESSION_TTL / 2, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This class acts as an garbage collector that
|
* This class acts as an garbage collector that
|
||||||
* removes old sessions from the session HashMap
|
* removes old sessions from the session HashMap
|
||||||
|
|
@ -118,16 +117,37 @@ public class HttpServer extends ThreadedTCPNetworkServer{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a HttpPage to a specific URL
|
* Add a HttpPage to a specific URL.
|
||||||
*
|
*
|
||||||
* @param name The URL or name of the page
|
* @param url The URL or name of the page
|
||||||
* @param page The page itself
|
* @param page The page itself
|
||||||
*/
|
*/
|
||||||
public void setPage(String name, HttpPage page) {
|
public void setPage(String url, HttpPage page) {
|
||||||
if (name.charAt(0) != '/')
|
if (url.charAt(0) != '/')
|
||||||
name = "/" +name;
|
url = "/" + url;
|
||||||
pages.put(name, page);
|
pages.put(url, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all pages to this server from the given server object.
|
||||||
|
*
|
||||||
|
* @param server is the HttpServer object that pages will be copied from.
|
||||||
|
*/
|
||||||
|
public void setPages(HttpServer server) {
|
||||||
|
pages.putAll(server.pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a page based on the URL.
|
||||||
|
*
|
||||||
|
* @param url The URL or name of the page
|
||||||
|
*/
|
||||||
|
public void removePage(String url) {
|
||||||
|
if (url.charAt(0) != '/')
|
||||||
|
url = "/" + url;
|
||||||
|
pages.remove(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
45
src/zutil/net/http/page/HttpStaticContentPage.java
Normal file
45
src/zutil/net/http/page/HttpStaticContentPage.java
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package zutil.net.http.page;
|
||||||
|
|
||||||
|
import zutil.net.http.HttpHeader;
|
||||||
|
import zutil.net.http.HttpPage;
|
||||||
|
import zutil.net.http.HttpPrintStream;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A page serving static content
|
||||||
|
*/
|
||||||
|
public class HttpStaticContentPage implements HttpPage {
|
||||||
|
private String contentType;
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
|
||||||
|
public HttpStaticContentPage(String content) {
|
||||||
|
this.setContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentType(String contentType) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void respond(HttpPrintStream out,
|
||||||
|
HttpHeader headers,
|
||||||
|
Map<String, Object> session,
|
||||||
|
Map<String, String> cookie,
|
||||||
|
Map<String, String> request) throws IOException {
|
||||||
|
if (contentType != null)
|
||||||
|
out.setHeader(HttpHeader.HEADER_CONTENT_TYPE, contentType);
|
||||||
|
|
||||||
|
out.setHeader(HttpHeader.HEADER_CONTENT_LENGTH, String.valueOf(content.length()));
|
||||||
|
out.print(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -55,7 +55,7 @@ public class MqttBroker extends ThreadedTCPNetworkServer {
|
||||||
private MqttSubscriptionListener globalListener;
|
private MqttSubscriptionListener globalListener;
|
||||||
private Map<String, List<MqttSubscriptionListener>> subscriptionListeners = new HashMap<>();
|
private Map<String, List<MqttSubscriptionListener>> subscriptionListeners = new HashMap<>();
|
||||||
|
|
||||||
public MqttBroker() {
|
public MqttBroker() throws IOException {
|
||||||
super(MQTT_PORT);
|
super(MQTT_PORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,17 @@ package zutil.net.threaded;
|
||||||
|
|
||||||
import zutil.log.LogUtil;
|
import zutil.log.LogUtil;
|
||||||
|
|
||||||
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
|
import javax.net.ssl.ManagerFactoryParameters;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.SSLServerSocketFactory;
|
import javax.net.ssl.SSLServerSocketFactory;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.security.KeyStoreException;
|
import java.security.*;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.cert.X509Certificate;
|
||||||
import java.security.NoSuchProviderException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
@ -46,48 +48,106 @@ import java.util.logging.Logger;
|
||||||
*
|
*
|
||||||
* @author Ziver
|
* @author Ziver
|
||||||
*/
|
*/
|
||||||
public abstract class ThreadedTCPNetworkServer extends Thread{
|
public abstract class ThreadedTCPNetworkServer extends Thread {
|
||||||
private static final Logger logger = LogUtil.getLogger();
|
private static final Logger logger = LogUtil.getLogger();
|
||||||
|
|
||||||
|
private Executor executor = Executors.newCachedThreadPool();
|
||||||
private final int port;
|
private final int port;
|
||||||
private Executor executor;
|
private ServerSocket serverSocket;
|
||||||
private File keyStore;
|
|
||||||
private String keyStorePass;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the sever
|
* Creates a new instance of the sever.
|
||||||
*
|
*
|
||||||
* @param port The port that the server should listen to
|
* @param port the port that the server should listen to
|
||||||
*/
|
*/
|
||||||
public ThreadedTCPNetworkServer(int port) {
|
public ThreadedTCPNetworkServer(int port) throws IOException {
|
||||||
this(port, null, null);
|
this.port = port;
|
||||||
|
this.serverSocket = new ServerSocket(port);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the sever
|
* Creates a new SSL instance of the sever.
|
||||||
*
|
*
|
||||||
* @param port The port that the server should listen to
|
* @param port the port that the server should listen to.
|
||||||
* @param keyStore If this is not null then the server will use SSL connection with this keyStore file path
|
* @param keyStoreFile the path to the key store file containing the server certificates
|
||||||
* @param keyStorePass If this is not null then the server will use a SSL connection with the given certificate
|
* @param keyStorePass the password to decrypt the key store file.
|
||||||
*/
|
*/
|
||||||
public ThreadedTCPNetworkServer(int port, File keyStore, String keyStorePass) {
|
public ThreadedTCPNetworkServer(int port, File keyStoreFile, char[] keyStorePass) throws IOException {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
executor = Executors.newCachedThreadPool();
|
this.serverSocket = createSSLSocket(port, keyStoreFile, keyStorePass);
|
||||||
this.keyStorePass = keyStorePass;
|
}
|
||||||
this.keyStore = keyStore;
|
/**
|
||||||
|
* Creates a new SSL instance of the sever.
|
||||||
|
*
|
||||||
|
* @param port the port that the server should listen to.
|
||||||
|
* @param certificate the certificate for the servers domain.
|
||||||
|
*/
|
||||||
|
public ThreadedTCPNetworkServer(int port, X509Certificate certificate) throws IOException {
|
||||||
|
this.port = port;
|
||||||
|
this.serverSocket = createSSLSocket(port, certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a SSLServerSocket
|
||||||
|
*
|
||||||
|
* @param port the port the server should to
|
||||||
|
* @param keyStoreFile the cert file location
|
||||||
|
* @param keyStorePass the password for the cert file
|
||||||
|
* @return a SSLServerSocket object
|
||||||
|
*/
|
||||||
|
private static ServerSocket createSSLSocket(int port, File keyStoreFile, char[] keyStorePass) throws IOException{
|
||||||
|
try {
|
||||||
|
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
|
keyStore.load(new FileInputStream(keyStoreFile), keyStorePass);
|
||||||
|
|
||||||
|
return createSSLSocket(port, keyStore, keyStorePass);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new IOException("Unable to configure certificate.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a SSLServerSocket
|
||||||
|
*
|
||||||
|
* @param port the port the server should to.
|
||||||
|
* @param certificate the certificate for the servers domain.
|
||||||
|
* @return a SSLServerSocket object
|
||||||
|
*/
|
||||||
|
private static ServerSocket createSSLSocket(int port, X509Certificate certificate) throws IOException{
|
||||||
|
try {
|
||||||
|
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
|
keyStore.load(null); // Create empty keystore
|
||||||
|
keyStore.setCertificateEntry("main_certificate", certificate);
|
||||||
|
|
||||||
|
return createSSLSocket(port, keyStore, null);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
throw new IOException("Unable to configure certificate.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates a SSLServerSocket
|
||||||
|
*
|
||||||
|
* @param port the port the server should to.
|
||||||
|
* @param keyStore the key store containing the domain certificates
|
||||||
|
* @param keyStorePass the password for the cert file
|
||||||
|
* @return a SSLServerSocket object
|
||||||
|
*/
|
||||||
|
private static ServerSocket createSSLSocket(int port, KeyStore keyStore, char[] keyStorePass)
|
||||||
|
throws IOException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, KeyManagementException {
|
||||||
|
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||||
|
keyManagerFactory.init(keyStore, keyStorePass);
|
||||||
|
|
||||||
|
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||||
|
ctx.init(keyManagerFactory.getKeyManagers(), null, SecureRandom.getInstanceStrong());
|
||||||
|
SSLServerSocketFactory socketFactory = ctx.getServerSocketFactory();
|
||||||
|
|
||||||
|
return socketFactory.createServerSocket(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
ServerSocket serverSocket = null;
|
|
||||||
try {
|
try {
|
||||||
if (keyStorePass != null && keyStore != null) {
|
logger.info("Listening for TCP Connections on port: " + port);
|
||||||
registerCertificate(keyStore, keyStorePass);
|
|
||||||
serverSocket = initSSL(port);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
serverSocket = new ServerSocket(port);
|
|
||||||
}
|
|
||||||
logger.info("Listening for TCP Connections on port: " +port);
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Socket connectionSocket = serverSocket.accept();
|
Socket connectionSocket = serverSocket.accept();
|
||||||
|
|
@ -105,7 +165,9 @@ public abstract class ThreadedTCPNetworkServer extends Thread{
|
||||||
} finally {
|
} finally {
|
||||||
if (serverSocket != null) {
|
if (serverSocket != null) {
|
||||||
try {
|
try {
|
||||||
|
logger.info("Closing TCP socket listener (Port: " + port + ").");
|
||||||
serverSocket.close();
|
serverSocket.close();
|
||||||
|
serverSocket = null;
|
||||||
} catch(IOException e) { logger.log(Level.SEVERE, null, e); }
|
} catch(IOException e) { logger.log(Level.SEVERE, null, e); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,33 +178,10 @@ public abstract class ThreadedTCPNetworkServer extends Thread{
|
||||||
* that will handle the newly made connection, if an null value is returned
|
* that will handle the newly made connection, if an null value is returned
|
||||||
* then the ThreadedTCPNetworkServer will close the new connection.
|
* then the ThreadedTCPNetworkServer will close the new connection.
|
||||||
*
|
*
|
||||||
* @param s is an new connection to an host
|
* @param socket is an new connection to an host
|
||||||
* @return a new instance of an thread or null
|
* @return a new instance of an thread or null
|
||||||
*/
|
*/
|
||||||
protected abstract ThreadedTCPNetworkServerThread getThreadInstance(Socket s) throws IOException;
|
protected abstract ThreadedTCPNetworkServerThread getThreadInstance(Socket socket) throws IOException;
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates a SSLServerSocket
|
|
||||||
*
|
|
||||||
* @param port The port to listen to
|
|
||||||
* @return The SSLServerSocket
|
|
||||||
*/
|
|
||||||
private ServerSocket initSSL(int port) throws IOException{
|
|
||||||
SSLServerSocketFactory sslserversocketfactory =
|
|
||||||
(SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
|
|
||||||
return sslserversocketfactory.createServerSocket(port);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the given cert file to the KeyStore
|
|
||||||
*
|
|
||||||
* @param keyStore The cert file
|
|
||||||
*/
|
|
||||||
protected void registerCertificate(File keyStore, String keyStorePass) {
|
|
||||||
System.setProperty("javax.net.ssl.keyStore", keyStore.getAbsolutePath());
|
|
||||||
System.setProperty("javax.net.ssl.keyStorePassword", keyStorePass);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the server and interrupts its internal thread.
|
* Stops the server and interrupts its internal thread.
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ package zutil.osal.app.ffmpeg;
|
||||||
import zutil.osal.app.ffmpeg.FFmpegConstants.*;
|
import zutil.osal.app.ffmpeg.FFmpegConstants.*;
|
||||||
import zutil.osal.app.ffmpeg.FFmpegProgressManager.FFmpegProgressListener;
|
import zutil.osal.app.ffmpeg.FFmpegProgressManager.FFmpegProgressListener;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -36,7 +37,6 @@ import java.util.List;
|
||||||
* @see <a href="https://ffmpeg.org/ffmpeg.html">FFmpeg Commandline Documentation</a>
|
* @see <a href="https://ffmpeg.org/ffmpeg.html">FFmpeg Commandline Documentation</a>
|
||||||
*/
|
*/
|
||||||
public class FFmpeg {
|
public class FFmpeg {
|
||||||
|
|
||||||
private FFmpegLogLevel logLevel;
|
private FFmpegLogLevel logLevel;
|
||||||
private boolean overwriteOutput = false;
|
private boolean overwriteOutput = false;
|
||||||
private List<FFmpegInput> inputs = new ArrayList<>();
|
private List<FFmpegInput> inputs = new ArrayList<>();
|
||||||
|
|
@ -71,9 +71,13 @@ public class FFmpeg {
|
||||||
if (listener == null)
|
if (listener == null)
|
||||||
throw new IllegalArgumentException("FFmpegProgressListener cannot be NULL.");
|
throw new IllegalArgumentException("FFmpegProgressListener cannot be NULL.");
|
||||||
|
|
||||||
if (progressManager != null)
|
try {
|
||||||
progressManager.close();
|
if (progressManager != null)
|
||||||
progressManager = new FFmpegProgressManager(listener);
|
progressManager.close();
|
||||||
|
progressManager = new FFmpegProgressManager(listener);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,10 @@ public class FFmpegProgressManager extends ThreadedTCPNetworkServer {
|
||||||
private FFmpegProgressListener listener;
|
private FFmpegProgressListener listener;
|
||||||
private String address;
|
private String address;
|
||||||
|
|
||||||
public FFmpegProgressManager(FFmpegProgressListener listener) {
|
public FFmpegProgressManager(FFmpegProgressListener listener) throws IOException {
|
||||||
this(listener, PROGRESS_DEFAULT_PORT);
|
this(listener, PROGRESS_DEFAULT_PORT);
|
||||||
}
|
}
|
||||||
public FFmpegProgressManager(FFmpegProgressListener listener, int port) {
|
public FFmpegProgressManager(FFmpegProgressListener listener, int port) throws IOException {
|
||||||
super(port);
|
super(port);
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.address = "tcp://" + InetAddress.getLoopbackAddress() + ":" + port;
|
this.address = "tcp://" + InetAddress.getLoopbackAddress() + ":" + port;
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ public class HttpGuessTheNumber implements HttpPage {
|
||||||
private static final String COOKIE_KEY_HIGH = "high";
|
private static final String COOKIE_KEY_HIGH = "high";
|
||||||
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) throws IOException {
|
||||||
LogUtil.setGlobalLevel(Level.ALL);
|
LogUtil.setGlobalLevel(Level.ALL);
|
||||||
LogUtil.setGlobalFormatter(new CompactLogFormatter());
|
LogUtil.setGlobalFormatter(new CompactLogFormatter());
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue