// // ======================================================================== // 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.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileAttribute; import java.util.ArrayList; import java.util.List; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.component.Destroyable; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** * <p>A specialized transformer for {@link AsyncMiddleManServlet} that performs * the transformation when the whole content has been received.</p> * <p>The content is buffered in memory up to a configurable {@link #getMaxInputBufferSize() maximum size}, * after which it is overflown to a file on disk. The overflow file is saved * in the {@link #getOverflowDirectory() overflow directory} as a * {@link Files#createTempFile(Path, String, String, FileAttribute[]) temporary file} * with a name starting with the {@link #getInputFilePrefix() input prefix} * and default suffix.</p> * <p>Application must implement the {@link #transform(Source, Sink) transformation method} * to transform the content.</p> * <p>The transformed content is buffered in memory up to a configurable {@link #getMaxOutputBufferSize() maximum size} * after which it is overflown to a file on disk. The overflow file is saved * in the {@link #getOverflowDirectory() overflow directory} as a * {@link Files#createTempFile(Path, String, String, FileAttribute[]) temporary file} * with a name starting with the {@link #getOutputFilePrefix()} output prefix} * and default suffix.</p> */ public abstract class AfterContentTransformer implements AsyncMiddleManServlet.ContentTransformer, Destroyable { private static final Logger LOG = Log.getLogger(AfterContentTransformer.class); private final List<ByteBuffer> sourceBuffers = new ArrayList<>(); private Path overflowDirectory = Paths.get(System.getProperty("java.io.tmpdir")); private String inputFilePrefix = "amms_adct_in_"; private String outputFilePrefix = "amms_adct_out_"; private long maxInputBufferSize = 1024 * 1024; private long inputBufferSize; private FileChannel inputFile; private long maxOutputBufferSize = maxInputBufferSize; private long outputBufferSize; private FileChannel outputFile; /** * <p>Returns the directory where input and output are overflown to * temporary files if they exceed, respectively, the * {@link #getMaxInputBufferSize() max input size} or the * {@link #getMaxOutputBufferSize() max output size}.</p> * <p>Defaults to the directory pointed by the {@code java.io.tmpdir} * system property.</p> * * @return the overflow directory path * @see #setOverflowDirectory(Path) */ public Path getOverflowDirectory() { return overflowDirectory; } /** * @param overflowDirectory the overflow directory path * @see #getOverflowDirectory() */ public void setOverflowDirectory(Path overflowDirectory) { this.overflowDirectory = overflowDirectory; } /** * @return the prefix of the input overflow temporary files * @see #setInputFilePrefix(String) */ public String getInputFilePrefix() { return inputFilePrefix; } /** * @param inputFilePrefix the prefix of the input overflow temporary files * @see #getInputFilePrefix() */ public void setInputFilePrefix(String inputFilePrefix) { this.inputFilePrefix = inputFilePrefix; } /** * <p>Returns the maximum input buffer size, after which the input is overflown to disk.</p> * <p>Defaults to 1 MiB, i.e. 1048576 bytes.</p> * * @return the max input buffer size * @see #setMaxInputBufferSize(long) */ public long getMaxInputBufferSize() { return maxInputBufferSize; } /** * @param maxInputBufferSize the max input buffer size * @see #getMaxInputBufferSize() */ public void setMaxInputBufferSize(long maxInputBufferSize) { this.maxInputBufferSize = maxInputBufferSize; } /** * @return the prefix of the output overflow temporary files * @see #setOutputFilePrefix(String) */ public String getOutputFilePrefix() { return outputFilePrefix; } /** * @param outputFilePrefix the prefix of the output overflow temporary files * @see #getOutputFilePrefix() */ public void setOutputFilePrefix(String outputFilePrefix) { this.outputFilePrefix = outputFilePrefix; } /** * <p>Returns the maximum output buffer size, after which the output is overflown to disk.</p> * <p>Defaults to 1 MiB, i.e. 1048576 bytes.</p> * * @return the max output buffer size * @see #setMaxOutputBufferSize(long) */ public long getMaxOutputBufferSize() { return maxOutputBufferSize; } /** * @param maxOutputBufferSize the max output buffer size */ public void setMaxOutputBufferSize(long maxOutputBufferSize) { this.maxOutputBufferSize = maxOutputBufferSize; } @Override public final void transform(ByteBuffer input, boolean finished, List<ByteBuffer> output) throws IOException { int remaining = input.remaining(); if (remaining > 0) { inputBufferSize += remaining; long max = getMaxInputBufferSize(); if (max >= 0 && inputBufferSize > max) { overflow(input); } else { ByteBuffer copy = ByteBuffer.allocate(input.remaining()); copy.put(input).flip(); sourceBuffers.add(copy); } } if (finished) { Source source = new Source(); Sink sink = new Sink(); if (transform(source, sink)) sink.drainTo(output); else source.drainTo(output); } } /** * <p>Transforms the original content read from the {@code source} into * transformed content written to the {@code sink}.</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).</p> * <p>Differently from {@link #transform(ByteBuffer, boolean, List)}, this * method is invoked only when the whole content is available, and offers * a blocking API via the InputStream and OutputStream that can be obtained * from {@link Source} and {@link Sink} respectively.</p> * <p>Implementations may read the source, inspect the input bytes and decide * that no transformation is necessary, and therefore the source must be copied * unchanged to the sink. In such case, the implementation must return false to * indicate that it wishes to just pipe the bytes from the source to the sink.</p> * <p>Typical implementations:</p> * <pre> * // Identity transformation (no transformation, the input is copied to the output) * public boolean transform(Source source, Sink sink) * { * org.eclipse.jetty.util.IO.copy(source.getInputStream(), sink.getOutputStream()); * return true; * } * </pre> * * @param source where the original content is read * @param sink where the transformed content is written * @return true if the transformation happened and the transformed bytes have * been written to the sink, false if no transformation happened and the source * must be copied to the sink. * @throws IOException if the transformation fails */ public abstract boolean transform(Source source, Sink sink) throws IOException; private void overflow(ByteBuffer input) throws IOException { if (inputFile == null) { Path path = Files.createTempFile(getOverflowDirectory(), getInputFilePrefix(), null); inputFile = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE); int size = sourceBuffers.size(); if (size > 0) { ByteBuffer[] buffers = sourceBuffers.toArray(new ByteBuffer[size]); sourceBuffers.clear(); IO.write(inputFile,buffers,0,buffers.length); } } inputFile.write(input); } @Override public void destroy() { close(inputFile); close(outputFile); } private void drain(FileChannel file, List<ByteBuffer> output) throws IOException { long position = 0; long length = file.size(); file.position(position); while (length > 0) { // At most 1 GiB file maps. long size = Math.min(1024 * 1024 * 1024, length); ByteBuffer buffer = file.map(FileChannel.MapMode.READ_ONLY, position, size); output.add(buffer); position += size; length -= size; } } private void close(Closeable closeable) { try { if (closeable != null) closeable.close(); } catch (IOException x) { LOG.ignore(x); } } /** * <p>The source from where the original content is read to be transformed.</p> * <p>The {@link #getInputStream() input stream} provided by this * class supports the {@link InputStream#reset()} method so that * the stream can be rewound to the beginning.</p> */ public class Source { private final InputStream stream; private Source() throws IOException { if (inputFile != null) { inputFile.force(true); stream = new ChannelInputStream(); } else { stream = new MemoryInputStream(); } stream.reset(); } /** * @return an input stream to read the original content from */ public InputStream getInputStream() { return stream; } private void drainTo(List<ByteBuffer> output) throws IOException { if (inputFile == null) { output.addAll(sourceBuffers); sourceBuffers.clear(); } else { drain(inputFile, output); } } } private class ChannelInputStream extends InputStream { private final InputStream stream = Channels.newInputStream(inputFile); @Override public int read(byte[] b, int off, int len) throws IOException { return stream.read(b, off, len); } @Override public int read() throws IOException { return stream.read(); } @Override public void reset() throws IOException { inputFile.position(0); } } private class MemoryInputStream extends InputStream { private final byte[] oneByte = new byte[1]; private int index; private ByteBuffer slice; @Override public int read(byte[] b, int off, int len) throws IOException { if (len == 0) return 0; if (index == sourceBuffers.size()) return -1; if (slice == null) slice = sourceBuffers.get(index).slice(); int size = Math.min(len, slice.remaining()); slice.get(b, off, size); if (!slice.hasRemaining()) { ++index; slice = null; } return size; } @Override public int read() throws IOException { int read = read(oneByte, 0, 1); return read < 0 ? read : oneByte[0] & 0xFF; } @Override public void reset() throws IOException { index = 0; slice = null; } } /** * <p>The target to where the transformed content is written after the transformation.</p> */ public class Sink { private final List<ByteBuffer> sinkBuffers = new ArrayList<>(); private final OutputStream stream = new SinkOutputStream(); /** * @return an output stream to write the transformed content to */ public OutputStream getOutputStream() { return stream; } private void overflow(ByteBuffer output) throws IOException { if (outputFile == null) { Path path = Files.createTempFile(getOverflowDirectory(), getOutputFilePrefix(), null); outputFile = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE); int size = sinkBuffers.size(); if (size > 0) { ByteBuffer[] buffers = sinkBuffers.toArray(new ByteBuffer[size]); sinkBuffers.clear(); IO.write(outputFile,buffers,0,buffers.length); } } outputFile.write(output); } private void drainTo(List<ByteBuffer> output) throws IOException { if (outputFile == null) { output.addAll(sinkBuffers); sinkBuffers.clear(); } else { outputFile.force(true); drain(outputFile, output); } } private class SinkOutputStream extends OutputStream { @Override public void write(byte[] b, int off, int len) throws IOException { if (len <= 0) return; outputBufferSize += len; long max = getMaxOutputBufferSize(); if (max >= 0 && outputBufferSize > max) { overflow(ByteBuffer.wrap(b, off, len)); } else { // The array may be reused by the // application so we need to copy it. byte[] copy = new byte[len]; System.arraycopy(b, off, copy, 0, len); sinkBuffers.add(ByteBuffer.wrap(copy)); } } @Override public void write(int b) throws IOException { write(new byte[]{(byte)b}, 0, 1); } } } }