/*
* Copyright 2013 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version
* 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.jboss.aerogear.io.netty.handler.codec.sockjs.transport;
import static io.netty.buffer.Unpooled.copiedBuffer;
import static io.netty.buffer.Unpooled.unreleasableBuffer;
import static io.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_CREDENTIALS;
import static io.netty.handler.codec.http.HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN;
import static io.netty.handler.codec.http.HttpHeaders.Names.ALLOW;
import static io.netty.handler.codec.http.HttpHeaders.Names.CACHE_CONTROL;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaders.Names.SET_COOKIE;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.Cookie;
import io.netty.handler.codec.http.CookieDecoder;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.ServerCookieEncoder;
import org.jboss.aerogear.io.netty.handler.codec.sockjs.SockJsConfig;
import io.netty.util.CharsetUtil;
import java.util.Set;
/**
* Transports contains constants, enums, and utility methods that are
* common across transport implementations.
*/
public final class Transports {
public static final String CONTENT_TYPE_PLAIN = "text/plain; charset=UTF-8";
public static final String CONTENT_TYPE_JAVASCRIPT = "application/javascript; charset=UTF-8";
public static final String CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
public static final String CONTENT_TYPE_HTML = "text/html; charset=UTF-8";
public static final String DEFAULT_COOKIE = "JSESSIONID=dummy;path=/";
public static final String JSESSIONID = "JSESSIONID";
private static final String NO_CACHE_HEADER = "no-store, no-cache, must-revalidate, max-age=0";
private static final ByteBuf NL = unreleasableBuffer(copiedBuffer("\n", CharsetUtil.UTF_8));
public enum Type {
WEBSOCKET,
XHR,
XHR_SEND,
XHR_STREAMING,
JSONP,
JSONP_SEND,
EVENTSOURCE,
HTMLFILE;
public String path() {
return '/' + name().toLowerCase();
}
}
private Transports() {
}
/**
* Encodes the passes in requests JSESSIONID, if one exists, setting it to path of '/'.
*
* @param request the {@link HttpRequest} to parse for a JSESSIONID cookie.
* @return {@code String} the encoded cookie or {@value Transports#DEFAULT_COOKIE} if no JSESSIONID cookie exits.
*/
public static String encodeSessionIdCookie(final HttpRequest request) {
final String cookieHeader = request.headers().get(HttpHeaders.Names.COOKIE);
if (cookieHeader != null) {
final Set<Cookie> cookies = CookieDecoder.decode(cookieHeader);
for (Cookie c : cookies) {
if (c.getName().equals(JSESSIONID)) {
c.setPath("/");
return ServerCookieEncoder.encode(c);
}
}
}
return DEFAULT_COOKIE;
}
/**
* Writes the passed in String content to the response and also sets te content-type and content-lenght headaers.
*
* @param response the {@link FullHttpResponse} to write the content to.
* @param content the content to be written.
* @param contentType the content-type that will be set as the Content-Type Http response header.
*/
public static void writeContent(final FullHttpResponse response, final String content, final String contentType) {
final ByteBuf buf = copiedBuffer(content, CharsetUtil.UTF_8);
response.headers().set(CONTENT_LENGTH, buf.readableBytes());
response.content().writeBytes(buf);
response.headers().set(CONTENT_TYPE, contentType);
buf.release();
}
/**
* Sets the following Http response headers
* - SET_COOKIE if {@link org.jboss.aerogear.io.netty.handler.codec.sockjs.SockJsConfig#areCookiesNeeded()} is true
* - CACHE_CONTROL to {@link Transports#setNoCacheHeaders(HttpResponse)}
* - CORS Headers to {@link Transports#setCORSHeaders(HttpResponse)}
*
* @param response the Http Response.
* @param config the SockJS configuration.
*/
public static void setDefaultHeaders(final HttpResponse response, final SockJsConfig config) {
if (config.areCookiesNeeded()) {
response.headers().set(SET_COOKIE, DEFAULT_COOKIE);
}
setNoCacheHeaders(response);
setCORSHeaders(response);
}
/**
* Sets the following Http response headers
* - SET_COOKIE if {@link SockJsConfig#areCookiesNeeded()} is true, and uses the requests cookie.
* - CACHE_CONTROL to {@link Transports#setNoCacheHeaders(HttpResponse)}
* - CORS Headers to {@link Transports#setCORSHeaders(HttpResponse)}
*
* @param response the Http Response.
* @param config the SockJS configuration.
*/
public static void setDefaultHeaders(final FullHttpResponse response, final SockJsConfig config,
final HttpRequest request) {
if (config.areCookiesNeeded()) {
response.headers().set(SET_COOKIE, encodeSessionIdCookie(request));
}
setNoCacheHeaders(response);
setCORSHeaders(response);
}
/**
* Sets the Http response header SET_COOKIE if {@link SockJsConfig#areCookiesNeeded()} is true.
*
* @param response the Http Response.
* @param config the SockJS configuration.
* @param request the Http request which will be inspected for the existence of a JSESSIONID cookie.
*/
public static void setSessionIdCookie(final FullHttpResponse response, final SockJsConfig config,
final HttpRequest request) {
if (config.areCookiesNeeded()) {
response.headers().set(SET_COOKIE, encodeSessionIdCookie(request));
}
}
/**
* Sets the Http response header CACHE_CONTROL to {@link Transports#NO_CACHE_HEADER}.
*
* @param response the Http response for which the CACHE_CONTROL header will be set.
*/
public static void setNoCacheHeaders(final HttpResponse response) {
response.headers().set(CACHE_CONTROL, NO_CACHE_HEADER);
}
/**
* Sets the CORS Http response headers ACCESS_CONTROLL_ALLOW_ORIGIN to '*', and ACCESS_CONTROL_ALLOW_CREDENTIALS to
* 'true".
*
* @param response the Http response for which the CORS headers will be set.
*/
public static void setCORSHeaders(final HttpResponse response) {
response.headers().set(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
response.headers().set(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}
/**
* Will add an new line character to the passed in ByteBuf.
*
* @param buf the {@link ByteBuf} for which an '\n', new line, will be added.
* @return {@code ByteBuf} a copied byte buffer with a '\n' appended.
*/
public static ByteBuf wrapWithLN(final ByteBuf buf) {
return copiedBuffer(buf, NL.duplicate());
}
/**
* Escapes unicode characters in the passed in char array to a Java string with
* Java style escaped charaters.
*
* @param value the char[] for which unicode characters should be escaped
* @return {@code String} Java style escaped unicode characters.
*/
public static String escapeCharacters(final char[] value) {
final StringBuilder buffer = new StringBuilder();
for (char ch : value) {
if (ch >= '\u0000' && ch <= '\u001F' ||
ch >= '\uD800' && ch <= '\uDFFF' ||
ch >= '\u200C' && ch <= '\u200F' ||
ch >= '\u2028' && ch <= '\u202F' ||
ch >= '\u2060' && ch <= '\u206F' ||
ch >= '\uFFF0' && ch <= '\uFFFF') {
final String ss = Integer.toHexString(ch);
buffer.append('\\').append('u');
for (int k = 0; k < 4 - ss.length(); k++) {
buffer.append('0');
}
buffer.append(ss.toLowerCase());
} else {
buffer.append(ch);
}
}
return buffer.toString();
}
/**
* Processes the input ByteBuf and escapes the any control characters, quotes, slashes,
* and unicode characters.
*
* @param input the bytes of characters to process.
* @param buffer the {@link ByteBuf} into which the result of processing will be added.
* @return {@code ByteBuf} which is the same ByteBuf as passed in as the buffer param. This is done to
* simplify method invocation where possible which might require a return value.
*/
public static ByteBuf escapeJson(final ByteBuf input, final ByteBuf buffer) {
final int count = input.readableBytes();
for (int i = 0; i < count; i++) {
final byte ch = input.getByte(i);
switch(ch) {
case '"': buffer.writeByte('\\').writeByte('\"'); break;
case '/': buffer.writeByte('\\').writeByte('/'); break;
case '\\': buffer.writeByte('\\').writeByte('\\'); break;
case '\b': buffer.writeByte('\\').writeByte('b'); break;
case '\f': buffer.writeByte('\\').writeByte('f'); break;
case '\n': buffer.writeByte('\\').writeByte('n'); break;
case '\r': buffer.writeByte('\\').writeByte('r'); break;
case '\t': buffer.writeByte('\\').writeByte('t'); break;
default:
// Reference: http://www.unicode.org/versions/Unicode5.1.0/
if (ch >= '\u0000' && ch <= '\u001F' ||
ch >= '\uD800' && ch <= '\uDFFF' ||
ch >= '\u200C' && ch <= '\u200F' ||
ch >= '\u2028' && ch <= '\u202F' ||
ch >= '\u2060' && ch <= '\u206F' ||
ch >= '\uFFF0' && ch <= '\uFFFF') {
final String ss = Integer.toHexString(ch);
buffer.writeByte('\\').writeByte('u');
for (int k = 0; k < 4 - ss.length(); k++) {
buffer.writeByte('0');
}
buffer.writeBytes(ss.toLowerCase().getBytes());
} else {
buffer.writeByte(ch);
}
}
}
return buffer;
}
/**
* Creates a {@code FullHttpResponse} with the {@code METHOD_NOT_ALLOWED} status.
*
* @param version the HttpVersion to be used.
* @return {@link FullHttpResponse} with the {@link HttpResponseStatus#METHOD_NOT_ALLOWED}.
*/
public static FullHttpResponse methodNotAllowedResponse(final HttpVersion version) {
final FullHttpResponse response = responseWithoutContent(version, METHOD_NOT_ALLOWED);
response.headers().add(ALLOW, GET);
return response;
}
/**
* Creates a {@code FullHttpResponse} with the {@code BAD_REQUEST} status and a body.
*
* @param version the HttpVersion to be used.
* @param content the content that will become the response body.
* @return {@link FullHttpResponse} with the {@link HttpResponseStatus#BAD_REQUEST}.
*/
public static FullHttpResponse badRequestResponse(final HttpVersion version, final String content) {
return responseWithContent(version, BAD_REQUEST, CONTENT_TYPE_PLAIN, content);
}
/**
* Creates a {@code FullHttpResponse} with the {@code INTERNAL_SERVER_ERROR} status and a body.
*
* @param version the HttpVersion to be used.
* @param content the content that will become the response body.
* @return {@link FullHttpResponse} with the {@link HttpResponseStatus#INTERNAL_SERVER_ERROR}.
*/
public static FullHttpResponse internalServerErrorResponse(final HttpVersion version, final String content) {
return responseWithContent(version, INTERNAL_SERVER_ERROR, CONTENT_TYPE_PLAIN, content);
}
/**
* Sends a HttpResponse using the ChannelHandlerContext passed in.
*
* @param ctx the {@link ChannelHandlerContext} to use.
* @param version the {@link HttpVersion} that the response should have.
* @param status the status of the HTTP response
* @param contentType the value for the 'Content-Type' HTTP response header.
* @param content the content that will become the body of the HTTP response.
* @param promise the {@link ChannelPromise}
*/
public static void respond(final ChannelHandlerContext ctx,
final HttpVersion version,
final HttpResponseStatus status,
final String contentType,
final String content,
final ChannelPromise promise) {
final FullHttpResponse response = responseWithContent(version, status, contentType, content);
writeResponse(ctx, response);
}
/**
* Sends a HttpResponse using the ChannelHandlerContext passed in.
*
* @param ctx the {@link ChannelHandlerContext} to use.
* @param version the {@link HttpVersion} that the response should have.
* @param status the status of the HTTP response
* @param contentType the value for the 'Content-Type' HTTP response header.
* @param content the content that will become the body of the HTTP response.
*/
public static void respond(final ChannelHandlerContext ctx,
final HttpVersion version,
final HttpResponseStatus status,
final String contentType,
final String content) {
final FullHttpResponse response = responseWithContent(version, status, contentType, content);
writeResponse(ctx, response);
}
/**
* Creates FullHttpResponse without a response body.
*
* @param version the {@link HttpVersion} that the response should have.
* @param status the status of the HTTP response
*/
public static FullHttpResponse responseWithoutContent(final HttpVersion version, final HttpResponseStatus status) {
final FullHttpResponse response = new DefaultFullHttpResponse(version, status);
response.headers().set(CONTENT_LENGTH, 0);
return response;
}
/**
* Creates FullHttpResponse with a response body.
*
* @param version the {@link HttpVersion} that the response should have.
* @param status the status of the HTTP response
* @param contentType the value for the 'Content-Type' HTTP response header.
* @param content the content that will become the body of the HTTP response.
*/
public static FullHttpResponse responseWithContent(final HttpVersion version, final HttpResponseStatus status,
final String contentType, final String content) {
final FullHttpResponse response = new DefaultFullHttpResponse(version, status);
writeContent(response, content, contentType);
return response;
}
/**
* Writes the passed in respone to the {@link ChannelHandlerContext} if it is active.
*
* @param ctx the {@link ChannelHandlerContext} to write the response to.
* @param response the {@link HttpResponseStatus} to be written.
*/
public static void writeResponse(final ChannelHandlerContext ctx, final HttpResponse response) {
if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
ctx.writeAndFlush(response);
}
}
/**
* Writes the passed in respone to the {@link ChannelHandlerContext} if it is active.
*
* @param ctx the {@link ChannelHandlerContext} to write the response to.
* @param promise the {@link ChannelPromise}
* @param response the {@link HttpResponseStatus} to be written.
*/
public static void writeResponse(final ChannelHandlerContext ctx, final ChannelPromise promise,
final HttpResponse response) {
if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
ctx.writeAndFlush(response, promise);
}
}
}