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}