Skip to content

Instantly share code, notes, and snippets.

@ato
Last active November 28, 2018 23:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ato/316a157ff42789ff6b0d86b99fae1129 to your computer and use it in GitHub Desktop.
Save ato/316a157ff42789ff6b0d86b99fae1129 to your computer and use it in GitHub Desktop.
OutbackProxy?
package outbackproxy;
import io.undertow.Undertow;
import io.undertow.connector.ByteBufferPool;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.BlockingHandler;
import io.undertow.util.HeaderMap;
import io.undertow.util.HttpString;
import org.jwat.arc.ArcReader;
import org.jwat.arc.ArcReaderFactory;
import org.jwat.arc.ArcRecordBase;
import org.jwat.common.ByteCountingPushBackInputStream;
import org.jwat.common.HttpHeader;
import org.jwat.gzip.GzipReader;
import org.jwat.warc.WarcReader;
import org.jwat.warc.WarcReaderFactory;
import org.jwat.warc.WarcRecord;
import outback.cdx.CdxClient;
import outback.cdx.CdxRecord;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import static io.undertow.util.Headers.*;
import static java.time.ZoneOffset.*;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
import static org.jwat.common.UriProfile.*;
public class ReplayProxy {
private static final HttpString ACCEPT_DATETIME = new HttpString("Accept-Datetime");
private static final HttpString MEMENTO_DATETIME = new HttpString("Memento-Datetime");
private final CdxClient cdxClient;
private final String warcBaseUrl;
private final Undertow webServer;
public static void main(String args[]) throws Exception {
Map<String, String> env = System.getenv();
String host = env.getOrDefault("HOST", "0.0.0.0");
int port = Integer.parseInt(env.getOrDefault("PORT", "8080"));
String cdxServerUrl = env.getOrDefault("CDX_URL", "http://localhost:9901/myindex");
String warcServerUrl = env.getOrDefault("WARC_URL", "");
CdxClient cdxClient = new CdxClient(cdxServerUrl);
new ReplayProxy(host, port, cdxClient, warcServerUrl).run();
}
public ReplayProxy(String host, int port, CdxClient cdxClient, String warcBaseUrl) throws Exception {
this.cdxClient = cdxClient;
this.warcBaseUrl = warcBaseUrl;
SSLContext sslContext = SelfSign.sslContext();
ByteBufferPool bufferPool = new DefaultByteBufferPool(true, 16 * 1024 - 20, -1, 4);
HttpHandler handler = new BlockingHandler(this::handleRequest);
handler = new SSLConnectHandler(handler, handler, sslContext, bufferPool);
webServer = Undertow.builder()
.addHttpListener(port, host)
.setByteBufferPool(bufferPool)
.setHandler(handler)
.build();
}
private void run() {
webServer.start();
}
/**
* Handle a proxy request from a client.
*/
private void handleRequest(HttpServerExchange exchange) throws IOException {
String url = exchange.getRequestURL();
if (exchange.getQueryString() != null) {
url += "?" + exchange.getQueryString();
}
Instant requestedTime = parseRequestedTime(exchange);
CdxRecord cdx = cdxClient.query(url).closest(requestedTime).first();
if (cdx == null) {
exchange.setStatusCode(404);
exchange.getResponseSender().send("Not in archive");
return;
}
try (ByteCountingPushBackInputStream stream = openWarcStream(cdx.filename(), cdx.offset(), cdx.compressedLength())) {
serveWarcRecord(exchange, stream);
}
}
/**
* Parse a Mememento style Accept-Datetime request header.
*/
private Instant parseRequestedTime(HttpServerExchange exchange) {
String time = exchange.getRequestHeaders().getFirst(ACCEPT_DATETIME);
if (time == null) {
return Instant.ofEpochSecond(1);
}
return RFC_1123_DATE_TIME.parse(time, Instant::from);
}
/**
* Fetch a (W)ARC record using a byte range request.
*/
private ByteCountingPushBackInputStream openWarcStream(String filename, long offset, long length) throws IOException {
URL url = new URL(warcBaseUrl + filename);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Range", "bytes=" + offset + "-" + (offset + length + 1));
ByteCountingPushBackInputStream stream = new ByteCountingPushBackInputStream(conn.getInputStream(), 32);
if (GzipReader.isGzipped(stream)) {
return new ByteCountingPushBackInputStream(new GZIPInputStream(stream, 8192), 32);
}
return stream;
}
/**
* Send a (W)ARC record to the client.
*/
private void serveWarcRecord(HttpServerExchange exchange, ByteCountingPushBackInputStream stream) throws IOException {
if (ArcReaderFactory.isArcRecord(stream)) {
ArcReader reader = ArcReaderFactory.getReaderUncompressed(stream);
reader.setUriProfile(RFC3986_ABS_16BIT_LAX);
ArcRecordBase record = reader.getNextRecord();
HttpHeader http = record.getHttpHeader();
sendResponse(exchange, http.getPayloadInputStream(), http.getProtocolContentType(), record.getArchiveDate());
} else if (WarcReaderFactory.isWarcRecord(stream)) {
WarcReader reader = WarcReaderFactory.getReaderUncompressed(stream);
reader.setUriProfile(RFC3986_ABS_16BIT_LAX);
WarcRecord record = reader.getNextRecord();
HttpHeader http = record.getHttpHeader();
if (http != null) { // response record
sendResponse(exchange, http.getPayloadInputStream(), http.getProtocolContentType(), record.header.warcDate);
} else { // resource record
String contentType = record.getHeader("Content-Type").value;
sendResponse(exchange, record.getPayload().getInputStream(), contentType, record.header.warcDate);
}
} else {
exchange.setStatusCode(500);
exchange.getResponseSender().send("not a WARC or ARC record!");
}
}
/**
* Send a HTTP payload to the client.
*/
private void sendResponse(HttpServerExchange exchange, InputStream payload, String contentType, Date date) throws IOException {
HeaderMap headers = exchange.getResponseHeaders();
headers.put(CONTENT_TYPE, contentType);
headers.put(MEMENTO_DATETIME, RFC_1123_DATE_TIME.format(date.toInstant().atOffset(UTC)));
headers.add(VARY, "accept-datetime");
OutputStream output = exchange.getOutputStream();
copyStream(payload, output);
output.close();
}
private void copyStream(InputStream input, OutputStream output) throws IOException {
byte[] buffer = new byte[8192];
for (int n = input.read(buffer); n >= 0; n = input.read(buffer)) {
output.write(buffer, 0, n);
}
}
}
package outbackproxy;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v1CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.security.auth.x500.X500Principal;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.time.Instant;
import java.util.Date;
import static java.time.temporal.ChronoUnit.DAYS;
/**
* Generates self-signed SSL certificates.
*
* We use an elliptic curve rather than RSA as its faster to generate and handshake.
*/
class SelfSign {
private static final char[] DUMMY_PASSWORD = "changeit".toCharArray();
static {
Security.addProvider(new BouncyCastleProvider());
}
static SSLContext sslContext() throws GeneralSecurityException, IOException, OperatorCreationException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(SelfSign.keyManagers(), null, null);
return context;
}
static KeyManager[] keyManagers() throws GeneralSecurityException, IOException, OperatorCreationException {
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509", "SunJSSE");
kmf.init(generateKeyStore(DUMMY_PASSWORD), DUMMY_PASSWORD);
return kmf.getKeyManagers();
}
private static KeyStore generateKeyStore(char[] password) throws GeneralSecurityException, OperatorCreationException, IOException {
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(null, null);
generateKeyPair(keyStore, password);
return keyStore;
}
private static void generateKeyPair(KeyStore keyStore, char[] password) throws GeneralSecurityException, OperatorCreationException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
KeyPair keyPair = keyPairGenerator.generateKeyPair();
java.security.cert.Certificate[] certs = new java.security.cert.Certificate[]{
selfSign(keyPair, "SHA256withECDSA")
};
keyStore.setKeyEntry("eckey", keyPair.getPrivate(), password, certs);
}
private static X509Certificate selfSign(KeyPair keyPair, String algo) throws CertificateException, OperatorCreationException {
Instant notBefore = Instant.now().minus(1, DAYS);
Instant notAfter = Instant.now().plus(365, DAYS);
X500Principal issuer = new X500Principal("CN=Web Archive Proxy Certificate");
X509v1CertificateBuilder builder = new JcaX509v1CertificateBuilder(issuer, BigInteger.ONE, Date.from(notBefore),
Date.from(notAfter), issuer, keyPair.getPublic());
ContentSigner signer = new JcaContentSignerBuilder(algo).build(keyPair.getPrivate());
X509CertificateHolder holder = builder.build(signer);
return new JcaX509CertificateConverter().getCertificate(holder);
}
}
package outbackproxy;
import io.undertow.connector.ByteBufferPool;
import io.undertow.protocols.ssl.UndertowXnioSsl;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.protocol.http.HttpOpenListener;
import io.undertow.util.Methods;
import org.xnio.OptionMap;
import org.xnio.StreamConnection;
import org.xnio.ssl.SslConnection;
import javax.net.ssl.SSLContext;
/**
* Handles the HTTP CONNECT method by establishing an SSL session.
*/
class SSLConnectHandler implements HttpHandler {
private final HttpHandler handler;
private final HttpHandler next;
private final SSLContext sslContext;
private final ByteBufferPool byteBufferPool;
SSLConnectHandler(HttpHandler handler, HttpHandler next, SSLContext sslContext, ByteBufferPool byteBufferPool) {
this.handler = handler;
this.next = next;
this.sslContext = sslContext;
this.byteBufferPool = byteBufferPool;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
if (exchange.getRequestMethod().equals(Methods.CONNECT)) {
exchange.acceptConnectRequest(this::connected);
} else {
next.handleRequest(exchange);
}
}
private void connected(StreamConnection connection, HttpServerExchange exchange) {
UndertowXnioSsl xnioSsl = new UndertowXnioSsl(connection.getWorker().getXnio(), OptionMap.EMPTY, sslContext);
SslConnection sslConnection = xnioSsl.wrapExistingConnection(connection, OptionMap.EMPTY);
UndertowXnioSsl.getSslEngine(sslConnection).setUseClientMode(false);
HttpOpenListener httpOpenListener = new HttpOpenListener(byteBufferPool, OptionMap.EMPTY);
httpOpenListener.setRootHandler(handler);
httpOpenListener.handleEvent(sslConnection);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment