ちょっと、プロキシサーバーを簡単に書けないかなぁと思いまして。できれば、Embeddedaleで。
と調べていると、どうもJettyができそうな感じなので試してみました。
Jetty - Servlet Engine and Http Server
JettyにProxyServletなるものがあるらしいので、こちらを使用してみることにします。
Embededd向けドキュメント。
何気に、初Jettyです。
ドキュメントが上記だけだとちょっと心許ないので、ソースコードも入手。
org.eclipse.jetty.project.git - Jetty: primary project repository
今回は、こちらからwget。
$ wget http://git.eclipse.org/c/jetty/org.eclipse.jetty.project.git/snapshot/jetty-9.3.6.v20151106.tar.gz
準備
Maven依存関係は、以下のように宣言。
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-proxy</artifactId> <version>9.3.6.v20151106</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-deploy</artifactId> <version>9.3.6.v20151106</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.13</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.13</version> </dependency>
SLF4Jは、ロギング用です。
ProxyServletを書いて試す
それでは、
src/main/java/org/littlewings/proxy/jetty/MyProxyServlet.java
package org.littlewings.proxy.jetty; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.proxy.ProxyServlet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyProxyServlet extends ProxyServlet { protected Logger logger = LoggerFactory.getLogger(getClass()); @Override protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { // 何か処理 super.service(request, response); } @Override protected HttpClient newHttpClient() { // HTTPクライアントをカスタマイズする場合は、ここで return new HttpClient(); } @Override protected String rewriteTarget(HttpServletRequest clientRequest) { // アクセスパスなどをRewriteする場合は、ここで return super.rewriteTarget(clientRequest); } @Override protected Response.Listener newProxyResponseListener(HttpServletRequest request, HttpServletResponse response) { // レスポンス受信時の挙動をカスタマイズする場合は、ここで return new MyProxyResponseListener(LocalDateTime.now(), request, response); } class MyProxyResponseListener extends ProxyResponseListener { LocalDateTime now; MyProxyResponseListener(LocalDateTime now, HttpServletRequest request, HttpServletResponse response) { super(request, response); this.now = now; } @Override public void onComplete(Result result) { // 完了時の処理 super.onComplete(result); Request req = result.getRequest(); Response res = result.getResponse(); logger.info("[{}] {}://{} \"{} {} {}\" {} \"{}\"", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), req.getScheme(), req.getHost(), req.getMethod(), req.getPath() + (req.getQuery() == null ? "" : "?" + req.getQuery()), req.getVersion(), res.getStatus(), req.getHeaders().get("User-Agent")); } } }
けっこういろんな箇所で、カスタマイズができそうな感じですね。書いていることはソースコードのまんまですが、今回はリクエスト完了時にアクセスログ的なものを出力してみました。
その他は、ProxyServletのソースコードを直接確認してみるとよいでしょう。幸い、パッケージも大きくありませんし。
それでは、このプロキシサーバーを使ってHTTPアクセスしてみます。
src/main/java/org/littlewings/proxy/jetty/SimpleHttpProxy.java
package org.littlewings.proxy.jetty; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.URL; import org.eclipse.jetty.proxy.ConnectHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Slf4jRequestLog; import org.eclipse.jetty.servlet.ServletContextHandler; public class SimpleHttpProxy { public static void main(String... args) throws Exception { int proxyPort = 8080; Server server = new Server(proxyPort); server.setRequestLog(new Slf4jRequestLog()); ServletContextHandler contextHandler = new ServletContextHandler(); contextHandler.addServlet(MyProxyServlet.class, "/"); server.setHandler(contextHandler); server.start(); // server.join(); Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyPort)); String hatenaUrl = "http://d.hatena.ne.jp/Kazuhira/"; HttpURLConnection hatenaConnection = (HttpURLConnection) new URL(hatenaUrl).openConnection(proxy); try { System.out.println(hatenaUrl + " = " + hatenaConnection.getResponseCode()); } finally { hatenaConnection.disconnect(); } server.stop(); } }
Jettyに作成したProxyServletを設定して、それをProxyとしてURLConnectionのオープンに利用しています。
仕込んだログが出力した内容は、こんな感じ。
[main] INFO org.littlewings.proxy.jetty.MyProxyServlet - [2015-12-30 20:50:15] http://d.hatena.ne.jp "GET /Kazuhira/ HTTP/1.1" 200 "Java/1.8.0_66"
とはいえ、このくらいだとSlf4jRequestLogを仕込んでいるので
server.setRequestLog(new Slf4jRequestLog());
あまり意味がなかったりしますが。
[main] INFO org.eclipse.jetty.server.RequestLog - 127.0.0.1 - - [30/12/2015:11:50:15 +0000] "GET http://d.hatena.ne.jp/Kazuhira/ HTTP/1.1" 200 53518
CONNECTメソッドを扱う
ProxyServletを使った場合、HTTPのプロキシはできますが、HTTPSのプロキシはできません。この場合、ConnectHandlerを使うみたいです。
src/main/java/org/littlewings/proxy/jetty/MyConnectHandler.java
package org.littlewings.proxy.jetty; import java.util.concurrent.ConcurrentMap; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.proxy.ConnectHandler; import org.eclipse.jetty.server.Request; public class MyConnectHandler extends ConnectHandler { @Override protected void handleConnect(Request baseRequest, HttpServletRequest request, HttpServletResponse response, String serverAddress) { // 何か処理 super.handleConnect(baseRequest, request, response, serverAddress); } }
とはいえ、CONNECTの時は中身に干渉できないから、別に拡張する必要が…?
利用する時は、こんな感じ。
src/main/java/org/littlewings/proxy/jetty/SimpleSslProxy.java
package org.littlewings.proxy.jetty; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.URL; import org.eclipse.jetty.proxy.ConnectHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Slf4jRequestLog; import org.eclipse.jetty.servlet.ServletContextHandler; public class SimpleSslProxy { public static void main(String... args) throws Exception { int proxyPort = 8080; Server server = new Server(proxyPort); server.setRequestLog(new Slf4jRequestLog()); ConnectHandler connectHandler = new MyConnectHandler(); server.setHandler(connectHandler); server.start(); // server.join(); Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyPort)); String googleUrl = "https://www.google.co.jp/?gws_rd=ssl"; HttpURLConnection googleConnection = (HttpURLConnection) new URL(googleUrl).openConnection(proxy); try { System.out.println(googleUrl + " = " + googleConnection.getResponseCode()); } finally { googleConnection.disconnect(); } server.stop(); } }
合わせ技+α
ProxyServletとConnectHandlerを、合わせて使うことも可能です。
src/main/java/org/littlewings/proxy/jetty/App.java
package org.littlewings.proxy.jetty; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.URL; import org.eclipse.jetty.proxy.ConnectHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; public class App { public static void main(String... args) throws Exception { int proxyPort = 8080; Server server = new Server(proxyPort); MySlf4jRequestLog requestLog = new MySlf4jRequestLog(); requestLog.setExtended(true); server.setRequestLog(requestLog); ConnectHandler connectHandler = new MyConnectHandler(); server.setHandler(connectHandler); ServletContextHandler contextHandler = new ServletContextHandler(connectHandler, "/"); contextHandler.addServlet(MyProxyServlet.class, "/"); server.start(); // server.join(); Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyPort)); String hatenaUrl = "http://d.hatena.ne.jp/Kazuhira/"; HttpURLConnection hatenaConnection = (HttpURLConnection) new URL(hatenaUrl).openConnection(proxy); try { System.out.println(hatenaUrl + " = " + hatenaConnection.getResponseCode()); } finally { hatenaConnection.disconnect(); } try { System.out.println(hatenaUrl + " = " + hatenaConnection.getResponseCode()); } finally { hatenaConnection.disconnect(); } String googleUrl = "https://www.google.co.jp/?gws_rd=ssl"; HttpURLConnection googleConnection = (HttpURLConnection) new URL(googleUrl).openConnection(proxy); try { System.out.println(googleUrl + " = " + googleConnection.getResponseCode()); } finally { googleConnection.disconnect(); } server.stop(); } }
ConnectHandlerの上に、ProxyServletを乗せればいい、と。
あと、このサンプルではRequestLogも拡張しています。
MySlf4jRequestLog requestLog = new MySlf4jRequestLog(); requestLog.setExtended(true); server.setRequestLog(requestLog);
extendedを有効にして、logExtendedメソッドをオーバーライド。
src/main/java/org/littlewings/proxy/jetty/MySlf4jRequestLog.java
package org.littlewings.proxy.jetty; import java.io.IOException; import java.util.Collections; import java.util.StringJoiner; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Slf4jRequestLog; public class MySlf4jRequestLog extends Slf4jRequestLog { protected void logExtended(Request request, StringBuilder b) throws IOException { super.logExtended(request, b); b.append(" "); b.append("["); b.append(String.join(", ", Collections.list(request.getHeaderNames()))); b.append("]"); } }
あまり意味はありませんが、HTTPヘッダ名を全部出力してみました。
こんな感じのアクセスログになります。
[qtp205797316-25] INFO org.eclipse.jetty.server.RequestLog - 127.0.0.1 - - [30/12/2015:12:05:47 +0000] "GET http://d.hatena.ne.jp/Kazuhira/ HTTP/1.1" 200 71551 "-" "Java/1.8.0_66" [User-Agent, Host, Accept, Proxy-Connection]
以上でしたー。