001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.devtools.tunnel.payload;
018
019import java.io.IOException;
020import java.io.InterruptedIOException;
021import java.nio.ByteBuffer;
022import java.nio.channels.Channels;
023import java.nio.channels.ReadableByteChannel;
024import java.nio.channels.WritableByteChannel;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028
029import org.springframework.http.HttpHeaders;
030import org.springframework.http.HttpInputMessage;
031import org.springframework.http.HttpOutputMessage;
032import org.springframework.http.MediaType;
033import org.springframework.util.Assert;
034import org.springframework.util.StringUtils;
035
036/**
037 * Encapsulates a payload data sent via a HTTP tunnel.
038 *
039 * @author Phillip Webb
040 * @since 1.3.0
041 */
042public class HttpTunnelPayload {
043
044        private static final String SEQ_HEADER = "x-seq";
045
046        private static final int BUFFER_SIZE = 1024 * 100;
047
048        protected static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray();
049
050        private static final Log logger = LogFactory.getLog(HttpTunnelPayload.class);
051
052        private final long sequence;
053
054        private final ByteBuffer data;
055
056        /**
057         * Create a new {@link HttpTunnelPayload} instance.
058         * @param sequence the sequence number of the payload
059         * @param data the payload data
060         */
061        public HttpTunnelPayload(long sequence, ByteBuffer data) {
062                Assert.isTrue(sequence > 0, "Sequence must be positive");
063                Assert.notNull(data, "Data must not be null");
064                this.sequence = sequence;
065                this.data = data;
066        }
067
068        /**
069         * Return the sequence number of the payload.
070         * @return the sequence
071         */
072        public long getSequence() {
073                return this.sequence;
074        }
075
076        /**
077         * Assign this payload to the given {@link HttpOutputMessage}.
078         * @param message the message to assign this payload to
079         * @throws IOException in case of I/O errors
080         */
081        public void assignTo(HttpOutputMessage message) throws IOException {
082                Assert.notNull(message, "Message must not be null");
083                HttpHeaders headers = message.getHeaders();
084                headers.setContentLength(this.data.remaining());
085                headers.add(SEQ_HEADER, Long.toString(getSequence()));
086                headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
087                WritableByteChannel body = Channels.newChannel(message.getBody());
088                while (this.data.hasRemaining()) {
089                        body.write(this.data);
090                }
091                body.close();
092        }
093
094        /**
095         * Write the content of this payload to the given target channel.
096         * @param channel the channel to write to
097         * @throws IOException in case of I/O errors
098         */
099        public void writeTo(WritableByteChannel channel) throws IOException {
100                Assert.notNull(channel, "Channel must not be null");
101                while (this.data.hasRemaining()) {
102                        channel.write(this.data);
103                }
104        }
105
106        /**
107         * Return the {@link HttpTunnelPayload} for the given message or {@code null} if there
108         * is no payload.
109         * @param message the HTTP message
110         * @return the payload or {@code null}
111         * @throws IOException in case of I/O errors
112         */
113        public static HttpTunnelPayload get(HttpInputMessage message) throws IOException {
114                long length = message.getHeaders().getContentLength();
115                if (length <= 0) {
116                        return null;
117                }
118                String seqHeader = message.getHeaders().getFirst(SEQ_HEADER);
119                Assert.state(StringUtils.hasLength(seqHeader), "Missing sequence header");
120                ReadableByteChannel body = Channels.newChannel(message.getBody());
121                ByteBuffer payload = ByteBuffer.allocate((int) length);
122                while (payload.hasRemaining()) {
123                        body.read(payload);
124                }
125                body.close();
126                payload.flip();
127                return new HttpTunnelPayload(Long.valueOf(seqHeader), payload);
128        }
129
130        /**
131         * Return the payload data for the given source {@link ReadableByteChannel} or null if
132         * the channel timed out whilst reading.
133         * @param channel the source channel
134         * @return payload data or {@code null}
135         * @throws IOException in case of I/O errors
136         */
137        public static ByteBuffer getPayloadData(ReadableByteChannel channel)
138                        throws IOException {
139                ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
140                try {
141                        int amountRead = channel.read(buffer);
142                        Assert.state(amountRead != -1, "Target server connection closed");
143                        buffer.flip();
144                        return buffer;
145                }
146                catch (InterruptedIOException ex) {
147                        return null;
148                }
149        }
150
151        /**
152         * Log incoming payload information at trace level to aid diagnostics.
153         */
154        public void logIncoming() {
155                log("< ");
156        }
157
158        /**
159         * Log incoming payload information at trace level to aid diagnostics.
160         */
161        public void logOutgoing() {
162                log("> ");
163        }
164
165        private void log(String prefix) {
166                if (logger.isTraceEnabled()) {
167                        logger.trace(prefix + toHexString());
168                }
169        }
170
171        /**
172         * Return the payload as a hexadecimal string.
173         * @return the payload as a hex string
174         */
175        public String toHexString() {
176                byte[] bytes = this.data.array();
177                char[] hex = new char[this.data.remaining() * 2];
178                for (int i = this.data.position(); i < this.data.remaining(); i++) {
179                        int b = bytes[i] & 0xFF;
180                        hex[i * 2] = HEX_CHARS[b >>> 4];
181                        hex[i * 2 + 1] = HEX_CHARS[b & 0x0F];
182                }
183                return new String(hex);
184        }
185
186}