/* * 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.handler; import static io.netty.buffer.Unpooled.copiedBuffer; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.util.CharsetUtil.UTF_8; import io.netty.buffer.ByteBuf; 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.HttpResponseStatus; import io.netty.handler.codec.http.QueryStringDecoder; import org.jboss.aerogear.io.netty.handler.codec.sockjs.SockJsConfig; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Formatter; import java.util.regex.Pattern; /** * IFrame is a way to get around problems in browsers where the streaming protocols do not support * cross domain communication. * * The SockJS client library can in these cases issue a request with a path starting * with '/iframe'. The class will respond with a iframe that contains SockJS JavaScript * which is then able to do cross domain calls. */ final class Iframe { private static final Pattern PATH_PATTERN = Pattern.compile(".*/iframe[0-9-.a-z_]*.html"); private static final long ONE_YEAR = 31536000000L; private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); } }; private static final ThreadLocal<MessageDigest> MESSAGE_DIGEST = new ThreadLocal<MessageDigest>() { @Override protected MessageDigest initialValue() { try { return MessageDigest.getInstance("MD5"); } catch (final NoSuchAlgorithmException e) { throw new IllegalStateException("Could not create a new MD5 instance", e); } } }; private Iframe() { } public static boolean matches(final String path) { return path.startsWith("/iframe"); } public static FullHttpResponse response(final SockJsConfig config, final HttpRequest request) throws Exception { final QueryStringDecoder qsd = new QueryStringDecoder(request.getUri()); final String path = qsd.path(); if (!PATH_PATTERN.matcher(path).matches()) { return createResponse(request, NOT_FOUND, copiedBuffer("Not found", UTF_8)); } if (request.headers().contains(HttpHeaders.Names.IF_NONE_MATCH)) { final FullHttpResponse response = createResponse(request, NOT_MODIFIED); response.headers().set(HttpHeaders.Names.SET_COOKIE, "JSESSIONID=dummy; path=/"); return response; } else { final String content = createContent(config.sockJsUrl()); final FullHttpResponse response = createResponse(request, OK, copiedBuffer(content, UTF_8)); response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=UTF-8"); response.headers().set(HttpHeaders.Names.CACHE_CONTROL, "max-age=31536000, public"); response.headers().set(HttpHeaders.Names.EXPIRES, generateExpires()); final String etag = '\"' + generateMd5(content) + '\"'; response.headers().set(HttpHeaders.Names.ETAG, etag); return response; } } private static String generateExpires() { return DATE_FORMAT.get().format(new Date(System.currentTimeMillis() + ONE_YEAR)); } private static FullHttpResponse createResponse(final HttpRequest request, final HttpResponseStatus status) { return new DefaultFullHttpResponse(request.getProtocolVersion(), status); } private static FullHttpResponse createResponse(final HttpRequest request, final HttpResponseStatus status, final ByteBuf content) { return new DefaultFullHttpResponse(request.getProtocolVersion(), status, content); } private static String createContent(final String url) { return "<!DOCTYPE html>\n" + "<html>\n" + "<head>\n" + " <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n" + " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" + " <script>\n" + " document.domain = document.domain;\n" + " _sockjs_onload = function(){SockJS.bootstrap_iframe();};\n" + " </script>\n" + " <script src=\"" + url + "\"></script>\n" + "</head>\n" + "<body>\n" + " <h2>Don't panic!</h2>\n" + " <p>This is a SockJS hidden iframe. It's used for cross domain magic.</p>\n" + "</body>\n" + "</html>"; } private static String generateMd5(final String value) throws Exception { final byte[] digest = MESSAGE_DIGEST.get().digest(value.getBytes(UTF_8)); Formatter formatter = null; try { formatter = new Formatter(); for (byte b : digest) { formatter.format("%02x", b); } return formatter.toString().toLowerCase(); } finally { if (formatter != null) { formatter.close(); } } } }