Esempio n. 1
1
  private static HttpResponse addEtag(
      HttpRequest nettyRequest, HttpResponse httpResponse, File file) {
    if (Play.mode == Play.Mode.DEV) {
      httpResponse.setHeader(CACHE_CONTROL, "no-cache");
    } else {
      String maxAge = Play.configuration.getProperty("http.cacheControl", "3600");
      if (maxAge.equals("0")) {
        httpResponse.setHeader(CACHE_CONTROL, "no-cache");
      } else {
        httpResponse.setHeader(CACHE_CONTROL, "max-age=" + maxAge);
      }
    }
    boolean useEtag = Play.configuration.getProperty("http.useETag", "true").equals("true");
    long last = file.lastModified();
    final String etag = "\"" + last + "-" + file.hashCode() + "\"";
    if (!isModified(etag, last, nettyRequest)) {
      if (nettyRequest.getMethod().equals(HttpMethod.GET)) {
        httpResponse.setStatus(HttpResponseStatus.NOT_MODIFIED);
      }
      if (useEtag) {
        httpResponse.setHeader(ETAG, etag);
      }

    } else {
      httpResponse.setHeader(LAST_MODIFIED, Utils.getHttpDateFormatter().format(new Date(last)));
      if (useEtag) {
        httpResponse.setHeader(ETAG, etag);
      }
    }
    return httpResponse;
  }
Esempio n. 2
0
  protected static void addToRequest(HttpRequest nettyRequest, Request request) {
    for (String key : nettyRequest.getHeaderNames()) {
      Http.Header hd = new Http.Header();
      hd.name = key.toLowerCase();
      hd.values = new ArrayList<String>();
      for (String next : nettyRequest.getHeaders(key)) {
        hd.values.add(next);
      }
      request.headers.put(hd.name, hd);
    }

    String value = nettyRequest.getHeader(COOKIE);
    if (value != null) {
      Set<Cookie> cookies = new CookieDecoder().decode(value);
      if (cookies != null) {
        for (Cookie cookie : cookies) {
          Http.Cookie playCookie = new Http.Cookie();
          playCookie.name = cookie.getName();
          playCookie.path = cookie.getPath();
          playCookie.domain = cookie.getDomain();
          playCookie.secure = cookie.isSecure();
          playCookie.value = cookie.getValue();
          playCookie.httpOnly = cookie.isHttpOnly();
          request.cookies.put(playCookie.name, playCookie);
        }
      }
    }
  }
Esempio n. 3
0
  public static boolean isModified(String etag, long last, HttpRequest nettyRequest) {

    if (nettyRequest.containsHeader(IF_NONE_MATCH)) {
      final String browserEtag = nettyRequest.getHeader(IF_NONE_MATCH);
      if (browserEtag.equals(etag)) {
        return false;
      }
      return true;
    }

    if (nettyRequest.containsHeader(IF_MODIFIED_SINCE)) {
      final String ifModifiedSince = nettyRequest.getHeader(IF_MODIFIED_SINCE);

      if (!StringUtils.isEmpty(ifModifiedSince)) {
        try {
          Date browserDate = Utils.getHttpDateFormatter().parse(ifModifiedSince);
          if (browserDate.getTime() >= last) {
            return false;
          }
        } catch (ParseException ex) {
          Logger.warn("Can't parse HTTP date", ex);
        }
        return true;
      }
    }
    return true;
  }
Esempio n. 4
0
  protected static void writeResponse(
      ChannelHandlerContext ctx,
      Response response,
      HttpResponse nettyResponse,
      HttpRequest nettyRequest) {
    Logger.trace("writeResponse: begin");
    byte[] content = null;

    final boolean keepAlive = isKeepAlive(nettyRequest);
    if (nettyRequest.getMethod().equals(HttpMethod.HEAD)) {
      content = new byte[0];
    } else {
      content = response.out.toByteArray();
    }

    ChannelBuffer buf = ChannelBuffers.copiedBuffer(content);
    nettyResponse.setContent(buf);

    if (keepAlive) {
      // Add 'Content-Length' header only for a keep-alive connection.
      Logger.trace("writeResponse: content length [" + response.out.size() + "]");
      setContentLength(nettyResponse, response.out.size());
    }

    ChannelFuture f = ctx.getChannel().write(nettyResponse);

    // Decide whether to close the connection or not.
    if (!keepAlive) {
      // Close the connection when the whole content is written out.
      f.addListener(ChannelFutureListener.CLOSE);
    }
    Logger.trace("writeResponse: end");
  }
Esempio n. 5
0
  void saveExceededSizeError(HttpRequest nettyRequest, Request request, Response response) {

    String warning = nettyRequest.getHeader(HttpHeaders.Names.WARNING);
    String length = nettyRequest.getHeader(HttpHeaders.Names.CONTENT_LENGTH);
    if (warning != null) {
      Logger.trace("saveExceededSizeError: begin");
      try {
        StringBuilder error = new StringBuilder();
        error.append("\u0000");
        // Cannot put warning which is play.netty.content.length.exceeded
        // as Key as it will result error when printing error
        error.append("play.netty.maxContentLength");
        error.append(":");
        String size = null;
        try {
          size = JavaExtensions.formatSize(Long.parseLong(length));
        } catch (Exception e) {
          size = length + " bytes";
        }
        error.append(Messages.get(warning, size));
        error.append("\u0001");
        error.append(size);
        error.append("\u0000");
        if (request.cookies.get(Scope.COOKIE_PREFIX + "_ERRORS") != null
            && request.cookies.get(Scope.COOKIE_PREFIX + "_ERRORS").value != null) {
          error.append(request.cookies.get(Scope.COOKIE_PREFIX + "_ERRORS").value);
        }
        String errorData = URLEncoder.encode(error.toString(), "utf-8");
        Http.Cookie c = new Http.Cookie();
        c.value = errorData;
        c.name = Scope.COOKIE_PREFIX + "_ERRORS";
        request.cookies.put(Scope.COOKIE_PREFIX + "_ERRORS", c);
        Logger.trace("saveExceededSizeError: end");
      } catch (Exception e) {
        throw new UnexpectedException("Error serialization problem", e);
      }
    }
  }
Esempio n. 6
0
  @Override
  public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e)
      throws Exception {
    Logger.trace("messageReceived: begin");
    final Object msg = e.getMessage();

    // Http request
    if (msg instanceof HttpRequest) {

      final HttpRequest nettyRequest = (HttpRequest) msg;

      // Websocket upgrade
      if (HttpHeaders.Values.UPGRADE.equalsIgnoreCase(nettyRequest.getHeader(CONNECTION))
          && HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase(
              nettyRequest.getHeader(HttpHeaders.Names.UPGRADE))) {
        websocketHandshake(ctx, nettyRequest, e);
        return;
      }

      // Plain old HttpRequest
      try {
        final Request request = parseRequest(ctx, nettyRequest);

        final Response response = new Response();
        Http.Response.current.set(response);

        // Buffered in memory output
        response.out = new ByteArrayOutputStream();

        // Direct output (will be set later)
        response.direct = null;

        // Streamed output (using response.writeChunk)
        response.onWriteChunk(
            new Action<Object>() {

              public void invoke(Object result) {
                writeChunk(request, response, ctx, nettyRequest, result);
              }
            });

        // Raw invocation
        boolean raw = false;
        for (PlayPlugin plugin : Play.plugins) {
          if (plugin.rawInvocation(request, response)) {
            raw = true;
            break;
          }
        }
        if (raw) {
          copyResponse(ctx, request, response, nettyRequest);
        } else {

          // Deleguate to Play framework
          Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, e));
        }

      } catch (Exception ex) {
        serve500(ex, ctx, nettyRequest);
      }
    }

    // Websocket frame
    if (msg instanceof WebSocketFrame) {
      WebSocketFrame frame = (WebSocketFrame) msg;
      websocketFrameReceived(ctx, frame);
    }

    Logger.trace("messageReceived: end");
  }
Esempio n. 7
0
  public void serveStatic(
      RenderStatic renderStatic,
      ChannelHandlerContext ctx,
      Request request,
      Response response,
      HttpRequest nettyRequest,
      MessageEvent e) {
    Logger.trace("serveStatic: begin");
    HttpResponse nettyResponse =
        new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.status));
    if (exposePlayServer) {
      nettyResponse.setHeader(SERVER, signature);
    }
    try {
      VirtualFile file = Play.getVirtualFile(renderStatic.file);
      if (file != null && file.exists() && file.isDirectory()) {
        file = file.child("index.html");
        if (file != null) {
          renderStatic.file = file.relativePath();
        }
      }
      if ((file == null || !file.exists())) {
        serve404(
            new NotFound("The file " + renderStatic.file + " does not exist"),
            ctx,
            request,
            nettyRequest);
      } else {
        boolean raw = false;
        for (PlayPlugin plugin : Play.plugins) {
          if (plugin.serveStatic(file, Request.current(), Response.current())) {
            raw = true;
            break;
          }
        }
        if (raw) {
          copyResponse(ctx, request, response, nettyRequest);
        } else {
          final File localFile = file.getRealFile();
          final boolean keepAlive = isKeepAlive(nettyRequest);
          nettyResponse = addEtag(nettyRequest, nettyResponse, localFile);

          if (nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {

            Channel ch = e.getChannel();

            // Write the initial line and the header.
            ChannelFuture writeFuture = ch.write(nettyResponse);
            if (!keepAlive) {
              // Write the content.
              writeFuture.addListener(ChannelFutureListener.CLOSE);
            }
          } else {

            final RandomAccessFile raf = new RandomAccessFile(localFile, "r");
            try {
              long fileLength = raf.length();

              Logger.trace("keep alive " + keepAlive);
              Logger.trace(
                  "content type " + (MimeTypes.getContentType(localFile.getName(), "text/plain")));

              if (keepAlive && !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {
                // Add 'Content-Length' header only for a keep-alive connection.
                Logger.trace("file length " + fileLength);
                setContentLength(nettyResponse, fileLength);
              }

              nettyResponse.setHeader(
                  CONTENT_TYPE, (MimeTypes.getContentType(localFile.getName(), "text/plain")));

              Channel ch = e.getChannel();

              // Write the initial line and the header.
              ChannelFuture writeFuture = ch.write(nettyResponse);

              // Write the content.
              if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)) {
                writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192));
              } else {
                raf.close();
              }

              if (!keepAlive) {
                // Close the connection when the whole content is written out.
                writeFuture.addListener(ChannelFutureListener.CLOSE);
              }
            } catch (Throwable exx) {
              try {
                raf.close();
              } catch (Throwable ex) {
                /* Left empty */
              }
              try {
                ctx.getChannel().close();
              } catch (Throwable ex) {
                /* Left empty */
              }
            }
          }
        }
      }
    } catch (Throwable ez) {
      Logger.error(ez, "serveStatic for request %s", request.method + " " + request.url);
      try {
        HttpResponse errorResponse =
            new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        ChannelBuffer buf =
            ChannelBuffers.copiedBuffer("Internal Error (check logs)".getBytes("utf-8"));
        errorResponse.setContent(buf);
        ChannelFuture future = ctx.getChannel().write(errorResponse);
        future.addListener(ChannelFutureListener.CLOSE);
      } catch (Exception ex) {
        Logger.error(ez, "serveStatic for request %s", request.method + " " + request.url);
      }
    }
    Logger.trace("serveStatic: end");
  }
Esempio n. 8
0
  public Request parseRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest)
      throws Exception {
    Logger.trace("parseRequest: begin");
    Logger.trace("parseRequest: URI = " + nettyRequest.getUri());
    int index = nettyRequest.getUri().indexOf("?");
    String querystring = "";

    String uri = nettyRequest.getUri();
    // Remove domain and port from URI if it's present.
    if (uri.startsWith("http://") || uri.startsWith("https://")) {
      // Begins searching / after 9th character (last / of https://)
      uri = uri.substring(uri.indexOf("/", 9));
    }

    String path = URLDecoder.decode(uri, "UTF-8");
    if (index != -1) {
      path = URLDecoder.decode(uri.substring(0, index), "UTF-8");
      querystring = uri.substring(index + 1);
    }

    final Request request = new Request();

    request.remoteAddress = getRemoteIPAddress(ctx);
    request.method = nettyRequest.getMethod().getName();
    request.path = path;
    request.querystring = querystring;
    final String contentType = nettyRequest.getHeader(CONTENT_TYPE);
    if (contentType != null) {
      request.contentType = contentType.split(";")[0].trim().toLowerCase();
    } else {
      request.contentType = "text/html";
    }

    if (nettyRequest.getHeader("X-HTTP-Method-Override") != null) {
      request.method = nettyRequest.getHeader("X-HTTP-Method-Override").intern();
    }

    ChannelBuffer b = nettyRequest.getContent();
    if (b instanceof FileChannelBuffer) {
      FileChannelBuffer buffer = (FileChannelBuffer) b;
      // An error occurred
      Integer max =
          Integer.valueOf(Play.configuration.getProperty("play.netty.maxContentLength", "-1"));

      request.body = buffer.getInputStream();
      if (!(max == -1 || request.body.available() < max)) {
        request.body = new ByteArrayInputStream(new byte[0]);
      }

    } else {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      IOUtils.copy(new ChannelBufferInputStream(b), out);
      byte[] n = out.toByteArray();
      request.body = new ByteArrayInputStream(n);
    }

    request.url = uri;
    request.host = nettyRequest.getHeader(HOST);
    request.isLoopback =
        ((InetSocketAddress) ctx.getChannel().getRemoteAddress()).getAddress().isLoopbackAddress()
            && request.host.matches("^127\\.0\\.0\\.1:?[0-9]*$");

    if (request.host == null) {
      request.host = "";
      request.port = 80;
      request.domain = "";
    } else {
      if (request.host.contains(":")) {
        final String[] host = request.host.split(":");
        request.port = Integer.parseInt(host[1]);
        request.domain = host[0];
      } else {
        request.port = 80;
        request.domain = request.host;
      }
    }

    if (Play.configuration.containsKey("XForwardedSupport")
        && nettyRequest.getHeader("X-Forwarded-For") != null) {
      if (!Arrays.asList(
              Play.configuration.getProperty("XForwardedSupport", "127.0.0.1").split(","))
          .contains(request.remoteAddress)) {
        throw new RuntimeException(
            "This proxy request is not authorized: " + request.remoteAddress);
      } else {
        request.secure =
            ("https".equals(Play.configuration.get("XForwardedProto"))
                || "https".equals(nettyRequest.getHeader("X-Forwarded-Proto"))
                || "on".equals(nettyRequest.getHeader("X-Forwarded-Ssl")));
        if (Play.configuration.containsKey("XForwardedHost")) {
          request.host = (String) Play.configuration.get("XForwardedHost");
        } else if (nettyRequest.getHeader("X-Forwarded-Host") != null) {
          request.host = nettyRequest.getHeader("X-Forwarded-Host");
        }
        if (nettyRequest.getHeader("X-Forwarded-For") != null) {
          request.remoteAddress = nettyRequest.getHeader("X-Forwarded-For");
        }
      }
    }

    addToRequest(nettyRequest, request);

    request.resolveFormat();

    request._init();

    Logger.trace("parseRequest: end");
    return request;
  }
Esempio n. 9
0
  public void copyResponse(
      ChannelHandlerContext ctx, Request request, Response response, HttpRequest nettyRequest)
      throws Exception {
    Logger.trace("copyResponse: begin");
    // response.out.flush();

    // Decide whether to close the connection or not.

    HttpResponse nettyResponse =
        new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.status));
    if (exposePlayServer) {
      nettyResponse.setHeader(SERVER, signature);
    }

    if (response.contentType != null) {
      nettyResponse.setHeader(
          CONTENT_TYPE,
          response.contentType
              + (response.contentType.startsWith("text/")
                      && !response.contentType.contains("charset")
                  ? "; charset=utf-8"
                  : ""));
    } else {
      nettyResponse.setHeader(CONTENT_TYPE, "text/plain; charset=utf-8");
    }

    addToResponse(response, nettyResponse);

    final Object obj = response.direct;
    File file = null;
    ChunkedInput stream = null;
    InputStream is = null;
    if (obj instanceof File) {
      file = (File) obj;
    } else if (obj instanceof InputStream) {
      is = (InputStream) obj;
    } else if (obj instanceof ChunkedInput) {
      stream = (ChunkedInput) obj;
    }

    final boolean keepAlive = isKeepAlive(nettyRequest);
    if (file != null && file.isFile()) {
      try {
        nettyResponse = addEtag(nettyRequest, nettyResponse, file);
        if (nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {

          Channel ch = ctx.getChannel();

          // Write the initial line and the header.
          ChannelFuture writeFuture = ch.write(nettyResponse);

          if (!keepAlive) {
            // Close the connection when the whole content is written out.
            writeFuture.addListener(ChannelFutureListener.CLOSE);
          }
        } else {
          nettyResponse.setHeader(
              CONTENT_TYPE, MimeTypes.getContentType(file.getName(), "text/plain"));
          final RandomAccessFile raf = new RandomAccessFile(file, "r");
          try {
            long fileLength = raf.length();

            if (keepAlive) {
              // Add 'Content-Length' header only for a keep-alive connection.
              Logger.trace("file length is [" + fileLength + "]");
              setContentLength(nettyResponse, fileLength);
            }

            Channel ch = ctx.getChannel();

            // Write the initial line and the header.
            ChannelFuture writeFuture = ch.write(nettyResponse);

            // Write the content.
            // If it is not a HEAD
            if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)) {
              writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192));
            } else {
              raf.close();
            }
            if (!keepAlive) {
              // Close the connection when the whole content is written out.
              writeFuture.addListener(ChannelFutureListener.CLOSE);
            }
          } catch (Throwable exx) {
            try {
              raf.close();
            } catch (Throwable ex) {
              /* Left empty */
            }
            try {
              ctx.getChannel().close();
            } catch (Throwable ex) {
              /* Left empty */
            }
          }
        }
      } catch (Exception e) {
        throw e;
      }
    } else if (is != null) {
      ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse);
      if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)
          && !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {
        writeFuture = ctx.getChannel().write(new ChunkedStream(is));
      } else {
        is.close();
      }
      if (!keepAlive) {
        writeFuture.addListener(ChannelFutureListener.CLOSE);
      }
    } else if (stream != null) {
      ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse);
      if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)
          && !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {
        writeFuture = ctx.getChannel().write(stream);
      } else {
        stream.close();
      }
      if (!keepAlive) {
        writeFuture.addListener(ChannelFutureListener.CLOSE);
      }
    } else {
      writeResponse(ctx, response, nettyResponse, nettyRequest);
    }
    Logger.trace("copyResponse: end");
  }
Esempio n. 10
0
  private void websocketHandshake(final ChannelHandlerContext ctx, HttpRequest req, MessageEvent e)
      throws Exception {

    // Create the WebSocket handshake response.
    HttpResponse res =
        new DefaultHttpResponse(
            HttpVersion.HTTP_1_1, new HttpResponseStatus(101, "Web Socket Protocol Handshake"));
    res.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET);
    res.addHeader(CONNECTION, HttpHeaders.Values.UPGRADE);

    // Fill in the headers and contents depending on handshake method.
    if (req.containsHeader(SEC_WEBSOCKET_KEY1) && req.containsHeader(SEC_WEBSOCKET_KEY2)) {
      // New handshake method with a challenge:
      res.addHeader(SEC_WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
      res.addHeader(
          SEC_WEBSOCKET_LOCATION, "ws://" + req.getHeader(HttpHeaders.Names.HOST) + req.getUri());
      String protocol = req.getHeader(SEC_WEBSOCKET_PROTOCOL);
      if (protocol != null) {
        res.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol);
      }

      // Calculate the answer of the challenge.
      String key1 = req.getHeader(SEC_WEBSOCKET_KEY1);
      String key2 = req.getHeader(SEC_WEBSOCKET_KEY2);
      int a =
          (int)
              (Long.parseLong(key1.replaceAll("[^0-9]", ""))
                  / key1.replaceAll("[^ ]", "").length());
      int b =
          (int)
              (Long.parseLong(key2.replaceAll("[^0-9]", ""))
                  / key2.replaceAll("[^ ]", "").length());
      long c = req.getContent().readLong();
      ChannelBuffer input = ChannelBuffers.buffer(16);
      input.writeInt(a);
      input.writeInt(b);
      input.writeLong(c);
      try {
        ChannelBuffer output =
            ChannelBuffers.wrappedBuffer(MessageDigest.getInstance("MD5").digest(input.array()));
        res.setContent(output);
      } catch (NoSuchAlgorithmException ex) {
        throw new UnexpectedException(ex);
      }
    } else {
      // Old handshake method with no challenge:
      res.addHeader(WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
      res.addHeader(
          WEBSOCKET_LOCATION, "ws://" + req.getHeader(HttpHeaders.Names.HOST) + req.getUri());
      String protocol = req.getHeader(WEBSOCKET_PROTOCOL);
      if (protocol != null) {
        res.addHeader(WEBSOCKET_PROTOCOL, protocol);
      }
    }

    // Keep the original request
    Http.Request request = parseRequest(ctx, req);

    // Route the websocket request
    request.method = "WS";
    Map<String, String> route = Router.route(request.method, request.path);
    if (!route.containsKey("action")) {
      // No route found to handle this websocket connection
      ctx.getChannel()
          .write(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND));
      return;
    }

    // Upgrade the connection and send the handshake response.
    ChannelPipeline p = ctx.getChannel().getPipeline();
    p.remove("aggregator");
    p.replace("decoder", "wsdecoder", new WebSocketFrameDecoder());

    // Connect
    ctx.getChannel().write(res);

    p.replace("encoder", "wsencoder", new WebSocketFrameEncoder());
    req.setMethod(new HttpMethod("WEBSOCKET"));

    // Inbound
    Http.Inbound inbound =
        new Http.Inbound() {

          @Override
          public boolean isOpen() {
            return ctx.getChannel().isOpen();
          }
        };
    channels.put(ctx, inbound);

    // Outbound
    Http.Outbound outbound =
        new Http.Outbound() {

          final List<ChannelFuture> writeFutures =
              Collections.synchronizedList(new ArrayList<ChannelFuture>());
          Promise<Void> closeTask;

          synchronized void writeAndClose(ChannelFuture writeFuture) {
            if (!writeFuture.isDone()) {
              writeFutures.add(writeFuture);
              writeFuture.addListener(
                  new ChannelFutureListener() {

                    public void operationComplete(ChannelFuture cf) throws Exception {
                      writeFutures.remove(cf);
                      futureClose();
                    }
                  });
            }
          }

          void futureClose() {
            if (closeTask != null && writeFutures.isEmpty()) {
              closeTask.invoke(null);
            }
          }

          @Override
          public void send(String data) {
            if (!isOpen()) {
              throw new IllegalStateException("The outbound channel is closed");
            }
            writeAndClose(ctx.getChannel().write(new DefaultWebSocketFrame(data)));
          }

          @Override
          public void send(byte opcode, byte[] data, int offset, int length) {
            if (!isOpen()) {
              throw new IllegalStateException("The outbound channel is closed");
            }
            writeAndClose(
                ctx.getChannel()
                    .write(new DefaultWebSocketFrame(opcode, wrappedBuffer(data, offset, length))));
          }

          @Override
          public synchronized boolean isOpen() {
            return ctx.getChannel().isOpen() && closeTask == null;
          }

          @Override
          public synchronized void close() {
            closeTask = new Promise<Void>();
            closeTask.onRedeem(
                new Action<Promise<Void>>() {

                  public void invoke(Promise<Void> completed) {
                    writeFutures.clear();
                    ctx.getChannel().disconnect();
                    closeTask = null;
                  }
                });
            futureClose();
          }
        };

    Invoker.invoke(new WebSocketInvocation(route, request, inbound, outbound, ctx, e));
  }