Live555源码分析RTSPServer中的用户认证

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

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


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

[cpp]  view plain  copy
 
  1. class UserAuthenticationDatabase {  
  2. public:  
  3.   UserAuthenticationDatabase(char const* realm = NULL,  
  4.                  Boolean passwordsAreMD5 = False);  
  5.     // If "passwordsAreMD5" is True, then each password stored into, or removed from,  
  6.     // the database is actually the value computed  
  7.     // by md5(::)  
  8.   virtual ~UserAuthenticationDatabase();  
  9.   virtual void addUserRecord(char const* username, char const* password);  
  10.   virtual void removeUserRecord(char const* username);  
  11.   
  12.   virtual char const* lookupPassword(char const* username);  
  13.       // returns NULL if the user name was not present  
  14.   
  15.   char const* realm() { return fRealm; }  
  16.   Boolean passwordsAreMD5() { return fPasswordsAreMD5; }  
  17.   
  18. protected:  
  19.   HashTable* fTable;  
  20.   char* fRealm;  
  21.   Boolean fPasswordsAreMD5;  
  22. };  

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

[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"

代码参见

[cpp]  view plain  copy
 
  1. Boolean RTSPServer::RTSPClientConnection::authenticationOK(char const* cmdName, char const* urlSuffix, char const* fullRequestStr)  
  2. {  
  3.     if (!fOurServer.specialClientAccessCheck(fClientInputSocket, fClientAddr, urlSuffix))  
  4.     {  
  5.         setRTSPResponse("401 Unauthorized");  
  6.         return False;  
  7.     }  
  8.   
  9.     // If we weren't set up with an authentication database, we're OK:  
  10.     "color:#3333ff;">UserAuthenticationDatabase* authDB = fOurServer.getAuthenticationDatabaseForCommand(cmdName);  
  11.     if (authDB == NULL) return True;  
  12.   
  13.     char const* username = NULL; char const* realm = NULL; char const* nonce = NULL;  
  14.     char const* uri = NULL; char const* response = NULL;  
  15.     Boolean success = False;  
  16.   
  17.     do {  
  18.         "color:#3333ff;">// To authenticate, we first need to have a nonce set up  
  19.         // from a previous attempt:  
  20.         if (fCurrentAuthenticator.nonce() == NULL) "color:#ff0000;">break;"color:#3333ff;">  
  21.   
  22.         // Next, the request needs to contain an "Authorization:" header,  
  23.         // containing a username, (our) realm, (our) nonce, uri,  
  24.         // and response string:  
  25.         "color:#3333ff;">if (!parseAuthorizationHeader(fullRequestStr,  
  26.             username, realm, nonce, uri, response)  
  27.             || username == NULL  
  28.             || realm == NULL || strcmp(realm, fCurrentAuthenticator.realm()) != 0  
  29.             || nonce == NULL || strcmp(nonce, fCurrentAuthenticator.nonce()) != 0  
  30.             || uri == NULL || response == NULL)  
  31.         {  
  32.             break;  
  33.         }  
  34.   
  35.         // Next, the username has to be known to us:  
  36.         char const* password = authDB->lookupPassword(username);  
  37. #ifdef DEBUG  
  38.         fprintf(stderr, "lookupPassword(%s) returned password %s\n", username, password);  
  39. #endif  
  40.         if (password == NULL) break;  
  41.         fCurrentAuthenticator.setUsernameAndPassword(username, password, authDB->passwordsAreMD5());  
  42.   
  43.         // Finally, compute a digest response from the information that we have,  
  44.         // and compare it to the one that we were given:  
  45.         char const* ourResponse  
  46.             = fCurrentAuthenticator.computeDigestResponse(cmdName, uri);  
  47.         success = (strcmp(ourResponse, response) == 0);  
  48.         fCurrentAuthenticator.reclaimDigestResponse(ourResponse);  
  49.     } while (0);  
  50.   
  51.     delete[] (char*)realm; delete[] (char*)nonce;  
  52.     delete[] (char*)uri; delete[] (char*)response;  
  53.   
  54.     if (success)  
  55.     {  
  56.         // The user has been authenticated.  
  57.         // Now allow subclasses a chance to validate the user against the IP address and/or URL suffix.  
  58.         if (!fOurServer.specialClientUserAccessCheck(fClientInputSocket, fClientAddr, urlSuffix, username))  
  59.         {  
  60.             // Note: We don't return a "WWW-Authenticate" header here, because the user is valid,  
  61.             // even though the server has decided that they should not have access.  
  62.             setRTSPResponse("401 Unauthorized");  
  63.             delete[] (char*)username;  
  64.             return False;  
  65.         }  
  66.     }  
  67.     delete[] (char*)username;  
  68.     if (success) return True;  
  69.   
  70.     "color:#cc0000;">// If we get here, we failed to authenticate the user.  
  71.     // Send back a "401 Unauthorized" response, with a new random nonce:  
  72.     "color:#ff0000;">fCurrentAuthenticator.setRealmAndRandomNonce(authDB->realm());  
  73.     snprintf((char*)fResponseBuffer, sizeof fResponseBuffer,  
  74.         "RTSP/1.0 401 Unauthorized\r\n"  
  75.         "CSeq: %s\r\n"  
  76.         "%s"  
  77.         "WWW-Authenticate: Digest realm=\"%s\", nonce=\"%s\"\r\n\r\n",  
  78.         fCurrentCSeq,  
  79.         dateHeader(),  
  80.         fCurrentAuthenticator.realm(), fCurrentAuthenticator.nonce());  
  81.     return False;  
  82. }  

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


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

[cpp]  view plain  copy
 
  1. class RTSPServer  
  2. {  
  3.     class RTSPClientConnection  
  4.     {  
  5.         ...  
  6.         protected:  
  7.             ...  
  8.             Authenticator fCurrentAuthenticator; // used if access control is needed  
  9.             ...  
  10.     };  
  11. };  


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

[cpp]  view plain  copy
 
  1. // A class used for digest authentication.  
  2. // The "realm", and "nonce" fields are supplied by the server  
  3. // (in a "401 Unauthorized" response).  
  4. // The "username" and "password" fields are supplied by the client.  
  5. class Authenticator {  
  6. public:  
  7.   Authenticator();  
  8.   Authenticator(char const* username, char const* password, Boolean passwordIsMD5 = False);  
  9.       // If "passwordIsMD5" is True, then "password" is actually the value computed  
  10.       // by md5(::)  
  11.   Authenticator(const Authenticator& orig);  
  12.   Authenticator& operator=(const Authenticator& rightSide);  
  13.   virtual ~Authenticator();  
  14.   
  15.   void reset();  
  16.   void setRealmAndNonce(char const* realm, char const* nonce);  
  17.   void setRealmAndRandomNonce(char const* realm);  
  18.       // as above, except that the nonce is created randomly.  
  19.       // (This is used by servers.)  
  20.   void setUsernameAndPassword(char const* username, char const* password, Boolean passwordIsMD5 = False);  
  21.       // If "passwordIsMD5" is True, then "password" is actually the value computed  
  22.       // by md5(::)  
  23.   
  24.   char const* realm() const { return fRealm; }  
  25.   char const* nonce() const { return fNonce; }  
  26.   char const* username() const { return fUsername; }  
  27.   char const* password() const { return fPassword; }  
  28.   
  29.   char const* computeDigestResponse(char const* cmd, char const* url) const;  
  30.       // The returned string from this function must later be freed by calling:  
  31.   void reclaimDigestResponse(char const* responseStr) const;  
  32.   
  33. private:  
  34.   void resetRealmAndNonce();  
  35.   void resetUsernameAndPassword();  
  36.   void assignRealmAndNonce(char const* realm, char const* nonce);  
  37.   void assignUsernameAndPassword(char const* username, char const* password, Boolean passwordIsMD5);  
  38.   void assign(char const* realm, char const* nonce,  
  39.           char const* username, char const* password, Boolean passwordIsMD5);  
  40.   
  41. private:  
  42.   char* fRealm; char* fNonce;  
  43.   char* fUsername; char* fPassword;  
  44.   Boolean fPasswordIsMD5;  
  45. };  

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

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

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

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

[cpp]  view plain  copy
 
  1. char const* password = authDB->lookupPassword(username);  
  2. if (password == NULL) break;  
  3. fCurrentAuthenticator.setUsernameAndPassword(username, password, authDB->passwordsAreMD5());  

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

[cpp]  view plain  copy
 
  1. // Finally, compute a digest response from the information that we have,  
  2. // and compare it to the one that we were given:  
  3. char const* ourResponse  
  4.     = fCurrentAuthenticator.computeDigestResponse(cmdName, uri);  
  5. success = (strcmp(ourResponse, response) == 0);  
  6. fCurrentAuthenticator.reclaimDigestResponse(ourResponse);  

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

[cpp]  view plain  copy
 
  1. char const* Authenticator::computeDigestResponse(char const* cmd, char const* url) const  
  2. {  
  3.   // The "response" field is computed as:  
  4.   //    md5(md5(::)::md5(:))  
  5.   // or, if "fPasswordIsMD5" is True:  
  6.   //    md5(::md5(:))       
  7.   char ha1Buf[33];  
  8.   if (fPasswordIsMD5) {  
  9.     strncpy(ha1Buf, password(), 32);  
  10.     ha1Buf[32] = '\0'// just in case  
  11.   } else {  
  12.     unsigned const ha1DataLen = strlen(username()) + 1  
  13.       + strlen(realm()) + 1 + strlen(password());  
  14.     unsigned char* ha1Data = new unsigned char[ha1DataLen+1];  
  15.     sprintf((char*)ha1Data, "%s:%s:%s", username(), realm(), password());  
  16.     our_MD5Data(ha1Data, ha1DataLen, ha1Buf);  
  17.     delete[] ha1Data;  
  18.   }  
  19.   
  20.   unsigned const ha2DataLen = strlen(cmd) + 1 + strlen(url);  
  21.   unsigned char* ha2Data = new unsigned char[ha2DataLen+1];  
  22.   sprintf((char*)ha2Data, "%s:%s", cmd, url);  
  23.   char ha2Buf[33];  
  24.   our_MD5Data(ha2Data, ha2DataLen, ha2Buf);  
  25.   delete[] ha2Data;  
  26.   
  27.   unsigned const digestDataLen  
  28.     = 32 + 1 + strlen(nonce()) + 1 + 32;  
  29.   unsigned char* digestData = new unsigned char[digestDataLen+1];  
  30.   sprintf((char*)digestData, "%s:%s:%s",  
  31.           ha1Buf, nonce(), ha2Buf);  
  32.   char const* result = our_MD5Data(digestData, digestDataLen, NULL);  
  33.   delete[] digestData;  
  34.   return result;  
  35. }  

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

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

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

B=nonce

C=md5(:)

如上代码中即分别为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:73724068291777415fec38a1593568e5:1d47c98b00946762aad35c10a7e61736"

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

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

你可能感兴趣的:(Live555)