/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.quercus.lib; import com.caucho.network.listen.SocketLinkDuplexListener; import com.caucho.network.listen.SocketLinkDuplexController; import com.caucho.quercus.module.AbstractQuercusModule; import com.caucho.quercus.env.Env; import com.caucho.quercus.env.StringValue; import com.caucho.quercus.env.StringBuilderValue; import com.caucho.quercus.env.BooleanValue; import com.caucho.quercus.env.Value; import com.caucho.server.http.CauchoRequest; import com.caucho.util.*; import com.caucho.vfs.*; import java.io.*; import java.util.Locale; import java.util.logging.*; public class WebSocketModule extends AbstractQuercusModule { private static final L10N L = new L10N(WebSocketModule.class); private static final Logger log = Logger.getLogger(WebSocketModule.class.getName()); /** * Writes a string to the websocket. */ public static Value websocket_write(Env env, StringValue string) { try { OutputStream out = env.getResponse().getOutputStream(); int length = string.length(); int offset = 0; out.write(0x00); for (; offset < length; offset++) { char ch = string.charAt(offset); if ((ch & 0xf0) == 0xf0) { env.error("websocket_write expects utf-8 encoded string"); } out.write(ch); } out.write(0xff); out.flush(); return BooleanValue.TRUE; } catch (IOException e) { log.log(Level.WARNING, e.toString(), e); return BooleanValue.FALSE; } } /** * Reads a string from the websocket. */ public static Value websocket_read(Env env) { try { InputStream is = env.getRequest().getInputStream(); int ch; for (ch = is.read(); Character.isWhitespace(ch); ch = is.read()) { } if (ch != 0x00) { log.fine("websocket_read expected 0x00 at '0x" + Integer.toHexString(ch) + "'"); return BooleanValue.FALSE; } StringValue sb = env.createStringBuilder(); while ((ch = is.read()) >= 0 && ch != 0xff) { if (ch < 0x80) sb.append((char) ch); else if ((ch & 0xe0) == 0xc0) { int ch2 = is.read(); if ((ch2 & 0x80) == 0x80) { sb.append(ch); sb.append(ch2); } else { log.fine("websocket_read expected 0x80 character at '0x" + Integer.toHexString(ch2) + "' for string " + sb); sb.append(0xfe); sb.append(0xdd); } } else if ((ch & 0xf0) == 0xe0) { int ch2 = is.read(); int ch3 = is.read(); if ((ch2 & 0x80) == 0x80 && (ch3 & 0x80) == 0x80) { sb.append(ch); sb.append(ch2); sb.append(ch3); } else { log.fine("websocket_read expected 0x80 character at " + " '0x" + Integer.toHexString(ch2) + "' '0x" + Integer.toHexString(ch3) + "' for string " + sb); sb.append(0xfe); sb.append(0xdd); } } else { log.fine("websocket_read invalid lead character " + " '0x" + Integer.toHexString(ch) + "' for string " + sb); sb.append(0xfe); sb.append(0xdd); } } if (ch != 0xff) { log.fine("websocket_read expected 0xff " + " '0x" + Integer.toHexString(ch) + "' for string " + sb); } return sb; } catch (IOException e) { log.log(Level.WARNING, e.toString(), e); return BooleanValue.FALSE; } } /** * Reads a string from the websocket. */ public static SocketLinkDuplexController websocket_start(Env env, StringValue path) { if (! (env.getRequest() instanceof CauchoRequest)) { env.warning("websocket_start requires a Resin request at " + env.getRequest()); return null; } CauchoRequest request = (CauchoRequest) env.getRequest(); String connection = request.getHeader("Connection"); String upgrade = request.getHeader("Upgrade"); if (! "WebSocket".equals(upgrade)) { env.warning("request Upgrade header '" + upgrade + "' must be 'WebSocket' for a websocket_start"); return null; } if (! "Upgrade".equalsIgnoreCase(connection)) { env.warning("request connection header '" + connection + "' must be 'Upgrade' for a websocket_start"); return null; } String origin = request.getHeader("Origin"); if (origin == null) { env.warning("websocket_start requires an 'Origin' header in the request"); return null; } env.getResponse().setStatus(101, "Web Socket Protocol Handshake"); env.getResponse().setHeader("Upgrade", "WebSocket"); String protocol = request.getHeader("WebSocket-Protocol"); StringBuilder sb = new StringBuilder(); if (request.isSecure()) sb.append("wss://"); else sb.append("ws://"); sb.append(request.getServerName()); if (! request.isSecure() && request.getServerPort() != 80 || request.isSecure() && request.getServerPort() != 443) { sb.append(":"); sb.append(request.getServerPort()); } sb.append(request.getContextPath()); if (request.getServletPath() != null) sb.append(request.getServletPath()); String url = sb.toString(); if (origin != null) env.getResponse().setHeader("WebSocket-Origin", origin.toLowerCase(Locale.ENGLISH)); if (protocol != null) env.getResponse().setHeader("WebSocket-Protocol", protocol); env.getResponse().setHeader("WebSocket-Location", url); // XXX: validate path QuercusWebSocketListener listener = new QuercusWebSocketListener(env, path); SocketLinkDuplexController context = null;//request.startDuplex(listener); // context.setTimeout(30 * 60000L); env.startDuplex(context); return context; } public static class QuercusWebSocketListener implements SocketLinkDuplexListener { private Env _env; private StringValue _path; QuercusWebSocketListener(Env env, StringValue path) { _env = env; _path = path; } @Override public void onRead(SocketLinkDuplexController context) { boolean isValid = false; try { if (log.isLoggable(Level.FINE)) log.fine(this + " WebSocket read " + _path); _env.include(_path); isValid = true; } finally { if (! isValid) { if (log.isLoggable(Level.FINE)) log.fine(this + " WebSocket exit " + _path); context.complete(); } } } public void onDisconnect(SocketLinkDuplexController context) { try { _env.closeDuplex(); } finally { context.complete(); } } public void onTimeout(SocketLinkDuplexController context) { try { _env.closeDuplex(); } finally { context.complete(); } } public String toString() { return getClass().getSimpleName() + "[" + _path + "]"; } @Override public void onStart(SocketLinkDuplexController context) throws IOException { } } }