/* * RED5 Open Source Flash Server - http://code.google.com/p/red5/ * * Copyright 2006-2012 by respective authors (see below). All rights reserved. * * Licensed 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.red5.server.net.rtmpt; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.List; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.mina.core.buffer.IoBuffer; import org.red5.logging.Red5LoggerFactory; import org.red5.server.api.Red5; import org.red5.server.net.rtmp.IRTMPConnManager; import org.red5.server.net.rtmp.RTMPConnection; import org.red5.server.net.servlet.ServletUtils; import org.slf4j.Logger; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; /** * Servlet that handles all RTMPT requests. * * @author The Red5 Project (red5@osflash.org) * @author Joachim Bauch (jojo@struktur.de) * @author Paul Gregoire (mondain@gmail.com) */ public class RTMPTServlet extends HttpServlet { /** * Serialization UID */ private static final long serialVersionUID = 5925399677454936613L; /** * Logger */ protected static Logger log = Red5LoggerFactory.getLogger(RTMPTServlet.class); /** * HTTP request method to use for RTMPT calls. */ private static final String REQUEST_METHOD = "POST"; /** * Content-Type to use for RTMPT requests / responses. */ private static final String CONTENT_TYPE = "application/x-fcs"; /** * Try to generate responses that contain at least 32768 bytes data. * Increasing this value results in better stream performance, but also increases the latency. */ private static int targetResponseSize = 32768; /** * Reference to RTMPT handler; */ private static RTMPTHandler handler; private static IRTMPConnManager rtmpConnManager; /** * Response sent for ident2 requests. If this is null a 404 will be returned */ private static String ident2; // Whether or not to enforce content type checking for requests private boolean enforceContentTypeCheck; /** * Thread local for request info storage */ protected ThreadLocal<RequestInfo> requestInfo = new ThreadLocal<RequestInfo>(); /** * Web app context */ protected transient WebApplicationContext appCtx; /** {@inheritDoc} */ @Override public void init() throws ServletException { super.init(); ServletContext ctx = getServletContext(); appCtx = WebApplicationContextUtils.getWebApplicationContext(ctx); if (appCtx == null) { appCtx = (WebApplicationContext) ctx.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); } } /** * Return an error message to the client. * * @param message Message * @param resp Servlet response * @throws IOException I/O exception */ protected void handleBadRequest(String message, HttpServletResponse resp) throws IOException { log.debug("handleBadRequest {}", message); resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); resp.setHeader("Connection", "Keep-Alive"); resp.setHeader("Cache-Control", "no-cache"); resp.setContentType("text/plain"); resp.setContentLength(message.length()); resp.getWriter().write(message); resp.flushBuffer(); } /** * Return a single byte to the client. * * @param message Message * @param resp Servlet response * @throws IOException I/O exception */ protected void returnMessage(byte message, HttpServletResponse resp) throws IOException { log.debug("returnMessage {}", message); resp.setStatus(HttpServletResponse.SC_OK); resp.setHeader("Connection", "Keep-Alive"); resp.setHeader("Cache-Control", "no-cache"); resp.setContentType(CONTENT_TYPE); resp.setContentLength(1); resp.getWriter().write(message); resp.flushBuffer(); } /** * Return a message to the client. * * @param message Message * @param resp Servlet response * @throws IOException I/O exception */ protected void returnMessage(String message, HttpServletResponse resp) throws IOException { log.debug("returnMessage {}", message); resp.setStatus(HttpServletResponse.SC_OK); resp.setHeader("Connection", "Keep-Alive"); resp.setHeader("Cache-Control", "no-cache"); resp.setContentType(CONTENT_TYPE); resp.setContentLength(message.length()); resp.getWriter().write(message); resp.flushBuffer(); } /** * Return raw data to the client. * * @param client RTMP connection * @param buffer Raw data as byte buffer * @param resp Servlet response * @throws IOException I/O exception */ protected void returnMessage(RTMPTConnection client, IoBuffer buffer, HttpServletResponse resp) throws IOException { log.trace("returnMessage {}", buffer); resp.setStatus(HttpServletResponse.SC_OK); resp.setHeader("Connection", "Keep-Alive"); resp.setHeader("Cache-Control", "no-cache"); resp.setContentType(CONTENT_TYPE); resp.setContentLength(buffer.limit() + 1); byte pollingDelay = client.getPollingDelay(); log.debug("Sending {} bytes; polling delay: {}", buffer.limit(), pollingDelay); ServletOutputStream output = resp.getOutputStream(); output.write(pollingDelay); ServletUtils.copy(buffer.asInputStream(), output); buffer.free(); buffer = null; } /** * Sets the request info for the current request. Request info contains the session id and request number gathered from * the incoming request. The URI is in this form /[method]/[session id]/[request number] ie. /send/CAFEBEEF01/7 * * @param req Servlet request */ protected void setRequestInfo(HttpServletRequest req) { String[] arr = req.getRequestURI().trim().split("/"); log.trace("Request parts: {}", Arrays.toString(arr)); RequestInfo info = new RequestInfo(arr[2], Integer.valueOf(arr[3])); requestInfo.set(info); } /** * Skip data sent by the client. * * @param req * Servlet request * @throws IOException * I/O exception */ protected void skipData(HttpServletRequest req) throws IOException { log.trace("skipData {}", req); IoBuffer data = IoBuffer.allocate(req.getContentLength()); ServletUtils.copy(req.getInputStream(), data.asOutputStream()); data.flip(); data.free(); data = null; } /** * Send pending messages to client. * * @param client RTMP connection * @param resp Servlet response */ protected void returnPendingMessages(RTMPTConnection client, HttpServletResponse resp) { log.debug("returnPendingMessages {}", client); // grab any pending outgoing data IoBuffer data = client.getPendingMessages(targetResponseSize); if (data != null) { try { returnMessage(client, data, resp); } catch (Exception ex) { // using "Exception" is meant to catch any exception that would occur when doing a write // this can be an IOException or a container specific one like ClientAbortException from catalina log.warn("Exception returning outgoing data", ex); client.realClose(); } } else { log.debug("No messages to send"); if (client.isClosing()) { log.debug("Client is closing, send close notification"); try { // tell client to close connection returnMessage((byte) 0, resp); } catch (IOException ex) { log.warn("Exception returning outgoing data - close notification", ex); } } else { try { returnMessage(client.getPollingDelay(), resp); } catch (IOException ex) { log.warn("Exception returning outgoing data - polling delay", ex); } } } } /** * Start a new RTMPT session. * * @param req Servlet request * @param resp Servlet response * @throws IOException I/O exception */ protected void handleOpen(HttpServletRequest req, HttpServletResponse resp) throws IOException { log.debug("handleOpen"); // skip sent data skipData(req); // TODO: should we evaluate the pathinfo? RTMPTConnection connection = createConnection(); if (connection != null) { connection.setServlet(this); connection.setServletRequest(req); if (connection.getId() != 0) { // return session id to client returnMessage(connection.getSessionId() + "\n", resp); } else { // no more clients are available for serving returnMessage((byte) 0, resp); } } else { resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); resp.setHeader("Connection", "Keep-Alive"); resp.setHeader("Cache-Control", "no-cache"); resp.flushBuffer(); } } /** * Close a RTMPT session. * * @param req Servlet request * @param resp Servlet response * @throws IOException I/O exception */ protected void handleClose(HttpServletRequest req, HttpServletResponse resp) throws IOException { log.debug("handleClose"); // skip sent data skipData(req); // get the associated connection RTMPTConnection connection = getConnection(); if (connection != null) { log.debug("Pending messges on close: {}", connection.getPendingMessages()); handler.connectionClosed(connection, connection.getState()); returnMessage((byte) 0, resp); connection.close(); connection.realClose(); } else { handleBadRequest(String.format("Close: unknown client session: %d", requestInfo.get().getSessionId()), resp); } } /** * Add data for an established session. * * @param req Servlet request * @param resp Servlet response * @throws IOException I/O exception */ protected void handleSend(HttpServletRequest req, HttpServletResponse resp) throws IOException { log.debug("handleSend"); final RTMPTConnection connection = getConnection(); if (connection != null) { // put the received data in a ByteBuffer int length = req.getContentLength(); log.trace("Request content length: {}", length); final IoBuffer data = IoBuffer.allocate(length); ServletUtils.copy(req.getInputStream(), data.asOutputStream()); data.flip(); // Thread reader = new Thread(new Runnable() { // public void run() { // decode the objects in the data final List<?> messages = connection.decode(data); // clear the buffer data.free(); // messages are either of IoBuffer or Packet type // handshaking uses IoBuffer and everything else should be Packet connection.read(messages); // } // }, "Reader#" + sessionId + "@" + System.nanoTime()); // reader.start(); // return pending returnPendingMessages(connection, resp); // // try { // reader.join(); // } catch (InterruptedException e) { // e.printStackTrace(); // } } else { handleBadRequest(String.format("Send: unknown client session: %s", requestInfo.get().getSessionId()), resp); } } /** * Poll RTMPT session for updates. * * @param req Servlet request * @param resp Servlet response * @throws IOException I/O exception */ protected void handleIdle(HttpServletRequest req, HttpServletResponse resp) throws IOException { log.debug("handleIdle"); // skip sent data skipData(req); // get associated connection RTMPTConnection connection = getConnection(); if (connection != null) { // return pending returnPendingMessages(connection, resp); } else { handleBadRequest(String.format("Idle: unknown client session: %s", requestInfo.get().getSessionId()), resp); } } /** * Main entry point for the servlet. * * @param req Request object * @param resp Response object * @throws IOException I/O exception */ @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { log.debug("Request - method: {} content type: {} path: {}", new Object[] { req.getMethod(), req.getContentType(), req.getServletPath() }); // allow only POST requests with valid content length if (!REQUEST_METHOD.equals(req.getMethod()) || req.getContentLength() == 0) { // Bad request - return simple error page handleBadRequest("Bad request, only RTMPT supported.", resp); return; } // decide whether or not to enforce request content checks if (enforceContentTypeCheck && !CONTENT_TYPE.equals(req.getContentType())) { handleBadRequest(String.format("Bad request, unsupported content type: %s.", req.getContentType()), resp); return; } // get the uri String uri = req.getRequestURI().trim(); log.debug("URI: {}", uri); // get the path String path = req.getServletPath(); // since the only current difference in the type of request that we are interested in is the 'second' character, we can double // the speed of this entry point by using a switch on the second character. char p = path.charAt(1); switch (p) { case 'o': // OPEN_REQUEST handleOpen(req, resp); break; case 'c': // CLOSE_REQUEST setRequestInfo(req); handleClose(req, resp); requestInfo.remove(); break; case 's': // SEND_REQUEST setRequestInfo(req); handleSend(req, resp); requestInfo.remove(); break; case 'i': // IDLE_REQUEST setRequestInfo(req); handleIdle(req, resp); requestInfo.remove(); break; case 'f': // HTTPIdent request (ident and ident2) //if HTTPIdent is requested send back some Red5 info //http://livedocs.adobe.com/flashmediaserver/3.0/docs/help.html?content=08_xmlref_011.html String ident = "<fcs><Company>Red5</Company><Team>Red5 Server</Team></fcs>"; // handle ident2 slightly different to appease osx clients if (uri.charAt(uri.length() - 1) == '2') { // check for pre-configured ident2 value if (ident2 != null) { ident = ident2; } else { // just send 404 back if no ident2 value is set resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.setHeader("Connection", "Keep-Alive"); resp.setHeader("Cache-Control", "no-cache"); resp.flushBuffer(); break; } } resp.setStatus(HttpServletResponse.SC_OK); resp.setHeader("Connection", "Keep-Alive"); resp.setHeader("Cache-Control", "no-cache"); resp.setContentType(CONTENT_TYPE); resp.setContentLength(ident.length()); resp.getWriter().write(ident); resp.flushBuffer(); break; default: handleBadRequest(String.format("RTMPT command %s is not supported.", path), resp); } // clear thread local reference Red5.setConnectionLocal(null); } /** {@inheritDoc} */ @Override public void destroy() { if (rtmpConnManager != null) { // Cleanup connections Collection<RTMPConnection> conns = rtmpConnManager.removeConnections(); for (RTMPConnection conn : conns) { if (conn instanceof RTMPTConnection) { log.debug("Connection scope on destroy: {}", conn.getScope()); if (conn.isConnected() && !((RTMPTConnection) conn).isClosing()) { conn.close(); } } } } super.destroy(); } /** * A connection has been closed that was created by this servlet. * * @param conn */ protected void notifyClosed(RTMPTConnection conn) { rtmpConnManager.removeConnection(conn.getId()); } /** * Returns a connection based on the current client session id. * * @return RTMPTConnection */ protected RTMPTConnection getConnection() { String sessionId = requestInfo.get().getSessionId(); RTMPTConnection conn = (RTMPTConnection) rtmpConnManager.getConnectionBySessionId(sessionId); if (conn != null) { // clear thread local reference Red5.setConnectionLocal(conn); } else { log.warn("Null connection for session id: {}", sessionId); } return conn; } /** * Creates an RTMPT connection. * * @return RTMPT connection */ protected RTMPTConnection createConnection() { RTMPTConnection conn = (RTMPTConnection) rtmpConnManager.createConnection(RTMPTConnection.class); if (conn != null) { conn.setHandler(handler); conn.setDecoder(handler.getCodecFactory().getRTMPDecoder()); conn.setEncoder(handler.getCodecFactory().getRTMPEncoder()); handler.connectionOpened(conn, conn.getState()); // set thread local reference Red5.setConnectionLocal(conn); } return conn; } protected void removeConnection(String sessionId) { RTMPTConnection conn = (RTMPTConnection) rtmpConnManager.getConnectionBySessionId(sessionId); if (conn != null) { rtmpConnManager.removeConnection(conn.getId()); } else { log.warn("Remove failed, null connection for session id: {}", sessionId); } } public void setRtmpConnManager(IRTMPConnManager rtmpConnManager) { RTMPTServlet.rtmpConnManager = rtmpConnManager; } /** * Set the RTMPTHandler to use in this servlet. * * @param handler handler */ public void setHandler(RTMPTHandler handler) { RTMPTServlet.handler = handler; } /** * Set the fcs/ident2 string * * @param ident2 */ public void setIdent2(String ident2) { RTMPTServlet.ident2 = ident2; } /** * Sets the target size for responses * * @param targetResponseSize the targetResponseSize to set */ public void setTargetResponseSize(int targetResponseSize) { RTMPTServlet.targetResponseSize = targetResponseSize; } /** * @return the enforceContentTypeCheck */ public boolean isEnforceContentTypeCheck() { return enforceContentTypeCheck; } /** * @param enforceContentTypeCheck the enforceContentTypeCheck to set */ public void setEnforceContentTypeCheck(boolean enforceContentTypeCheck) { this.enforceContentTypeCheck = enforceContentTypeCheck; } /** * Used to store request information per thread. */ protected final class RequestInfo { private String sessionId; private Integer requestNumber; RequestInfo(String sessionId, Integer requestNumber) { this.sessionId = sessionId; this.requestNumber = requestNumber; } /** * @return the sessionId */ public String getSessionId() { return sessionId; } /** * @return the requestNumber */ public Integer getRequestNumber() { return requestNumber; } } }