package org.red5.server.plugin.auth;
/*
* RED5 Open Source Flash Server - http://www.osflash.org/red5
*
* Copyright (c) 2006-2009 by respective authors (see below). All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any later
* version.
*
* This library 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. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with this library; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
import java.security.Security;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.adapter.ApplicationLifecycle;
import org.red5.server.api.IConnection;
import org.red5.server.exception.ClientRejectedException;
import org.red5.server.net.rtmp.status.StatusCodes;
import org.red5.server.net.rtmp.status.StatusObject;
import org.slf4j.Logger;
/**
* Provides FMS-style authentication using an application listener.
*
* @author Paul Gregoire
* @author Dan Rossi
* @author Gavriloaie Eugen-Andrei
*/
public class FMSAuthenticationHandler extends ApplicationLifecycle {
private static Logger log = Red5LoggerFactory.getLogger(FMSAuthenticationHandler.class, "plugins");
private static StatusObject rejectMissingAuth = new StatusObject(StatusCodes.NC_CONNECT_REJECTED,
StatusObject.ERROR, "[ code=403 .need auth; authmod=adobe ]");
private static StatusObject invalidAuthMod = new StatusObject(StatusCodes.NC_CONNECT_REJECTED, StatusObject.ERROR,
"[ AccessManager.Reject ] : [ authmod=adobe ] : ?reason=invalid_authmod&opaque=-");
private static StatusObject noSuchUser = new StatusObject(StatusCodes.NC_CONNECT_REJECTED, StatusObject.ERROR,
"[ AccessManager.Reject ] : [ authmod=adobe ] : ?reason=nosuchuser&opaque=sTQAAA=");
/*
private static StatusObject invalidSessionId = new StatusObject(StatusCodes.NC_CONNECT_REJECTED,
StatusObject.ERROR, "[ AccessManager.Reject ] : [ authmod=adobe ] : ?reason=invalid_session_id&opaque=-");
*/
//test password - testing only - user passwords should be looked up in a real implementation
private static final String password = "test";
private static ConcurrentMap<String, AuthSession> sessions = new ConcurrentHashMap<String, AuthSession>();
static {
//get security provider
Security.addProvider(new BouncyCastleProvider());
}
public boolean appConnect(IConnection conn, Object[] params) {
log.info("appConnect");
boolean result = false;
log.debug("Connection: {}", conn);
log.debug("Params: {}", params);
StatusObject status = null;
Map<String, Object> connectionParams = conn.getConnectParams();
log.debug("Connection params: {}", connectionParams);
if (!connectionParams.containsKey("queryString")) {
//set as missing auth notification
status = rejectMissingAuth;
} else {
//get the raw query string
String rawQueryString = (String) connectionParams.get("queryString");
try {
//parse into a usable query string
UrlQueryStringMap<String, String> queryString = UrlQueryStringMap.parse(rawQueryString);
//get the values we want
String user = queryString.get("user");
log.debug("User: {}", user);
String authmod = queryString.get("authmod");
log.debug("Authmod: {}", authmod);
//make sure they requested adobe auth
if ("adobe".equals(authmod)) {
String response = queryString.get("response");
log.debug("Response: {}", response);
//no response yet, send salt etc.
if (response != null) {
//lookup session and remove at the same time
AuthSession session = sessions.remove(user);
//verify response
if (session != null) {
//1. construct the first part
String str1 = user + session.salt + password;
log.trace("Part 1: {}", str1);
//2. md5 and base64 encode
String hash1 = calculateMD5(str1);
log.trace("Hash 1: {}", hash1);
//3. construct second part using challenge from client
//String str2 = hash1 + session.challenge + queryString.get("challenge");
String str2 = hash1 + queryString.get("opaque") + queryString.get("challenge");
log.trace("Part 2: {}", str2);
//4. md5 and base64 encode
String hash2 = calculateMD5(str2);
log.trace("Hash 2: {}", hash2);
//5. compare response with hash2
if (hash2.equals(response)) {
log.debug("Response is valid");
//return success
result = true;
} else {
log.info("Response {} did not match hash {}", response, hash2);
}
} else {
status = noSuchUser;
}
} else {
//create auth session
AuthSession session = new AuthSession();
sessions.put(user, session);
//set as rejected
status = new StatusObject(StatusCodes.NC_CONNECT_REJECTED, StatusObject.ERROR,
String.format("[ AccessManager.Reject ] : [ authmod=adobe ] : ?reason=needauth&user=%s&salt=%s&challenge=%s&opaque=%s", user, session.salt, session.challenge, session.opaque));
}
} else {
status = invalidAuthMod;
}
} catch (Exception e) {
log.error("Error authenticating", e);
}
}
//status.setAdditional("secureToken", "testing secure token status property from RED5 !!!");
//send the status object
log.debug("Status: {}", status);
if (!result) {
throw new ClientRejectedException(status);
}
return result;
}
/**
* Generate an MD5 hash and return encoded with Base64.
*
* @param input
* @return
*/
private String calculateMD5(String input) {
String result = null;
MD5Digest md5;
try {
md5 = new MD5Digest();
byte[] output = null;
try {
byte[] buf = input.getBytes();
md5.update(buf, 0, buf.length);
output = new byte[md5.getDigestSize()];
md5.doFinal(output, 0);
} catch (Exception e) {
log.error("State error", e);
}
//encode in b64 and strip any cr/lf
byte[] res = Base64.encodeBase64(output);
result = new String(res).replaceAll("(\r\n|\r|\n|\n\r)", "");
//result = Base64.encodeBase64String(output).replaceAll("(\r\n|\r|\n|\n\r)", "");
} catch (SecurityException e) {
log.error("Security exception when getting MD5", e);
} catch (Exception e) {
log.error("Error using MD5", e);
}
return result;
}
private final class AuthSession {
public String salt;
public String challenge;
public String opaque;
@SuppressWarnings("unused")
public long created = System.currentTimeMillis();
{
salt = calculateMD5("red5rox");
challenge = calculateMD5("red5");
//these are equal for now
opaque = challenge;
}
}
}