/*
* 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.net.URL;
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.apache.mina.core.session.DummySession;
import org.apache.mina.core.session.IoSession;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.Red5;
import org.red5.server.net.protocol.ProtocolState;
import org.red5.server.net.rtmp.IRTMPConnManager;
import org.red5.server.net.rtmp.RTMPConnection;
import org.red5.server.net.rtmp.codec.RTMP;
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;
/**
* Web app context
*/
protected transient WebApplicationContext appCtx;
/**
* 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;
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 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.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.debug("returnMessage {}", buffer);
resp.setStatus(HttpServletResponse.SC_OK);
resp.setHeader("Connection", "Keep-Alive");
resp.setHeader("Cache-Control", "no-cache");
resp.setContentType(CONTENT_TYPE);
log.debug("Sending {} bytes", buffer.limit());
resp.setContentLength(buffer.limit() + 1);
ServletOutputStream output = resp.getOutputStream();
output.write(client.getPollingDelay());
ServletUtils.copy(buffer.asInputStream(), output);
buffer.free();
buffer = null;
}
/**
* Return the client id from a url like /send/123456/12 -> 123456
*
* @param req Servlet request
* @return Client id
*/
protected Integer getClientId(HttpServletRequest req) {
String uri = req.getRequestURL().toString();
URL url = null;
try {
url = new URL(uri);
} catch (Exception e) {
log.warn("getclientId: error parsing url: {}", uri);
return null;
}
// get path
String path = url.getPath();
if (path.equals("")) {
log.error("getClientId: path is empty");
return null;
}
// trim off end
int pos = path.lastIndexOf('/');
path = path.substring(0, pos);
// trim off beginning
pos = path.lastIndexOf('/');
if (pos != -1) {
path = path.substring(pos + 1);
}
try {
return Integer.valueOf(path);
} catch (Exception e) {
log.error("getClientId: parse error", e);
return null;
}
}
/**
* Get the RTMPT client for a session.
*
* @param req
* Servlet request
* @return RTMP client connection
*/
protected RTMPTConnection getClientConnection(HttpServletRequest req) {
final Integer id = getClientId(req);
return getConnection(id);
}
/**
* Skip data sent by the client.
*
* @param req
* Servlet request
* @throws IOException
* I/O exception
*/
protected void skipData(HttpServletRequest req) throws IOException {
log.debug("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
* @throws IOException
* I/O exception
*/
protected void returnPendingMessages(RTMPTConnection client, HttpServletResponse resp) throws IOException {
log.debug("returnPendingMessages {}", client);
IoBuffer data = client.getPendingMessages(targetResponseSize);
if (data != null) {
returnMessage(client, data, resp);
} else {
// no more messages to send...
if (client.isClosing()) {
// tell client to close connection
returnMessage((byte) 0, resp);
} else {
returnMessage(client.getPollingDelay(), resp);
}
}
}
/**
* Start a new RTMPT session.
*
* @param req
* Servlet request
* @param resp
* Servlet response
* @throws ServletException
* Servlet exception
* @throws IOException
* I/O exception
*/
protected void handleOpen(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.debug("handleOpen");
// Skip sent data
skipData(req);
// TODO: should we evaluate the pathinfo?
RTMPTConnection connection = createConnection();
connection.setServlet(this);
connection.setServletRequest(req);
if (connection.getId() != 0) {
// Return connection id to client
returnMessage(connection.getId() + "\n", resp);
} else {
// no more clients are available for serving
returnMessage((byte) 0, resp);
}
}
/**
* Close a RTMPT session.
*
* @param req
* Servlet request
* @param resp
* Servlet response
* @throws ServletException
* Servlet exception
* @throws IOException
* I/O exception
*/
protected void handleClose(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.debug("handleClose");
// skip sent data
skipData(req);
// get the associated connection
RTMPTConnection connection = getClientConnection(req);
if (connection == null) {
handleBadRequest(String.format("Close: unknown client with id: %s", getClientId(req)), resp);
return;
}
removeConnection(connection.getId());
handler.connectionClosed(connection, connection.getState());
returnMessage((byte) 0, resp);
connection.realClose();
}
/**
* Add data for an established session.
*
* @param req
* Servlet request
* @param resp
* Servlet response
* @throws ServletException
* Servlet exception
* @throws IOException
* I/O exception
*/
protected void handleSend(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.debug("handleSend");
RTMPTConnection connection = getClientConnection(req);
if (connection == null) {
handleBadRequest(String.format("Send: unknown client with id: %s", getClientId(req)), resp);
return;
} else if (connection.getStateCode() == RTMP.STATE_DISCONNECTED) {
removeConnection(connection.getId());
handleBadRequest("Connection already closed", resp);
return;
}
// put the received data in a ByteBuffer
int length = req.getContentLength();
IoBuffer data = IoBuffer.allocate(length);
ServletUtils.copy(req.getInputStream(), data.asOutputStream());
data.flip();
// decode the objects in the data
List<?> messages = connection.decode(data);
data.free();
data = null;
if (messages == null || messages.isEmpty()) {
returnMessage(connection.getPollingDelay(), resp);
return;
}
// execute the received RTMP messages
IoSession session = new DummySession();
session.setAttribute(RTMPConnection.RTMP_CONNECTION_KEY, connection);
session.setAttribute(ProtocolState.SESSION_KEY, connection.getState());
for (Object message : messages) {
try {
handler.messageReceived(message, session);
} catch (Exception e) {
log.error("Could not process message", e);
}
}
// send results to client
returnPendingMessages(connection, resp);
}
/**
* Poll RTMPT session for updates.
*
* @param req
* Servlet request
* @param resp
* Servlet response
* @throws ServletException
* Servlet exception
* @throws IOException
* I/O exception
*/
protected void handleIdle(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
log.debug("handleIdle");
// skip sent data
skipData(req);
// get associated connection
RTMPTConnection connection = getClientConnection(req);
if (connection == null) {
handleBadRequest("Idle: unknown client with id: " + getClientId(req), resp);
return;
} else if (connection.isClosing()) {
// tell client to close the connection
returnMessage((byte) 0, resp);
connection.realClose();
return;
} else if (connection.getStateCode() == RTMP.STATE_DISCONNECTED) {
removeConnection(connection.getId());
handleBadRequest("Connection already closed", resp);
return;
}
returnPendingMessages(connection, resp);
}
/**
* Main entry point for the servlet.
*
* @param req
* Request object
* @param resp
* Response object
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, 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 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
handleClose(req, resp);
break;
case 's': // SEND_REQUEST
handleSend(req, resp);
break;
case 'i': // IDLE_REQUEST
handleIdle(req, resp);
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
String uri = req.getRequestURI().trim();
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 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);
}
}
/** {@inheritDoc} */
@Override
public void destroy() {
if (rtmpConnManager != null) {
// Cleanup connections
Collection<RTMPConnection> conns = rtmpConnManager.removeConnections();
for (RTMPConnection conn : conns) {
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());
}
protected RTMPTConnection getConnection(int clientId) {
RTMPTConnection conn = (RTMPTConnection) rtmpConnManager.getConnection(clientId);
if (conn != null) {
// clear thread local reference
Red5.setConnectionLocal(conn);
} else {
log.warn("Null connection for clientId: {}", clientId);
}
return conn;
}
protected RTMPTConnection createConnection() {
RTMPTConnection conn = (RTMPTConnection) rtmpConnManager.createConnection(RTMPTConnection.class);
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(int clientId) {
rtmpConnManager.removeConnection(clientId);
}
/**
* @return the enforceContentTypeCheck
*/
public boolean isEnforceContentTypeCheck() {
return enforceContentTypeCheck;
}
/**
* @param enforceContentTypeCheck the enforceContentTypeCheck to set
*/
public void setEnforceContentTypeCheck(boolean enforceContentTypeCheck) {
this.enforceContentTypeCheck = enforceContentTypeCheck;
}
}