package logbook.server.proxy; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.net.InetAddress; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import logbook.config.AppConfig; import logbook.data.Data; import logbook.data.DataType; import logbook.data.UndefinedData; import logbook.data.context.GlobalContext; import org.apache.commons.lang3.StringUtils; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.api.ProxyConfiguration; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.swt.widgets.Display; /** * リバースプロキシ * */ public final class ReverseProxyServlet extends ProxyServlet { /** ライブラリバグ対応 (HttpRequest#queryを上書きする) */ private static final Field QUERY_FIELD = getDeclaredField(HttpRequest.class, "query"); /* * リモートホストがローカルループバックアドレス以外の場合400を返し通信しない */ @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (AppConfig.get().isAllowOnlyFromLocalhost() && !AppConfig.get().isCloseOutsidePort()) { if (!InetAddress.getByName(request.getRemoteAddr()).isLoopbackAddress()) { response.setStatus(400); return; } } super.service(request, response); } /* * Hop-by-Hop ヘッダーを除去します */ @Override protected void customizeProxyRequest(Request proxyRequest, HttpServletRequest request) { proxyRequest.onRequestContent(new RequestContentListener(request)); if (!AppConfig.get().isUseProxy()) { // アップストリームプロキシがある場合は除外 // HTTP/1.1 ならkeep-aliveを追加します if (proxyRequest.getVersion() == HttpVersion.HTTP_1_1) { proxyRequest.header(HttpHeader.CONNECTION, "keep-alive"); } // Pragma: no-cache はプロキシ用なので Cache-Control: no-cache に変換します String pragma = proxyRequest.getHeaders().get(HttpHeader.PRAGMA); if ((pragma != null) && pragma.equals("no-cache")) { proxyRequest.header(HttpHeader.PRAGMA, null); if (!proxyRequest.getHeaders().containsKey(HttpHeader.CACHE_CONTROL.asString())) { proxyRequest.header(HttpHeader.CACHE_CONTROL, "no-cache"); } } } String queryString = ((org.eclipse.jetty.server.Request) request).getQueryString(); fixQueryString(proxyRequest, queryString); super.customizeProxyRequest(proxyRequest, request); } @Override protected String filterResponseHeader(HttpServletRequest request, String headerName, String headerValue) { // Content Encoding を取得する if (headerName.compareToIgnoreCase("Content-Encoding") == 0) { request.setAttribute(Filter.CONTENT_ENCODING, headerValue); } return super.filterResponseHeader(request, headerName, headerValue); } /* * レスポンスが帰ってきた */ @Override protected void onResponseContent(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, byte[] buffer, int offset, int length) throws IOException { // フィルタークラスで必要かどうかを判別後、必要であれば内容をキャプチャする // 注意: 1回のリクエストで複数回の応答が帰ってくるので全ての応答をキャプチャする必要がある if (Filter.isNeed(request.getServerName(), response.getContentType())) { ByteArrayOutputStream stream = (ByteArrayOutputStream) request.getAttribute(Filter.RESPONSE_BODY); if (stream == null) { stream = new ByteArrayOutputStream(); request.setAttribute(Filter.RESPONSE_BODY, stream); } // ストリームに書き込む stream.write(buffer, offset, length); } super.onResponseContent(request, response, proxyResponse, buffer, offset, length); } /* * レスポンスが完了した */ @Override protected void onResponseSuccess(HttpServletRequest request, HttpServletResponse response, Response proxyResponse) { if (Filter.isNeed(request.getServerName(), response.getContentType())) { byte[] postField = (byte[]) request.getAttribute(Filter.REQUEST_BODY); ByteArrayOutputStream stream = (ByteArrayOutputStream) request.getAttribute(Filter.RESPONSE_BODY); if (stream != null) { final UndefinedData rawData = new UndefinedData(request.getRequestURL().toString(), request.getRequestURI(), postField, stream.toByteArray()); final String contentEncoding = (String) request.getAttribute(Filter.CONTENT_ENCODING); final String serverName = request.getServerName(); Display.getDefault().asyncExec(new Runnable() { @Override public void run() { UndefinedData decodedData = rawData.decode(contentEncoding); // 統計データベース(http://kancolle-db.net/)に送信する DatabaseClient.send(decodedData); // キャプチャしたバイト配列は何のデータかを決定する Data data = decodedData.toDefinedData(); if (data.getDataType() != DataType.UNDEFINED) { // 定義済みのデータの場合にキューに追加する GlobalContext.updateContext(data); // サーバー名が不明の場合、サーバー名をセットする if (!Filter.isServerDetected()) { Filter.setServerName(serverName); } } } }); } } super.onResponseSuccess(request, response, proxyResponse); } /* * HttpClientを作成する */ @Override protected HttpClient newHttpClient() { HttpClient client = super.newHttpClient(); // プロキシを設定する if (AppConfig.get().isUseProxy()) { // ポート int port = AppConfig.get().getProxyPort(); // ホスト String host = AppConfig.get().getProxyHost(); // 設定する client.setProxyConfiguration(new ProxyConfiguration(host, port)); } return client; } /** * private フィールドを取得する * @param clazz クラス * @param string フィールド名 * @return フィールドオブジェクト */ private static <T> Field getDeclaredField(Class<T> clazz, String string) { try { Field field = clazz.getDeclaredField(string); field.setAccessible(true); return field; } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } /** * <p> * ライブラリのバグを修正します<br> * URLにマルチバイト文字が含まれている場合にURLが正しく組み立てられないバグを修正します * </p> */ private static void fixQueryString(Request proxyRequest, String queryString) { if (!StringUtils.isEmpty(queryString)) { if (proxyRequest instanceof HttpRequest) { try { QUERY_FIELD.set(proxyRequest, queryString); } catch (IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException(e); } } } } }