001/* 002 * Copyright 2002-2019 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 * https://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.web.socket.sockjs.transport.handler; 018 019import java.io.IOException; 020import java.util.Map; 021 022import org.springframework.http.HttpStatus; 023import org.springframework.http.MediaType; 024import org.springframework.http.server.ServerHttpRequest; 025import org.springframework.http.server.ServerHttpResponse; 026import org.springframework.util.StringUtils; 027import org.springframework.web.socket.CloseStatus; 028import org.springframework.web.socket.WebSocketHandler; 029import org.springframework.web.socket.sockjs.SockJsException; 030import org.springframework.web.socket.sockjs.SockJsTransportFailureException; 031import org.springframework.web.socket.sockjs.frame.DefaultSockJsFrameFormat; 032import org.springframework.web.socket.sockjs.frame.SockJsFrameFormat; 033import org.springframework.web.socket.sockjs.transport.SockJsServiceConfig; 034import org.springframework.web.socket.sockjs.transport.SockJsSession; 035import org.springframework.web.socket.sockjs.transport.TransportHandler; 036import org.springframework.web.socket.sockjs.transport.TransportType; 037import org.springframework.web.socket.sockjs.transport.session.AbstractHttpSockJsSession; 038import org.springframework.web.socket.sockjs.transport.session.StreamingSockJsSession; 039import org.springframework.web.util.JavaScriptUtils; 040 041/** 042 * An HTTP {@link TransportHandler} that uses a famous browser 043 * {@code document.domain technique}. See <a href= 044 * "https://stackoverflow.com/questions/1481251/what-does-document-domain-document-domain-do"> 045 * stackoverflow.com/questions/1481251/what-does-document-domain-document-domain-do</a> 046 * for details. 047 * 048 * @author Rossen Stoyanchev 049 * @since 4.0 050 */ 051public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandler { 052 053 private static final String PARTIAL_HTML_CONTENT; 054 055 // Safari needs at least 1024 bytes to parse the website. 056 // https://code.google.com/p/browsersec/wiki/Part2#Survey_of_content_sniffing_behaviors 057 private static final int MINIMUM_PARTIAL_HTML_CONTENT_LENGTH = 1024; 058 059 060 static { 061 StringBuilder sb = new StringBuilder( 062 "<!doctype html>\n" + 063 "<html><head>\n" + 064 " <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n" + 065 " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" + 066 "</head><body><h2>Don't panic!</h2>\n" + 067 " <script>\n" + 068 " document.domain = document.domain;\n" + 069 " var c = parent.%s;\n" + 070 " c.start();\n" + 071 " function p(d) {c.message(d);};\n" + 072 " window.onload = function() {c.stop();};\n" + 073 " </script>" 074 ); 075 076 while (sb.length() < MINIMUM_PARTIAL_HTML_CONTENT_LENGTH) { 077 sb.append(" "); 078 } 079 PARTIAL_HTML_CONTENT = sb.toString(); 080 } 081 082 083 @Override 084 public TransportType getTransportType() { 085 return TransportType.HTML_FILE; 086 } 087 088 @Override 089 protected MediaType getContentType() { 090 return new MediaType("text", "html", UTF8_CHARSET); 091 } 092 093 @Override 094 public boolean checkSessionType(SockJsSession session) { 095 return (session instanceof HtmlFileStreamingSockJsSession); 096 } 097 098 @Override 099 public StreamingSockJsSession createSession( 100 String sessionId, WebSocketHandler handler, Map<String, Object> attributes) { 101 102 return new HtmlFileStreamingSockJsSession(sessionId, getServiceConfig(), handler, attributes); 103 } 104 105 @Override 106 public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response, 107 AbstractHttpSockJsSession sockJsSession) throws SockJsException { 108 109 String callback = getCallbackParam(request); 110 if (!StringUtils.hasText(callback)) { 111 response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); 112 try { 113 response.getBody().write("\"callback\" parameter required".getBytes(UTF8_CHARSET)); 114 } 115 catch (IOException ex) { 116 sockJsSession.tryCloseWithSockJsTransportError(ex, CloseStatus.SERVER_ERROR); 117 throw new SockJsTransportFailureException("Failed to write to response", sockJsSession.getId(), ex); 118 } 119 return; 120 } 121 122 super.handleRequestInternal(request, response, sockJsSession); 123 } 124 125 @Override 126 protected SockJsFrameFormat getFrameFormat(ServerHttpRequest request) { 127 return new DefaultSockJsFrameFormat("<script>\np(\"%s\");\n</script>\r\n") { 128 @Override 129 protected String preProcessContent(String content) { 130 return JavaScriptUtils.javaScriptEscape(content); 131 } 132 }; 133 } 134 135 136 private class HtmlFileStreamingSockJsSession extends StreamingSockJsSession { 137 138 public HtmlFileStreamingSockJsSession(String sessionId, SockJsServiceConfig config, 139 WebSocketHandler wsHandler, Map<String, Object> attributes) { 140 141 super(sessionId, config, wsHandler, attributes); 142 } 143 144 @Override 145 protected byte[] getPrelude(ServerHttpRequest request) { 146 // We already validated the parameter above... 147 String callback = getCallbackParam(request); 148 String html = String.format(PARTIAL_HTML_CONTENT, callback); 149 return html.getBytes(UTF8_CHARSET); 150 } 151 } 152 153}