Author: markt Date: Tue Nov 27 22:04:53 2012 New Revision: 1414427 URL: http://svn.apache.org/viewvc?rev=1414427&view=rev Log: WebSocket 1.0 implementation part 3 of many Complete the WsServlet implementation Now gets as far as Endpoint.onOpen() and then immediately closes Add the new programmatic echo endpoint to the examples web app for testing
Added: tomcat/trunk/java/org/apache/tomcat/websocket/WsEndpointPojo.java (with props) tomcat/trunk/java/org/apache/tomcat/websocket/WsProtocolHandler.java (with props) tomcat/trunk/java/org/apache/tomcat/websocket/WsSession.java (with props) Modified: tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties tomcat/trunk/java/org/apache/tomcat/websocket/ServerContainerImpl.java tomcat/trunk/java/org/apache/tomcat/websocket/WsServlet.java tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/echo/EchoEndpoint.java tomcat/trunk/webapps/examples/websocket/echo.html Modified: tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties?rev=1414427&r1=1414426&r2=1414427&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/LocalStrings.properties Tue Nov 27 22:04:53 2012 @@ -14,6 +14,7 @@ # limitations under the License. sci.newInstance.fail=Failed to create an Endpoint instance of type [{0}] serverContainer.endpointDeploy=Endpoint class [{0}] deploying to path [{1}] in ServletContext [{2}] +serverContainer.missingEndpoint=An Endpoint instance has been request for path [{0}] but no matching Endpoint class was found serverContainer.pojoDeploy=POJO class [{0}] deploying to path [{1}] in ServletContext [{2}] serverContainer.servletContextMismatch=Attempted to register a POJO annotated for WebSocket at path [{0}] in the ServletContext with context path [{1}] when the WebSocket ServerContainer is allocated to the ServletContext with context path [{2}] serverContainer.servletContextMissing=No ServletContext was specified \ No newline at end of file Modified: tomcat/trunk/java/org/apache/tomcat/websocket/ServerContainerImpl.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/ServerContainerImpl.java?rev=1414427&r1=1414426&r2=1414427&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/ServerContainerImpl.java (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/ServerContainerImpl.java Tue Nov 27 22:04:53 2012 @@ -160,4 +160,23 @@ public class ServerContainerImpl extends sr.addMapping(mapping); } + + + public Endpoint getEndpoint(String servletPath) + throws InstantiationException, IllegalAccessException { + Class<? extends Endpoint> clazzEndpoint = endpointMap.get(servletPath); + if (clazzEndpoint != null) { + Endpoint ep = clazzEndpoint.newInstance(); + return ep; + } + + Class<?> clazzPojo = pojoMap.get(servletPath); + if (clazzPojo != null) { + Endpoint ep = new WsEndpointPojo(clazzPojo, servletPath); + return ep; + } + + throw new IllegalStateException( + sm.getString("serverContainer.missingEndpoint", servletPath)); + } } Added: tomcat/trunk/java/org/apache/tomcat/websocket/WsEndpointPojo.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/WsEndpointPojo.java?rev=1414427&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/WsEndpointPojo.java (added) +++ tomcat/trunk/java/org/apache/tomcat/websocket/WsEndpointPojo.java Tue Nov 27 22:04:53 2012 @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.tomcat.websocket; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.websocket.CloseReason; +import javax.websocket.DefaultServerConfiguration; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfiguration; +import javax.websocket.Session; + +public class WsEndpointPojo extends Endpoint { + + private final Object pojo; + private final EndpointConfiguration config; + private final Method onOpen; + private final Method onClose; + private final Method onError; + + public WsEndpointPojo(Class<?> clazzPojo, String path) + throws InstantiationException, IllegalAccessException { + this.pojo = clazzPojo.newInstance(); + this.config = new DefaultServerConfiguration(path); + + // TODO - Find these + this.onOpen = null; + this.onClose = null; + this.onError = null; + } + + @Override + public EndpointConfiguration getEndpointConfiguration() { + return config; + } + + @Override + public void onOpen(Session session) { + if (onOpen != null) { + try { + onOpen.invoke(pojo, session); + } catch (IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + @Override + public void onClose(CloseReason closeReason) { + if (onClose != null) { + try { + onClose.invoke(pojo, (Object[]) null); + } catch (IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + @Override + public void onError(Throwable throwable) { + if (onError != null) { + try { + onError.invoke(pojo, throwable); + } catch (IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } +} Propchange: tomcat/trunk/java/org/apache/tomcat/websocket/WsEndpointPojo.java ------------------------------------------------------------------------------ svn:eol-style = native Added: tomcat/trunk/java/org/apache/tomcat/websocket/WsProtocolHandler.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/WsProtocolHandler.java?rev=1414427&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/WsProtocolHandler.java (added) +++ tomcat/trunk/java/org/apache/tomcat/websocket/WsProtocolHandler.java Tue Nov 27 22:04:53 2012 @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.tomcat.websocket; + +import javax.servlet.http.ProtocolHandler; +import javax.servlet.http.WebConnection; +import javax.websocket.Endpoint; + +public class WsProtocolHandler implements ProtocolHandler { + + private final Endpoint ep; + + public WsProtocolHandler(Endpoint ep) { + this.ep = ep; + } + + @Override + public void init(WebConnection connection) { + + ep.onOpen(new WsSession()); + + // TODO Message handling + } +} Propchange: tomcat/trunk/java/org/apache/tomcat/websocket/WsProtocolHandler.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: tomcat/trunk/java/org/apache/tomcat/websocket/WsServlet.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/WsServlet.java?rev=1414427&r1=1414426&r2=1414427&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/WsServlet.java (original) +++ tomcat/trunk/java/org/apache/tomcat/websocket/WsServlet.java Tue Nov 27 22:04:53 2012 @@ -17,21 +17,192 @@ package org.apache.tomcat.websocket; import java.io.IOException; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.ProtocolHandler; +import javax.websocket.Endpoint; +import javax.websocket.ServerEndpointConfiguration; +import javax.xml.bind.DatatypeConverter; + public class WsServlet extends HttpServlet { private static final long serialVersionUID = 1L; + private static final Charset ISO_8859_1; + static { + ISO_8859_1 = Charset.forName("ISO-8859-1"); + } + + private static final byte[] WS_ACCEPT = + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".getBytes(ISO_8859_1); + + private final Queue<MessageDigest> sha1Helpers = + new ConcurrentLinkedQueue<>(); + + + @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - resp.setContentType("text/plain"); - resp.getWriter().print("TODO: Implement HTTP Upgrade"); + // Information required to send the server handshake message + String key; + String subProtocol = null; + List<String> extensions = Collections.emptyList(); + + if (!headerContainsToken(req, "upgrade", "websocket")) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + if (!headerContainsToken(req, "connection", "upgrade")) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + if (!headerContainsToken(req, "sec-websocket-version", "13")) { + resp.setStatus(426); + resp.setHeader("Sec-WebSocket-Version", "13"); + return; + } + + key = req.getHeader("Sec-WebSocket-Key"); + if (key == null) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + // Need an Endpoint instance to progress this further + ServerContainerImpl cp = ServerContainerImpl.getServerContainer(); + Endpoint ep = null; + try { + ep = cp.getEndpoint(req.getServletPath()); + } catch (InstantiationException | IllegalAccessException e) { + // This will trigger an error response + throw new ServletException(e); + } + + ServerEndpointConfiguration epConfig = + (ServerEndpointConfiguration) ep.getEndpointConfiguration(); + + // Origin check + String origin = req.getHeader("Origin"); + if (!epConfig.checkOrigin(origin)) { + resp.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + + // Sub-protocols + List<String> subProtocols = + getTokensFromHeader(req, "Sec-WebSocket-Protocol"); + if (!subProtocols.isEmpty()) { + subProtocol = epConfig.getNegotiatedSubprotocol(subProtocols); + } + + // Extensions + List<String> requestedExtensions = + getTokensFromHeader(req, "Sec-WebSocket-Extensions"); + if (!extensions.isEmpty()) { + extensions = epConfig.getNegotiatedExtensions(requestedExtensions); + } + + // If we got this far, all is good. Accept the connection. + resp.setHeader("Upgrade", "websocket"); + resp.setHeader("Connection", "upgrade"); + resp.setHeader("Sec-WebSocket-Accept", getWebSocketAccept(key)); + if (subProtocol != null) { + resp.setHeader("Sec-WebSocket-Protocol", subProtocol); + } + if (!extensions.isEmpty()) { + StringBuilder sb = new StringBuilder(); + Iterator<String> iter = extensions.iterator(); + // There must be at least one + sb.append(iter.next()); + while (iter.hasNext()) { + sb.append(','); + sb.append(iter.next()); + } + resp.setHeader("Sec-WebSocket-Extensions", sb.toString()); + } + + ProtocolHandler wsHandler = new WsProtocolHandler(ep); + + req.upgrade(wsHandler); + } + + + /* + * This only works for tokens. Quoted strings need more sophisticated + * parsing. + */ + private boolean headerContainsToken(HttpServletRequest req, + String headerName, String target) { + Enumeration<String> headers = req.getHeaders(headerName); + while (headers.hasMoreElements()) { + String header = headers.nextElement(); + String[] tokens = header.split(","); + for (String token : tokens) { + if (target.equalsIgnoreCase(token.trim())) { + return true; + } + } + } + return false; + } + + + /* + * This only works for tokens. Quoted strings need more sophisticated + * parsing. + */ + private List<String> getTokensFromHeader(HttpServletRequest req, + String headerName) { + List<String> result = new ArrayList<>(); + + Enumeration<String> headers = req.getHeaders(headerName); + while (headers.hasMoreElements()) { + String header = headers.nextElement(); + String[] tokens = header.split(","); + for (String token : tokens) { + result.add(token.trim()); + } + } + return result; + } + + + private String getWebSocketAccept(String key) throws ServletException { + + MessageDigest sha1Helper = sha1Helpers.poll(); + if (sha1Helper == null) { + try { + sha1Helper = MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + throw new ServletException(e); + } + } + + sha1Helper.reset(); + sha1Helper.update(key.getBytes(ISO_8859_1)); + String result = DatatypeConverter.printBase64Binary( + sha1Helper.digest(WS_ACCEPT)); + + sha1Helpers.add(sha1Helper); + + return result; } } Added: tomcat/trunk/java/org/apache/tomcat/websocket/WsSession.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/websocket/WsSession.java?rev=1414427&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/websocket/WsSession.java (added) +++ tomcat/trunk/java/org/apache/tomcat/websocket/WsSession.java Tue Nov 27 22:04:53 2012 @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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.apache.tomcat.websocket; + +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.websocket.ClientContainer; +import javax.websocket.CloseReason; +import javax.websocket.Encoder; +import javax.websocket.MessageHandler; +import javax.websocket.RemoteEndpoint; +import javax.websocket.Session; + +public class WsSession implements Session { + + @Override + public ClientContainer getContainer() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setEncoders(List<Encoder> encoders) { + // TODO Auto-generated method stub + + } + + @Override + public void addMessageHandler(MessageHandler listener) { + // TODO Auto-generated method stub + + } + + @Override + public Set<MessageHandler> getMessageHandlers() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void removeMessageHandler(MessageHandler listener) { + // TODO Auto-generated method stub + + } + + @Override + public String getProtocolVersion() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getNegotiatedSubprotocol() { + // TODO Auto-generated method stub + return null; + } + + @Override + public List<String> getNegotiatedExtensions() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isSecure() { + // TODO Auto-generated method stub + return false; + } + + @Override + public long getInactiveTime() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public boolean isActive() { + // TODO Auto-generated method stub + return false; + } + + @Override + public long getTimeout() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void setTimeout(long seconds) { + // TODO Auto-generated method stub + + } + + @Override + public void setMaximumMessageSize(long length) { + // TODO Auto-generated method stub + + } + + @Override + public long getMaximumMessageSize() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public RemoteEndpoint getRemote() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void close() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void close(CloseReason closeStatus) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public URI getRequestURI() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map<String, String[]> getRequestParameterMap() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getQueryString() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Map<String, String> getPathParameters() { + // TODO Auto-generated method stub + return null; + } + +} Propchange: tomcat/trunk/java/org/apache/tomcat/websocket/WsSession.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/echo/EchoEndpoint.java URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/echo/EchoEndpoint.java?rev=1414427&r1=1414426&r2=1414427&view=diff ============================================================================== --- tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/echo/EchoEndpoint.java (original) +++ tomcat/trunk/webapps/examples/WEB-INF/classes/websocket/echo/EchoEndpoint.java Tue Nov 27 22:04:53 2012 @@ -25,7 +25,14 @@ import javax.websocket.Session; public class EchoEndpoint extends Endpoint{ private static ServerEndpointConfiguration config = - new DefaultServerConfiguration("/websocket/echoProgrammatic"); + new DefaultServerConfiguration("/websocket/echoProgrammatic") { + + @Override + public boolean checkOrigin(String originHeaderValue) { + // Accept any + return true; + } + }; @Override public EndpointConfiguration getEndpointConfiguration() { @@ -34,6 +41,8 @@ public class EchoEndpoint extends Endpoi @Override public void onOpen(Session session) { + // TODO - Review this debug hack + System.out.println("EchoEndpoint onOpen() called"); // TODO Auto-generated method stub } Modified: tomcat/trunk/webapps/examples/websocket/echo.html URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/websocket/echo.html?rev=1414427&r1=1414426&r2=1414427&view=diff ============================================================================== --- tomcat/trunk/webapps/examples/websocket/echo.html (original) +++ tomcat/trunk/webapps/examples/websocket/echo.html Tue Nov 27 22:04:53 2012 @@ -137,6 +137,9 @@ <!-- echo example using messages on the server side --> <input id="radio2" type="radio" name="group1" value="/examples/websocket/echoMessage" onclick="updateTarget(this.value);"> <label for="radio2">messages</label> + <!-- echo example using new programmatic API on the server side --> + <input id="radio3" type="radio" name="group1" value="/examples/websocket/echoProgrammatic" + onclick="updateTarget(this.value);"> <label for="radio2">new programmatic</label> </div> <div> <input id="target" type="text" size="40" style="width: 350px"/> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org