| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.proxy; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Queue; |
| import java.util.concurrent.TimeUnit; |
| import java.util.zip.GZIPOutputStream; |
| |
| import javax.servlet.AsyncContext; |
| import javax.servlet.ReadListener; |
| import javax.servlet.ServletConfig; |
| import javax.servlet.ServletException; |
| import javax.servlet.ServletInputStream; |
| import javax.servlet.ServletOutputStream; |
| import javax.servlet.WriteListener; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.eclipse.jetty.client.ContentDecoder; |
| import org.eclipse.jetty.client.GZIPContentDecoder; |
| 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.client.util.DeferredContentProvider; |
| import org.eclipse.jetty.http.HttpHeader; |
| import org.eclipse.jetty.http.HttpVersion; |
| import org.eclipse.jetty.io.RuntimeIOException; |
| import org.eclipse.jetty.util.BufferUtil; |
| import org.eclipse.jetty.util.Callback; |
| import org.eclipse.jetty.util.CountingCallback; |
| import org.eclipse.jetty.util.IteratingCallback; |
| import org.eclipse.jetty.util.component.Destroyable; |
| |
| /** |
| * <p>Servlet 3.1 asynchronous proxy servlet with capability |
| * to intercept and modify request/response content.</p> |
| * <p>Both the request processing and the I/O are asynchronous.</p> |
| * |
| * @see ProxyServlet |
| * @see AsyncProxyServlet |
| * @see ConnectHandler |
| */ |
| public class AsyncMiddleManServlet extends AbstractProxyServlet |
| { |
| private static final String PROXY_REQUEST_CONTENT_COMMITTED_ATTRIBUTE = AsyncMiddleManServlet.class.getName() + ".proxyRequestContentCommitted"; |
| private static final String CLIENT_TRANSFORMER_ATTRIBUTE = AsyncMiddleManServlet.class.getName() + ".clientTransformer"; |
| private static final String SERVER_TRANSFORMER_ATTRIBUTE = AsyncMiddleManServlet.class.getName() + ".serverTransformer"; |
| private static final String CONTINUE_ACTION_ATTRIBUTE = AsyncMiddleManServlet.class.getName() + ".continueAction"; |
| |
| @Override |
| protected void service(HttpServletRequest clientRequest, HttpServletResponse proxyResponse) throws ServletException, IOException |
| { |
| String rewrittenTarget = rewriteTarget(clientRequest); |
| if (_log.isDebugEnabled()) |
| { |
| StringBuffer target = clientRequest.getRequestURL(); |
| if (clientRequest.getQueryString() != null) |
| target.append("?").append(clientRequest.getQueryString()); |
| _log.debug("{} rewriting: {} -> {}", getRequestId(clientRequest), target, rewrittenTarget); |
| } |
| if (rewrittenTarget == null) |
| { |
| onProxyRewriteFailed(clientRequest, proxyResponse); |
| return; |
| } |
| |
| final Request proxyRequest = getHttpClient().newRequest(rewrittenTarget) |
| .method(clientRequest.getMethod()) |
| .version(HttpVersion.fromString(clientRequest.getProtocol())); |
| |
| copyRequestHeaders(clientRequest, proxyRequest); |
| |
| addProxyHeaders(clientRequest, proxyRequest); |
| |
| final AsyncContext asyncContext = clientRequest.startAsync(); |
| // We do not timeout the continuation, but the proxy request. |
| asyncContext.setTimeout(0); |
| proxyRequest.timeout(getTimeout(), TimeUnit.MILLISECONDS); |
| |
| // If there is content, the send of the proxy request |
| // is delayed and performed when the content arrives, |
| // to allow optimization of the Content-Length header. |
| if (hasContent(clientRequest)) |
| { |
| DeferredContentProvider provider = newProxyContentProvider(clientRequest, proxyResponse, proxyRequest); |
| proxyRequest.content(provider); |
| |
| if (expects100Continue(clientRequest)) |
| { |
| proxyRequest.attribute(CLIENT_REQUEST_ATTRIBUTE, clientRequest); |
| proxyRequest.attribute(CONTINUE_ACTION_ATTRIBUTE, (Runnable)() -> |
| { |
| try |
| { |
| ServletInputStream input = clientRequest.getInputStream(); |
| input.setReadListener(newProxyReadListener(clientRequest, proxyResponse, proxyRequest, provider)); |
| } |
| catch (Throwable failure) |
| { |
| onClientRequestFailure(clientRequest, proxyRequest, proxyResponse, failure); |
| } |
| }); |
| sendProxyRequest(clientRequest, proxyResponse, proxyRequest); |
| } |
| else |
| { |
| ServletInputStream input = clientRequest.getInputStream(); |
| input.setReadListener(newProxyReadListener(clientRequest, proxyResponse, proxyRequest, provider)); |
| } |
| } |
| else |
| { |
| sendProxyRequest(clientRequest, proxyResponse, proxyRequest); |
| } |
| } |
| |
| protected DeferredContentProvider newProxyContentProvider(final HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest) throws IOException |
| { |
| return new DeferredContentProvider() |
| { |
| @Override |
| public boolean offer(ByteBuffer buffer, Callback callback) |
| { |
| if (_log.isDebugEnabled()) |
| _log.debug("{} proxying content to upstream: {} bytes", getRequestId(clientRequest), buffer.remaining()); |
| return super.offer(buffer, callback); |
| } |
| }; |
| } |
| |
| protected ReadListener newProxyReadListener(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest, DeferredContentProvider provider) |
| { |
| return new ProxyReader(clientRequest, proxyResponse, proxyRequest, provider); |
| } |
| |
| protected ProxyWriter newProxyWriteListener(HttpServletRequest clientRequest, Response proxyResponse) |
| { |
| return new ProxyWriter(clientRequest, proxyResponse); |
| } |
| |
| @Override |
| protected Response.CompleteListener newProxyResponseListener(HttpServletRequest clientRequest, HttpServletResponse proxyResponse) |
| { |
| return new ProxyResponseListener(clientRequest, proxyResponse); |
| } |
| |
| protected ContentTransformer newClientRequestContentTransformer(HttpServletRequest clientRequest, Request proxyRequest) |
| { |
| return ContentTransformer.IDENTITY; |
| } |
| |
| protected ContentTransformer newServerResponseContentTransformer(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Response serverResponse) |
| { |
| return ContentTransformer.IDENTITY; |
| } |
| |
| @Override |
| protected void onContinue(HttpServletRequest clientRequest, Request proxyRequest) |
| { |
| super.onContinue(clientRequest, proxyRequest); |
| Runnable action = (Runnable)proxyRequest.getAttributes().get(CONTINUE_ACTION_ATTRIBUTE); |
| action.run(); |
| } |
| |
| private void transform(ContentTransformer transformer, ByteBuffer input, boolean finished, List<ByteBuffer> output) throws IOException |
| { |
| try |
| { |
| transformer.transform(input, finished, output); |
| } |
| catch (Throwable x) |
| { |
| _log.info("Exception while transforming " + transformer, x); |
| throw x; |
| } |
| } |
| |
| int readClientRequestContent(ServletInputStream input, byte[] buffer) throws IOException |
| { |
| return input.read(buffer); |
| } |
| |
| void writeProxyResponseContent(ServletOutputStream output, ByteBuffer content) throws IOException |
| { |
| write(output, content); |
| } |
| |
| private static void write(OutputStream output, ByteBuffer content) throws IOException |
| { |
| int length = content.remaining(); |
| int offset = 0; |
| byte[] buffer; |
| if (content.hasArray()) |
| { |
| offset = content.arrayOffset(); |
| buffer = content.array(); |
| } |
| else |
| { |
| buffer = new byte[length]; |
| content.get(buffer); |
| } |
| output.write(buffer, offset, length); |
| } |
| |
| private void cleanup(HttpServletRequest clientRequest) |
| { |
| ContentTransformer clientTransformer = (ContentTransformer)clientRequest.getAttribute(CLIENT_TRANSFORMER_ATTRIBUTE); |
| if (clientTransformer instanceof Destroyable) |
| ((Destroyable)clientTransformer).destroy(); |
| ContentTransformer serverTransformer = (ContentTransformer)clientRequest.getAttribute(SERVER_TRANSFORMER_ATTRIBUTE); |
| if (serverTransformer instanceof Destroyable) |
| ((Destroyable)serverTransformer).destroy(); |
| } |
| |
| /** |
| * <p>Convenience extension of {@link AsyncMiddleManServlet} that offers transparent proxy functionalities.</p> |
| * |
| * @see org.eclipse.jetty.proxy.AbstractProxyServlet.TransparentDelegate |
| */ |
| public static class Transparent extends ProxyServlet |
| { |
| private final TransparentDelegate delegate = new TransparentDelegate(this); |
| |
| @Override |
| public void init(ServletConfig config) throws ServletException |
| { |
| super.init(config); |
| delegate.init(config); |
| } |
| |
| @Override |
| protected String rewriteTarget(HttpServletRequest request) |
| { |
| return delegate.rewriteTarget(request); |
| } |
| } |
| |
| protected class ProxyReader extends IteratingCallback implements ReadListener |
| { |
| private final byte[] buffer = new byte[getHttpClient().getRequestBufferSize()]; |
| private final List<ByteBuffer> buffers = new ArrayList<>(); |
| private final HttpServletRequest clientRequest; |
| private final HttpServletResponse proxyResponse; |
| private final Request proxyRequest; |
| private final DeferredContentProvider provider; |
| private final int contentLength; |
| private final boolean expects100Continue; |
| private int length; |
| |
| protected ProxyReader(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest, DeferredContentProvider provider) |
| { |
| this.clientRequest = clientRequest; |
| this.proxyResponse = proxyResponse; |
| this.proxyRequest = proxyRequest; |
| this.provider = provider; |
| this.contentLength = clientRequest.getContentLength(); |
| this.expects100Continue = expects100Continue(clientRequest); |
| } |
| |
| @Override |
| public void onDataAvailable() throws IOException |
| { |
| iterate(); |
| } |
| |
| @Override |
| public void onAllDataRead() throws IOException |
| { |
| if (!provider.isClosed()) |
| { |
| process(BufferUtil.EMPTY_BUFFER, new Callback() |
| { |
| @Override |
| public void failed(Throwable x) |
| { |
| onError(x); |
| } |
| }, true); |
| } |
| |
| if (_log.isDebugEnabled()) |
| _log.debug("{} proxying content to upstream completed", getRequestId(clientRequest)); |
| } |
| |
| @Override |
| public void onError(Throwable t) |
| { |
| cleanup(clientRequest); |
| onClientRequestFailure(clientRequest, proxyRequest, proxyResponse, t); |
| } |
| |
| @Override |
| protected Action process() throws Exception |
| { |
| ServletInputStream input = clientRequest.getInputStream(); |
| while (input.isReady() && !input.isFinished()) |
| { |
| int read = readClientRequestContent(input, buffer); |
| |
| if (_log.isDebugEnabled()) |
| _log.debug("{} asynchronous read {} bytes on {}", getRequestId(clientRequest), read, input); |
| |
| if (read < 0) |
| return Action.SUCCEEDED; |
| |
| if (contentLength > 0 && read > 0) |
| length += read; |
| |
| ByteBuffer content = read > 0 ? ByteBuffer.wrap(buffer, 0, read) : BufferUtil.EMPTY_BUFFER; |
| boolean finished = length == contentLength; |
| process(content, this, finished); |
| |
| if (read > 0) |
| return Action.SCHEDULED; |
| } |
| |
| if (input.isFinished()) |
| { |
| if (_log.isDebugEnabled()) |
| _log.debug("{} asynchronous read complete on {}", getRequestId(clientRequest), input); |
| return Action.SUCCEEDED; |
| } |
| else |
| { |
| if (_log.isDebugEnabled()) |
| _log.debug("{} asynchronous read pending on {}", getRequestId(clientRequest), input); |
| return Action.IDLE; |
| } |
| } |
| |
| private void process(ByteBuffer content, Callback callback, boolean finished) throws IOException |
| { |
| ContentTransformer transformer = (ContentTransformer)clientRequest.getAttribute(CLIENT_TRANSFORMER_ATTRIBUTE); |
| if (transformer == null) |
| { |
| transformer = newClientRequestContentTransformer(clientRequest, proxyRequest); |
| clientRequest.setAttribute(CLIENT_TRANSFORMER_ATTRIBUTE, transformer); |
| } |
| |
| int contentBytes = content.remaining(); |
| |
| // Skip transformation for empty non-last buffers. |
| if (contentBytes == 0 && !finished) |
| { |
| callback.succeeded(); |
| return; |
| } |
| |
| transform(transformer, content, finished, buffers); |
| |
| int newContentBytes = 0; |
| int size = buffers.size(); |
| if (size > 0) |
| { |
| CountingCallback counter = new CountingCallback(callback, size); |
| for (int i = 0; i < size; ++i) |
| { |
| ByteBuffer buffer = buffers.get(i); |
| newContentBytes += buffer.remaining(); |
| provider.offer(buffer, counter); |
| } |
| buffers.clear(); |
| } |
| |
| if (finished) |
| provider.close(); |
| |
| if (_log.isDebugEnabled()) |
| _log.debug("{} upstream content transformation {} -> {} bytes", getRequestId(clientRequest), contentBytes, newContentBytes); |
| |
| boolean contentCommitted = clientRequest.getAttribute(PROXY_REQUEST_CONTENT_COMMITTED_ATTRIBUTE) != null; |
| if (!contentCommitted && (size > 0 || finished)) |
| { |
| clientRequest.setAttribute(PROXY_REQUEST_CONTENT_COMMITTED_ATTRIBUTE, true); |
| if (!expects100Continue) |
| { |
| proxyRequest.header(HttpHeader.CONTENT_LENGTH, null); |
| sendProxyRequest(clientRequest, proxyResponse, proxyRequest); |
| } |
| } |
| |
| if (size == 0) |
| callback.succeeded(); |
| } |
| |
| @Override |
| protected void onCompleteFailure(Throwable x) |
| { |
| onError(x); |
| } |
| } |
| |
| protected class ProxyResponseListener extends Response.Listener.Adapter implements Callback |
| { |
| private final String WRITE_LISTENER_ATTRIBUTE = AsyncMiddleManServlet.class.getName() + ".writeListener"; |
| |
| private final Callback complete = new CountingCallback(this, 2); |
| private final List<ByteBuffer> buffers = new ArrayList<>(); |
| private final HttpServletRequest clientRequest; |
| private final HttpServletResponse proxyResponse; |
| private boolean hasContent; |
| private long contentLength; |
| private long length; |
| private Response response; |
| |
| protected ProxyResponseListener(HttpServletRequest clientRequest, HttpServletResponse proxyResponse) |
| { |
| this.clientRequest = clientRequest; |
| this.proxyResponse = proxyResponse; |
| } |
| |
| @Override |
| public void onBegin(Response serverResponse) |
| { |
| response = serverResponse; |
| proxyResponse.setStatus(serverResponse.getStatus()); |
| } |
| |
| @Override |
| public void onHeaders(Response serverResponse) |
| { |
| contentLength = serverResponse.getHeaders().getLongField(HttpHeader.CONTENT_LENGTH.asString()); |
| onServerResponseHeaders(clientRequest, proxyResponse, serverResponse); |
| } |
| |
| @Override |
| public void onContent(final Response serverResponse, ByteBuffer content, final Callback callback) |
| { |
| try |
| { |
| int contentBytes = content.remaining(); |
| if (_log.isDebugEnabled()) |
| _log.debug("{} received server content: {} bytes", getRequestId(clientRequest), contentBytes); |
| |
| hasContent = true; |
| |
| ProxyWriter proxyWriter = (ProxyWriter)clientRequest.getAttribute(WRITE_LISTENER_ATTRIBUTE); |
| boolean committed = proxyWriter != null; |
| if (proxyWriter == null) |
| { |
| proxyWriter = newProxyWriteListener(clientRequest, serverResponse); |
| clientRequest.setAttribute(WRITE_LISTENER_ATTRIBUTE, proxyWriter); |
| } |
| |
| ContentTransformer transformer = (ContentTransformer)clientRequest.getAttribute(SERVER_TRANSFORMER_ATTRIBUTE); |
| if (transformer == null) |
| { |
| transformer = newServerResponseContentTransformer(clientRequest, proxyResponse, serverResponse); |
| clientRequest.setAttribute(SERVER_TRANSFORMER_ATTRIBUTE, transformer); |
| } |
| |
| length += contentBytes; |
| |
| boolean finished = contentLength >= 0 && length == contentLength; |
| transform(transformer, content, finished, buffers); |
| |
| int newContentBytes = 0; |
| int size = buffers.size(); |
| if (size > 0) |
| { |
| Callback counter = size == 1 ? callback : new CountingCallback(callback, size); |
| for (int i = 0; i < size; ++i) |
| { |
| ByteBuffer buffer = buffers.get(i); |
| newContentBytes += buffer.remaining(); |
| proxyWriter.offer(buffer, counter); |
| } |
| buffers.clear(); |
| } |
| else |
| { |
| proxyWriter.offer(BufferUtil.EMPTY_BUFFER, callback); |
| } |
| if (finished) |
| proxyWriter.offer(BufferUtil.EMPTY_BUFFER, complete); |
| |
| if (_log.isDebugEnabled()) |
| _log.debug("{} downstream content transformation {} -> {} bytes", getRequestId(clientRequest), contentBytes, newContentBytes); |
| |
| if (committed) |
| { |
| proxyWriter.onWritePossible(); |
| } |
| else |
| { |
| if (contentLength >= 0) |
| proxyResponse.setContentLength(-1); |
| |
| // Setting the WriteListener triggers an invocation to |
| // onWritePossible(), possibly on a different thread. |
| // We cannot succeed the callback from here, otherwise |
| // we run into a race where the different thread calls |
| // onWritePossible() and succeeding the callback causes |
| // this method to be called again, which also may call |
| // onWritePossible(). |
| proxyResponse.getOutputStream().setWriteListener(proxyWriter); |
| } |
| } |
| catch (Throwable x) |
| { |
| callback.failed(x); |
| } |
| } |
| |
| @Override |
| public void onSuccess(final Response serverResponse) |
| { |
| try |
| { |
| if (hasContent) |
| { |
| // If we had unknown length content, we need to call the |
| // transformer to signal that the content is finished. |
| if (contentLength < 0) |
| { |
| ProxyWriter proxyWriter = (ProxyWriter)clientRequest.getAttribute(WRITE_LISTENER_ATTRIBUTE); |
| ContentTransformer transformer = (ContentTransformer)clientRequest.getAttribute(SERVER_TRANSFORMER_ATTRIBUTE); |
| |
| transform(transformer, BufferUtil.EMPTY_BUFFER, true, buffers); |
| |
| long newContentBytes = 0; |
| int size = buffers.size(); |
| if (size > 0) |
| { |
| Callback callback = size == 1 ? complete : new CountingCallback(complete, size); |
| for (int i = 0; i < size; ++i) |
| { |
| ByteBuffer buffer = buffers.get(i); |
| newContentBytes += buffer.remaining(); |
| proxyWriter.offer(buffer, callback); |
| } |
| buffers.clear(); |
| } |
| else |
| { |
| proxyWriter.offer(BufferUtil.EMPTY_BUFFER, complete); |
| } |
| |
| if (_log.isDebugEnabled()) |
| _log.debug("{} downstream content transformation to {} bytes", getRequestId(clientRequest), newContentBytes); |
| |
| proxyWriter.onWritePossible(); |
| } |
| } |
| else |
| { |
| complete.succeeded(); |
| } |
| } |
| catch (Throwable x) |
| { |
| complete.failed(x); |
| } |
| } |
| |
| @Override |
| public void onComplete(Result result) |
| { |
| if (result.isSucceeded()) |
| complete.succeeded(); |
| else |
| complete.failed(result.getFailure()); |
| } |
| |
| @Override |
| public void succeeded() |
| { |
| cleanup(clientRequest); |
| onProxyResponseSuccess(clientRequest, proxyResponse, response); |
| } |
| |
| @Override |
| public void failed(Throwable failure) |
| { |
| cleanup(clientRequest); |
| onProxyResponseFailure(clientRequest, proxyResponse, response, failure); |
| } |
| } |
| |
| protected class ProxyWriter implements WriteListener |
| { |
| private final Queue<DeferredContentProvider.Chunk> chunks = new ArrayDeque<>(); |
| private final HttpServletRequest clientRequest; |
| private final Response serverResponse; |
| private DeferredContentProvider.Chunk chunk; |
| private boolean writePending; |
| |
| protected ProxyWriter(HttpServletRequest clientRequest, Response serverResponse) |
| { |
| this.clientRequest = clientRequest; |
| this.serverResponse = serverResponse; |
| } |
| |
| public boolean offer(ByteBuffer content, Callback callback) |
| { |
| if (_log.isDebugEnabled()) |
| _log.debug("{} proxying content to downstream: {} bytes {}", getRequestId(clientRequest), content.remaining(), callback); |
| return chunks.offer(new DeferredContentProvider.Chunk(content, callback)); |
| } |
| |
| @Override |
| public void onWritePossible() throws IOException |
| { |
| ServletOutputStream output = clientRequest.getAsyncContext().getResponse().getOutputStream(); |
| |
| // If we had a pending write, let's succeed it. |
| if (writePending) |
| { |
| if (_log.isDebugEnabled()) |
| _log.debug("{} pending async write complete of {} on {}", getRequestId(clientRequest), chunk, output); |
| writePending = false; |
| if (succeed(chunk.callback)) |
| return; |
| } |
| |
| int length = 0; |
| DeferredContentProvider.Chunk chunk = null; |
| while (output.isReady()) |
| { |
| if (chunk != null) |
| { |
| if (_log.isDebugEnabled()) |
| _log.debug("{} async write complete of {} ({} bytes) on {}", getRequestId(clientRequest), chunk, length, output); |
| if (succeed(chunk.callback)) |
| return; |
| } |
| |
| this.chunk = chunk = chunks.poll(); |
| if (chunk == null) |
| return; |
| |
| length = chunk.buffer.remaining(); |
| if (length > 0) |
| writeProxyResponseContent(output, chunk.buffer); |
| } |
| |
| if (_log.isDebugEnabled()) |
| _log.debug("{} async write pending of {} ({} bytes) on {}", getRequestId(clientRequest), chunk, length, output); |
| writePending = true; |
| } |
| |
| private boolean succeed(Callback callback) |
| { |
| // Succeeding the callback may cause to reenter in onWritePossible() |
| // because typically the callback is the one that controls whether the |
| // content received from the server has been consumed, so succeeding |
| // the callback causes more content to be received from the server, |
| // and hence more to be written to the client by onWritePossible(). |
| // A reentrant call to onWritePossible() performs another write, |
| // which may remain pending, which means that the reentrant call |
| // to onWritePossible() returns all the way back to just after the |
| // succeed of the callback. There, we cannot just loop attempting |
| // write, but we need to check whether we are write pending. |
| callback.succeeded(); |
| return writePending; |
| } |
| |
| @Override |
| public void onError(Throwable failure) |
| { |
| DeferredContentProvider.Chunk chunk = this.chunk; |
| if (chunk != null) |
| chunk.callback.failed(failure); |
| else |
| serverResponse.abort(failure); |
| } |
| } |
| |
| /** |
| * <p>Allows applications to transform upstream and downstream content.</p> |
| * <p>Typical use cases of transformations are URL rewriting of HTML anchors |
| * (where the value of the <code>href</code> attribute of <a> elements |
| * is modified by the proxy), field renaming of JSON documents, etc.</p> |
| * <p>Applications should override {@link #newClientRequestContentTransformer(HttpServletRequest, Request)} |
| * and/or {@link #newServerResponseContentTransformer(HttpServletRequest, HttpServletResponse, Response)} |
| * to provide the transformer implementation.</p> |
| */ |
| public interface ContentTransformer |
| { |
| /** |
| * The identity transformer that does not perform any transformation. |
| */ |
| public static final ContentTransformer IDENTITY = new IdentityContentTransformer(); |
| |
| /** |
| * <p>Transforms the given input byte buffers into (possibly multiple) byte buffers.</p> |
| * <p>The transformation must happen synchronously in the context of a call |
| * to this method (it is not supported to perform the transformation in another |
| * thread spawned during the call to this method). |
| * The transformation may happen or not, depending on the transformer implementation. |
| * For example, a buffering transformer may buffer the input aside, and only |
| * perform the transformation when the whole input is provided (by looking at the |
| * {@code finished} flag).</p> |
| * <p>The input buffer will be cleared and reused after the call to this method. |
| * Implementations that want to buffer aside the input (or part of it) must copy |
| * the input bytes that they want to buffer.</p> |
| * <p>Typical implementations:</p> |
| * <pre> |
| * // Identity transformation (no transformation, the input is copied to the output) |
| * public void transform(ByteBuffer input, boolean finished, List<ByteBuffer> output) |
| * { |
| * output.add(input); |
| * } |
| * |
| * // Discard transformation (all input is discarded) |
| * public void transform(ByteBuffer input, boolean finished, List<ByteBuffer> output) |
| * { |
| * // Empty |
| * } |
| * |
| * // Buffering identity transformation (all input is buffered aside until it is finished) |
| * public void transform(ByteBuffer input, boolean finished, List<ByteBuffer> output) |
| * { |
| * ByteBuffer copy = ByteBuffer.allocate(input.remaining()); |
| * copy.put(input).flip(); |
| * store(copy); |
| * |
| * if (finished) |
| * { |
| * List<ByteBuffer> copies = retrieve(); |
| * output.addAll(copies); |
| * } |
| * } |
| * </pre> |
| * |
| * @param input the input content to transform (may be of length zero) |
| * @param finished whether the input content is finished or more will come |
| * @param output where to put the transformed output content |
| * @throws IOException in case of transformation failures |
| */ |
| public void transform(ByteBuffer input, boolean finished, List<ByteBuffer> output) throws IOException; |
| } |
| |
| private static class IdentityContentTransformer implements ContentTransformer |
| { |
| @Override |
| public void transform(ByteBuffer input, boolean finished, List<ByteBuffer> output) |
| { |
| output.add(input); |
| } |
| } |
| |
| public static class GZIPContentTransformer implements ContentTransformer |
| { |
| private final List<ByteBuffer> buffers = new ArrayList<>(2); |
| private final ContentDecoder decoder = new GZIPContentDecoder(); |
| private final ContentTransformer transformer; |
| private final ByteArrayOutputStream out; |
| private final GZIPOutputStream gzipOut; |
| |
| public GZIPContentTransformer(ContentTransformer transformer) |
| { |
| try |
| { |
| this.transformer = transformer; |
| this.out = new ByteArrayOutputStream(); |
| this.gzipOut = new GZIPOutputStream(out); |
| } |
| catch (IOException x) |
| { |
| throw new RuntimeIOException(x); |
| } |
| } |
| |
| @Override |
| public void transform(ByteBuffer input, boolean finished, List<ByteBuffer> output) throws IOException |
| { |
| if (!input.hasRemaining()) |
| { |
| if (finished) |
| transformer.transform(input, true, buffers); |
| } |
| else |
| { |
| while (input.hasRemaining()) |
| { |
| ByteBuffer decoded = decoder.decode(input); |
| if (decoded.hasRemaining()) |
| transformer.transform(decoded, finished && !input.hasRemaining(), buffers); |
| } |
| } |
| |
| if (!buffers.isEmpty() || finished) |
| { |
| ByteBuffer result = gzip(buffers, finished); |
| buffers.clear(); |
| output.add(result); |
| } |
| } |
| |
| private ByteBuffer gzip(List<ByteBuffer> buffers, boolean finished) throws IOException |
| { |
| for (ByteBuffer buffer : buffers) |
| write(gzipOut, buffer); |
| if (finished) |
| gzipOut.close(); |
| byte[] gzipBytes = out.toByteArray(); |
| out.reset(); |
| return ByteBuffer.wrap(gzipBytes); |
| } |
| } |
| } |