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}