Skip to content

Commit

Permalink
Refactor of HttpUtil and HttpHeaderUtil
Browse files Browse the repository at this point in the history
Motivation:
There currently exists http.HttpUtil, http2.HttpUtil, and http.HttpHeaderUtil. Having 2 HttpUtil methods can be confusing and the utilty methods in the http package could be consolidated.

Modifications:
- Rename http2.HttpUtil to http2.HttpConversionUtil
- Move http.HttpHeaderUtil methods into http.HttpUtil

Result:
Consolidated utilities whose names don't overlap.
Fixes netty#4120
  • Loading branch information
Scottmitch committed Aug 27, 2015
1 parent ff26c15 commit fcd8a3e
Show file tree
Hide file tree
Showing 32 changed files with 483 additions and 161 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import io.netty.util.internal.logging.InternalLoggerFactory;

import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderUtil.getContentLength;
import static io.netty.handler.codec.http.HttpUtil.getContentLength;

/**
* A {@link ChannelHandler} that aggregates an {@link HttpMessage}
Expand Down Expand Up @@ -119,7 +119,7 @@ protected boolean isContentLengthInvalid(HttpMessage start, int maxContentLength

@Override
protected Object newContinueResponse(HttpMessage start, int maxContentLength, ChannelPipeline pipeline) {
if (HttpHeaderUtil.is100ContinueExpected(start)) {
if (HttpUtil.is100ContinueExpected(start)) {
if (getContentLength(start, -1) <= maxContentLength) {
return CONTINUE.duplicate().retain();
}
Expand All @@ -145,7 +145,7 @@ protected boolean ignoreContentAfterContinueResponse(Object msg) {
protected FullHttpMessage beginAggregation(HttpMessage start, ByteBuf content) throws Exception {
assert !(start instanceof FullHttpMessage);

HttpHeaderUtil.setTransferEncodingChunked(start, false);
HttpUtil.setTransferEncodingChunked(start, false);

AggregatedFullHttpMessage ret;
if (start instanceof HttpRequest) {
Expand Down Expand Up @@ -174,7 +174,7 @@ protected void finishAggregation(FullHttpMessage aggregated) throws Exception {
// transmitted if a GET would have been used.
//
// See rfc2616 14.13 Content-Length
if (!HttpHeaderUtil.isContentLengthSet(aggregated)) {
if (!HttpUtil.isContentLengthSet(aggregated)) {
aggregated.headers().set(
HttpHeaderNames.CONTENT_LENGTH,
String.valueOf(aggregated.content().readableBytes()));
Expand All @@ -199,7 +199,7 @@ public void operationComplete(ChannelFuture future) throws Exception {
// If the client started to send data already, close because it's impossible to recover.
// If keep-alive is off and 'Expect: 100-continue' is missing, no need to leave the connection open.
if (oversized instanceof FullHttpMessage ||
!HttpHeaderUtil.is100ContinueExpected(oversized) && !HttpHeaderUtil.isKeepAlive(oversized)) {
!HttpUtil.is100ContinueExpected(oversized) && !HttpUtil.isKeepAlive(oversized)) {
future.addListener(ChannelFutureListener.CLOSE);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> ou

// Handle the last unfinished message.
if (message != null) {
boolean chunked = HttpHeaderUtil.isTransferEncodingChunked(message);
boolean chunked = HttpUtil.isTransferEncodingChunked(message);
if (currentState == State.READ_VARIABLE_LENGTH_CONTENT && !in.isReadable() && !chunked) {
// End of connection.
out.add(LastHttpContent.EMPTY_LAST_CONTENT);
Expand Down Expand Up @@ -588,9 +588,9 @@ private State readHeaders(ByteBuf buffer) {
State nextState;

if (isContentAlwaysEmpty(message)) {
HttpHeaderUtil.setTransferEncodingChunked(message, false);
HttpUtil.setTransferEncodingChunked(message, false);
nextState = State.SKIP_CONTROL_CHARS;
} else if (HttpHeaderUtil.isTransferEncodingChunked(message)) {
} else if (HttpUtil.isTransferEncodingChunked(message)) {
nextState = State.READ_CHUNK_SIZE;
} else if (contentLength() >= 0) {
nextState = State.READ_FIXED_LENGTH_CONTENT;
Expand All @@ -602,7 +602,7 @@ private State readHeaders(ByteBuf buffer) {

private long contentLength() {
if (contentLength == Long.MIN_VALUE) {
contentLength = HttpHeaderUtil.getContentLength(message, -1);
contentLength = HttpUtil.getContentLength(message, -1);
}
return contentLength;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) t
encodeInitialLine(buf, m);
encodeHeaders(m.headers(), buf);
buf.writeBytes(CRLF);
state = HttpHeaderUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK;
state = HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK;
}

// Bypass the encoder in case of an empty buffer, so that the following idiom works:
Expand Down
251 changes: 251 additions & 0 deletions codec-http/src/main/java/io/netty/handler/codec/http/HttpUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/
package io.netty.handler.codec.http;

import io.netty.buffer.ByteBuf;

import java.net.URI;
import java.util.Iterator;
import java.util.List;

/**
* Utility methods useful in the HTTP context.
Expand All @@ -42,4 +46,251 @@ public static boolean isAsteriskForm(URI uri) {
uri.getHost() == null && uri.getAuthority() == null && uri.getQuery() == null &&
uri.getFragment() == null;
}

/**
* Returns {@code true} if and only if the connection can remain open and
* thus 'kept alive'. This methods respects the value of the
* {@code "Connection"} header first and then the return value of
* {@link HttpVersion#isKeepAliveDefault()}.
*/
public static boolean isKeepAlive(HttpMessage message) {
CharSequence connection = message.headers().get(HttpHeaderNames.CONNECTION);
if (connection != null && HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(connection)) {
return false;
}

if (message.protocolVersion().isKeepAliveDefault()) {
return !HttpHeaderValues.CLOSE.contentEqualsIgnoreCase(connection);
} else {
return HttpHeaderValues.KEEP_ALIVE.contentEqualsIgnoreCase(connection);
}
}

/**
* Sets the value of the {@code "Connection"} header depending on the
* protocol version of the specified message. This getMethod sets or removes
* the {@code "Connection"} header depending on what the default keep alive
* mode of the message's protocol version is, as specified by
* {@link HttpVersion#isKeepAliveDefault()}.
* <ul>
* <li>If the connection is kept alive by default:
* <ul>
* <li>set to {@code "close"} if {@code keepAlive} is {@code false}.</li>
* <li>remove otherwise.</li>
* </ul></li>
* <li>If the connection is closed by default:
* <ul>
* <li>set to {@code "keep-alive"} if {@code keepAlive} is {@code true}.</li>
* <li>remove otherwise.</li>
* </ul></li>
* </ul>
*/
public static void setKeepAlive(HttpMessage message, boolean keepAlive) {
HttpHeaders h = message.headers();
if (message.protocolVersion().isKeepAliveDefault()) {
if (keepAlive) {
h.remove(HttpHeaderNames.CONNECTION);
} else {
h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
}
} else {
if (keepAlive) {
h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
} else {
h.remove(HttpHeaderNames.CONNECTION);
}
}
}

/**
* Returns the length of the content. Please note that this value is
* not retrieved from {@link HttpContent#content()} but from the
* {@code "Content-Length"} header, and thus they are independent from each
* other.
*
* @return the content length
*
* @throws NumberFormatException
* if the message does not have the {@code "Content-Length"} header
* or its value is not a number
*/
public static long getContentLength(HttpMessage message) {
Long value = message.headers().getLong(HttpHeaderNames.CONTENT_LENGTH);
if (value != null) {
return value;
}

// We know the content length if it's a Web Socket message even if
// Content-Length header is missing.
long webSocketContentLength = getWebSocketContentLength(message);
if (webSocketContentLength >= 0) {
return webSocketContentLength;
}

// Otherwise we don't.
throw new NumberFormatException("header not found: " + HttpHeaderNames.CONTENT_LENGTH);
}

/**
* Returns the length of the content. Please note that this value is
* not retrieved from {@link HttpContent#content()} but from the
* {@code "Content-Length"} header, and thus they are independent from each
* other.
*
* @return the content length or {@code defaultValue} if this message does
* not have the {@code "Content-Length"} header or its value is not
* a number
*/
public static long getContentLength(HttpMessage message, long defaultValue) {
Long value = message.headers().getLong(HttpHeaderNames.CONTENT_LENGTH);
if (value != null) {
return value;
}

// We know the content length if it's a Web Socket message even if
// Content-Length header is missing.
long webSocketContentLength = getWebSocketContentLength(message);
if (webSocketContentLength >= 0) {
return webSocketContentLength;
}

// Otherwise we don't.
return defaultValue;
}

/**
* Get an {@code int} representation of {@link #getContentLength(HttpMessage, long)}.
* @return the content length or {@code defaultValue} if this message does
* not have the {@code "Content-Length"} header or its value is not
* a number. Not to exceed the boundaries of integer.
*/
public static int getContentLength(HttpMessage message, int defaultValue) {
return (int) Math.min(Integer.MAX_VALUE, HttpHeaderUtil.getContentLength(message, (long) defaultValue));
}

/**
* Returns the content length of the specified web socket message. If the
* specified message is not a web socket message, {@code -1} is returned.
*/
private static int getWebSocketContentLength(HttpMessage message) {
// WebSockset messages have constant content-lengths.
HttpHeaders h = message.headers();
if (message instanceof HttpRequest) {
HttpRequest req = (HttpRequest) message;
if (HttpMethod.GET.equals(req.method()) &&
h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY1) &&
h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY2)) {
return 8;
}
} else if (message instanceof HttpResponse) {
HttpResponse res = (HttpResponse) message;
if (res.status().code() == 101 &&
h.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN) &&
h.contains(HttpHeaderNames.SEC_WEBSOCKET_LOCATION)) {
return 16;
}
}

// Not a web socket message
return -1;
}

/**
* Sets the {@code "Content-Length"} header.
*/
public static void setContentLength(HttpMessage message, long length) {
message.headers().setLong(HttpHeaderNames.CONTENT_LENGTH, length);
}

public static boolean isContentLengthSet(HttpMessage m) {
return m.headers().contains(HttpHeaderNames.CONTENT_LENGTH);
}

/**
* Returns {@code true} if and only if the specified message contains the
* {@code "Expect: 100-continue"} header.
*/
public static boolean is100ContinueExpected(HttpMessage message) {
// Expect: 100-continue is for requests only.
if (!(message instanceof HttpRequest)) {
return false;
}

// It works only on HTTP/1.1 or later.
if (message.protocolVersion().compareTo(HttpVersion.HTTP_1_1) < 0) {
return false;
}

// In most cases, there will be one or zero 'Expect' header.
CharSequence value = message.headers().get(HttpHeaderNames.EXPECT);
if (value == null) {
return false;
}
if (HttpHeaderValues.CONTINUE.contentEqualsIgnoreCase(value)) {
return true;
}

// Multiple 'Expect' headers. Search through them.
return message.headers().contains(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE, true);
}

/**
* Sets or removes the {@code "Expect: 100-continue"} header to / from the
* specified message. If the specified {@code value} is {@code true},
* the {@code "Expect: 100-continue"} header is set and all other previous
* {@code "Expect"} headers are removed. Otherwise, all {@code "Expect"}
* headers are removed completely.
*/
public static void set100ContinueExpected(HttpMessage message, boolean expected) {
if (expected) {
message.headers().set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
} else {
message.headers().remove(HttpHeaderNames.EXPECT);
}
}

/**
* Checks to see if the transfer encoding in a specified {@link HttpMessage} is chunked
*
* @param message The message to check
* @return True if transfer encoding is chunked, otherwise false
*/
public static boolean isTransferEncodingChunked(HttpMessage message) {
return message.headers().contains(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED, true);
}

public static void setTransferEncodingChunked(HttpMessage m, boolean chunked) {
if (chunked) {
m.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
m.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
} else {
List<CharSequence> values = m.headers().getAll(HttpHeaderNames.TRANSFER_ENCODING);
if (values.isEmpty()) {
return;
}
Iterator<CharSequence> valuesIt = values.iterator();
while (valuesIt.hasNext()) {
CharSequence value = valuesIt.next();
if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(value)) {
valuesIt.remove();
}
}
if (values.isEmpty()) {
m.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
} else {
m.headers().set(HttpHeaderNames.TRANSFER_ENCODING, values);
}
}
}

static void encodeAscii0(CharSequence seq, ByteBuf buf) {
int length = seq.length();
for (int i = 0 ; i < length; i++) {
buf.writeByte(c2b(seq.charAt(i)));
}
}

private static byte c2b(char c) {
return c > 255 ? (byte) '?' : (byte) c;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import io.netty.handler.codec.http.HttpConstants;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
Expand Down Expand Up @@ -761,7 +761,7 @@ public HttpRequest finalizeRequest() throws ErrorDataEncoderException {
}
}
}
HttpHeaderUtil.setTransferEncodingChunked(request, true);
HttpUtil.setTransferEncodingChunked(request, true);

// wrap to hide the possible content
return new WrappedHttpRequest(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
Expand Down Expand Up @@ -151,7 +151,7 @@ public static ChannelFuture sendUnsupportedVersionResponse(Channel channel, Chan
HttpVersion.HTTP_1_1,
HttpResponseStatus.UPGRADE_REQUIRED);
res.headers().set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, WebSocketVersion.V13.toHttpHeaderValue());
HttpHeaderUtil.setContentLength(res, 0);
HttpUtil.setContentLength(res, 0);
return channel.writeAndFlush(res, promise);
}
}
Loading

0 comments on commit fcd8a3e

Please sign in to comment.