Live555源码分析[2]:RTSPServer中的用户认证

http://blog.csdn.net/njzhujinhua @20140601

说到鉴权,这是我多年来工作中的一部分,但这里rtsp中的认证简单多了,只是最基本的digest鉴权的策略。

在Live555的实现中, 用户信息由如下类维护,其提供增删查的接口。realm默认值为"LIVE555 Streaming Media"

class UserAuthenticationDatabase {
public:
  UserAuthenticationDatabase(char const* realm = NULL,
			     Boolean passwordsAreMD5 = False);
    // If "passwordsAreMD5" is True, then each password stored into, or removed from,
    // the database is actually the value computed
    // by md5(<username>:<realm>:<actual-password>)
  virtual ~UserAuthenticationDatabase();
  virtual void addUserRecord(char const* username, char const* password);
  virtual void removeUserRecord(char const* username);

  virtual char const* lookupPassword(char const* username);
      // returns NULL if the user name was not present

  char const* realm() { return fRealm; }
  Boolean passwordsAreMD5() { return fPasswordsAreMD5; }

protected:
  HashTable* fTable;
  char* fRealm;
  Boolean fPasswordsAreMD5;
};

一个鉴权过程的例子如下:

[1]C-->S

OPTIONS rtsp://10.0.0.10:8554/h264ESVideoTest RTSP/1.0

CSeq: 2

User-Agent: LibVLC/2.1.3 (LIVE555 Streaming Media v2014.01.21)

[2]S-->C

RTSP/1.0 200 OK

CSeq: 2

Date: Sat, May 31 2014 14:16:42 GMT

Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER

[3]C-->S

DESCRIBE rtsp://10.0.0.10:8554/h264ESVideoTest RTSP/1.0

CSeq: 3

User-Agent: LibVLC/2.1.3 (LIVE555 Streaming Media v2014.01.21)

Accept: application/sdp

[4]S-->C

RTSP/1.0 401 Unauthorized

CSeq: 3

Date: Sat, May 31 2014 14:16:43 GMT

WWW-Authenticate: Digest realm="LIVE555 Streaming Media", nonce="73724068291777415fec38a1593568e5"

[5]C-->S

DESCRIBE rtsp://10.0.0.10:8554/h264ESVideoTest RTSP/1.0

CSeq: 4

Authorization: Digest username="zjh", realm="LIVE555 Streaming Media", nonce="73724068291777415fec38a1593568e5", uri="rtsp://10.0.0.10:8554/h264ESVideoTest", response="b8c755d897abddd0206954bab0e0b763"

User-Agent: LibVLC/2.1.3 (LIVE555 Streaming Media v2014.01.21)

Accept: application/sdp

[6]S

lookupPassword(zjh) returned password 123

鉴权通过,生成SDP信息

RTSP的鉴权发生在DESCRIBE命令之时,在收到DESCRIBE命令时,如果不需要处理鉴权,则直接就走到第6步,生成SDP信息,发送RTSP/1.0 200 OK及SDP信息并等待下一步的setup命令了。

如果需要鉴权则检查是否对本连接生成过nonce随机数,即是否已挑战过。如果没有则发送RTSP/1.0 401 Unauthorized,同时发送的内容中包含server指定的realm以及产生的随机数nonce。WWW-Authenticate: Digest realm="LIVE555 Streaming Media", nonce="73724068291777415fec38a1593568e5"

代码参见

Boolean RTSPServer::RTSPClientConnection::authenticationOK(char const* cmdName, char const* urlSuffix, char const* fullRequestStr)
{
    if (!fOurServer.specialClientAccessCheck(fClientInputSocket, fClientAddr, urlSuffix))
    {
        setRTSPResponse("401 Unauthorized");
        return False;
    }

    // If we weren't set up with an authentication database, we're OK:
    <span style="color:#3333ff;">UserAuthenticationDatabase* authDB = fOurServer.getAuthenticationDatabaseForCommand(cmdName);</span>
    if (authDB == NULL) return True;

    char const* username = NULL; char const* realm = NULL; char const* nonce = NULL;
    char const* uri = NULL; char const* response = NULL;
    Boolean success = False;

    do {
        <span style="color:#3333ff;">// To authenticate, we first need to have a nonce set up
        // from a previous attempt:
        if (fCurrentAuthenticator.nonce() == NULL) </span><span style="color:#ff0000;"><strong>break;</strong></span><span style="color:#3333ff;">

        // Next, the request needs to contain an "Authorization:" header,
        // containing a username, (our) realm, (our) nonce, uri,
        // and response string:</span>
        <span style="color:#3333ff;">if (!parseAuthorizationHeader(fullRequestStr,
            username, realm, nonce, uri, response)
            || username == NULL
            || realm == NULL || strcmp(realm, fCurrentAuthenticator.realm()) != 0
            || nonce == NULL || strcmp(nonce, fCurrentAuthenticator.nonce()) != 0
            || uri == NULL || response == NULL)
        {
            break;
        }

        // Next, the username has to be known to us:
        char const* password = authDB->lookupPassword(username);
#ifdef DEBUG
        fprintf(stderr, "lookupPassword(%s) returned password %s\n", username, password);
#endif
        if (password == NULL) break;
        fCurrentAuthenticator.setUsernameAndPassword(username, password, authDB->passwordsAreMD5());

        // Finally, compute a digest response from the information that we have,
        // and compare it to the one that we were given:
        char const* ourResponse
            = fCurrentAuthenticator.computeDigestResponse(cmdName, uri);
        success = (strcmp(ourResponse, response) == 0);
        fCurrentAuthenticator.reclaimDigestResponse(ourResponse);
    } while (0);

    delete[] (char*)realm; delete[] (char*)nonce;
    delete[] (char*)uri; delete[] (char*)response;

    if (success)
    {
        // The user has been authenticated.
        // Now allow subclasses a chance to validate the user against the IP address and/or URL suffix.
        if (!fOurServer.specialClientUserAccessCheck(fClientInputSocket, fClientAddr, urlSuffix, username))
        {
            // Note: We don't return a "WWW-Authenticate" header here, because the user is valid,
            // even though the server has decided that they should not have access.
            setRTSPResponse("401 Unauthorized");
            delete[] (char*)username;
            return False;
        }
    }
    delete[] (char*)username;
    if (success) return True;</span>

    <span style="color:#cc0000;">// If we get here, we failed to authenticate the user.
    // Send back a "401 Unauthorized" response, with a new random nonce:</span>
    <span style="color:#ff0000;">fCurrentAuthenticator.setRealmAndRandomNonce(authDB->realm());
    snprintf((char*)fResponseBuffer, sizeof fResponseBuffer,
        "RTSP/1.0 401 Unauthorized\r\n"
        "CSeq: %s\r\n"
        "%s"
        "WWW-Authenticate: Digest realm=\"%s\", nonce=\"%s\"\r\n\r\n",
        fCurrentCSeq,
        dateHeader(),
        fCurrentAuthenticator.realm(), fCurrentAuthenticator.nonce());
    return False;</span>
}

其中第一次收到describe命令调用的如上函数走的有效代码如上述红色所述,其它无关紧要。在第二次收到describe的时候因以生成过挑战nonce,则走的流程如蓝色代码所示。

上述函数中用到的fCurrentAuthenticator定义如下

class RTSPServer
{
    class RTSPClientConnection
    {
        ...
        protected:
            ...
            Authenticator fCurrentAuthenticator; // used if access control is needed
            ...
    };
};

Authenticator则是一个用于digest鉴权的类,定义如下,

// A class used for digest authentication.
// The "realm", and "nonce" fields are supplied by the server
// (in a "401 Unauthorized" response).
// The "username" and "password" fields are supplied by the client.
class Authenticator {
public:
  Authenticator();
  Authenticator(char const* username, char const* password, Boolean passwordIsMD5 = False);
      // If "passwordIsMD5" is True, then "password" is actually the value computed
      // by md5(<username>:<realm>:<actual-password>)
  Authenticator(const Authenticator& orig);
  Authenticator& operator=(const Authenticator& rightSide);
  virtual ~Authenticator();

  void reset();
  void setRealmAndNonce(char const* realm, char const* nonce);
  void setRealmAndRandomNonce(char const* realm);
      // as above, except that the nonce is created randomly.
      // (This is used by servers.)
  void setUsernameAndPassword(char const* username, char const* password, Boolean passwordIsMD5 = False);
      // If "passwordIsMD5" is True, then "password" is actually the value computed
      // by md5(<username>:<realm>:<actual-password>)

  char const* realm() const { return fRealm; }
  char const* nonce() const { return fNonce; }
  char const* username() const { return fUsername; }
  char const* password() const { return fPassword; }

  char const* computeDigestResponse(char const* cmd, char const* url) const;
      // The returned string from this function must later be freed by calling:
  void reclaimDigestResponse(char const* responseStr) const;

private:
  void resetRealmAndNonce();
  void resetUsernameAndPassword();
  void assignRealmAndNonce(char const* realm, char const* nonce);
  void assignUsernameAndPassword(char const* username, char const* password, Boolean passwordIsMD5);
  void assign(char const* realm, char const* nonce,
	      char const* username, char const* password, Boolean passwordIsMD5);

private:
  char* fRealm; char* fNonce;
  char* fUsername; char* fPassword;
  Boolean fPasswordIsMD5;
};

在fCurrentAuthenticator.nonce()不为空即已发送过挑战后收到describe后的鉴权过程如下

1:首先parseAuthorizationHeader得到username, realm, nonce, uri以及response。这五项均不能为空,否则鉴权失败。

其中username为用户名,realm为域名,必须与server上一步发送的完全一致,nonce为server上一步发送的随机challenge,必须完全一致。uri为要访问的资源标识,response则是client通过digest计算的响应。

2:获取用户名及密码,用户名不存在则鉴权失败

        char const* password = authDB->lookupPassword(username);
        if (password == NULL) break;
        fCurrentAuthenticator.setUsernameAndPassword(username, password, authDB->passwordsAreMD5());

3:根据server知道的信息同样计算response并与收到的response比较

        // Finally, compute a digest response from the information that we have,
        // and compare it to the one that we were given:
        char const* ourResponse
            = fCurrentAuthenticator.computeDigestResponse(cmdName, uri);
        success = (strcmp(ourResponse, response) == 0);
        fCurrentAuthenticator.reclaimDigestResponse(ourResponse);

关于计算ourResponse的函数computeDigestResponse实现如下

char const* Authenticator::computeDigestResponse(char const* cmd, char const* url) const
{
  // The "response" field is computed as:
  //    md5(md5(<username>:<realm>:<password>):<nonce>:md5(<cmd>:<url>))
  // or, if "fPasswordIsMD5" is True:
  //    md5(<password>:<nonce>:md5(<cmd>:<url>))
  char ha1Buf[33];
  if (fPasswordIsMD5) {
    strncpy(ha1Buf, password(), 32);
    ha1Buf[32] = '\0'; // just in case
  } else {
    unsigned const ha1DataLen = strlen(username()) + 1
      + strlen(realm()) + 1 + strlen(password());
    unsigned char* ha1Data = new unsigned char[ha1DataLen+1];
    sprintf((char*)ha1Data, "%s:%s:%s", username(), realm(), password());
    our_MD5Data(ha1Data, ha1DataLen, ha1Buf);
    delete[] ha1Data;
  }

  unsigned const ha2DataLen = strlen(cmd) + 1 + strlen(url);
  unsigned char* ha2Data = new unsigned char[ha2DataLen+1];
  sprintf((char*)ha2Data, "%s:%s", cmd, url);
  char ha2Buf[33];
  our_MD5Data(ha2Data, ha2DataLen, ha2Buf);
  delete[] ha2Data;

  unsigned const digestDataLen
    = 32 + 1 + strlen(nonce()) + 1 + 32;
  unsigned char* digestData = new unsigned char[digestDataLen+1];
  sprintf((char*)digestData, "%s:%s:%s",
          ha1Buf, nonce(), ha2Buf);
  char const* result = our_MD5Data(digestData, digestDataLen, NULL);
  delete[] digestData;
  return result;
}

本函数实现及注释十分清晰,几乎看文字描述一样了。

注释说的很明白,如果password是MD5格式,则其已经是username:realm:plainPwd的md5摘要。我们将response表示为md5(A:B:C)

则A=md5格式password 或 md5(<username>:<realm>:<明文password>)

B=nonce

C=md5(<cmd>:<uri>)

如上代码中即分别为ha1Buf,nonce(),和ha2Buf()的拼接过程了。

这里的数据全是字符串。如ha1Data="zjh:LIVE555 Streaming Media:123"  ha1Buf=md5(ha1Data)="ad68dbfd3e130bcabd2e61d19e5695fd"

ha2Data="DESCRIBE:rtsp://10.0.0.10:8554/h264ESVideoTest"   ha2Buf="1d47c98b00946762aad35c10a7e61736"

digestData则为"ad68dbfd3e130bcabd2e61d19e5695fd:11b4aa0d77bb6ae84377474049dae9a1:1d47c98b00946762aad35c10a7e61736"

在访问同一资源uri的多次情况下,实际变动的只有中间的nonce部分。

计算之后只需将此response与client在describe命令中的response比较即可。

Live555源码分析[2]:RTSPServer中的用户认证,布布扣,bubuko.com

时间: 2024-12-15 01:44:52

Live555源码分析[2]:RTSPServer中的用户认证的相关文章

lodash源码分析之compact中的遍历

小时候, 乡愁是一枚小小的邮票, 我在这头, 母亲在那头. 长大后,乡愁是一张窄窄的船票, 我在这头, 新娘在那头. 后来啊, 乡愁是一方矮矮的坟墓, 我在外头, 母亲在里头. 而现在, 乡愁是一湾浅浅的海峡, 我在这头, 大陆在那头. --余光中<乡愁> 本文为读 lodash 源码的第三篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodash gitbook也会同步仓库的更新,gitbook地址:pocket-lodash 作用与用法 compact 函数用来去除数组中的

Spring Boot(3):加载DataSource过程的源码分析及yml中DataSource的配置

Spring Boot实现了自动加载DataSource及相关配置.当然,使用时加上@EnableAutoConfiguration注解是必须的.下面就是对这一部分的源码分析. (1)Spring Boot启动后会调用org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.下面是部分源码. 1 @Configuration 2 @ConditionalOnClass({ DataSource.class, E

String源码分析之Java中的String为什么是不可变的以及replace方法源码分析

什么是不可变对象? 众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的.不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变. 区分对象和对象的引用 对于Java初学者, 对于String是不可变对象总是存有疑惑.看下面代码: String s = "ABCabc";

【转】【java源码分析】Map中的hash算法分析

全网把Map中的hash()分析的最透彻的文章,别无二家. 2018年05月09日 09:08:08 阅读数:957 你知道HashMap中hash方法的具体实现吗?你知道HashTable.ConcurrentHashMap中hash方法的实现以及原因吗?你知道为什么要这么实现吗?你知道为什么JDK 7和JDK 8中hash方法实现的不同以及区别吗?如果你不能很好的回答这些问题,那么你需要好好看看这篇文章.文中涉及到大量代码和计算机底层原理知识.绝对的干货满满.整个互联网,把hash()分析的

【Flume】【源码分析】flume中sink到hdfs,文件系统频繁产生文件,文件滚动配置不起作用? ERROR hdfs.BucketWriter: Hit max consecutive under-replication rotations (30)

[转载] http://blog.csdn.net/simonchi/article/details/43231891 ERROR hdfs.BucketWriter: Hit max consecutive under-replication rotations (30) 本人在测试hdfs的sink,发现sink端的文件滚动配置项起不到任何作用,配置如下: [plain] view plain copy print? a1.sinks.k1.type=hdfs a1.sinks.k1.cha

MapReduce源码分析之Task中关于对应TaskAttempt存储Map方案的一些思考

我们知道,MapReduce有三层调度模型,即Job-->Task-->TaskAttempt,并且: 1.通常一个Job存在多个Task,这些Task总共有Map Task和Redcue Task两种大的类型(为简化描述,Map-Only作业.JobSetup Task等复杂的情况这里不做考虑): 2.每个Task可以尝试运行1-n此,而且通常很多情况下都是1次,只有当开启了推测执行原理且存在拖后腿Task,或者Task之前执行失败时,Task才执行多次. 而TaskImpl中存在一个成员变

【Flume】【源码分析】flume中事件Event的数据结构分析以及Event分流

前言 首先来看一下flume官网中对Event的定义 一行文本内容会被反序列化成一个event[序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储和传输数据],event的最大定义为2048字节,超过,则会切割,剩下的会被放到下一个event中,默认编码是UTF-8,这都是统一的. 但是这个解释是针对Avro反序列化系统中的Event的定义,而flume ng中很多event用的不是这个,所以你只要记住event的数据

Android 中View的绘制机制源码分析 三

到目前为止,measure过程已经讲解完了,今天开始我们就来学习layout过程,不过在学习layout过程之前,大家有没有发现我换了编辑器,哈哈,终于下定决心从Html编辑器切换为markdown编辑器,这里之所以使用"下定决心"这个词,是因为毕竟Html编辑器使用好几年了,很多习惯都已经养成了,要改变多年的习惯确实不易,相信这也是还有很多人坚持使用Html编辑器的原因.这也反应了一个现象,当人对某一事物非常熟悉时,一旦出现了新的事物想取代老的事物时,人们都有一种抵触的情绪,做技术的

转:Mongodb源码分析之Replication模式

原文出处:http://www.cnblogs.com/daizhj/archive/2011/06/13/mongodb_sourcecode_rep mongodb中提供了复制(Replication)机制,通过该机制可以帮助我们很容易实现读写分离方案,并支持灾难恢复(服务器断电)等意外情况下的数据安全. 在老版本(1.6)中,Mongo提供了两种方式的复制:master-slave及replica pair模式(注:mongodb最新支持的replset复制集方式可看成是pair的升级版,