Last active
November 28, 2018 23:12
-
-
Save ato/316a157ff42789ff6b0d86b99fae1129 to your computer and use it in GitHub Desktop.
OutbackProxy?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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