Skip to content

Commit

Permalink
Add AirPlay password authentication support.
Browse files Browse the repository at this point in the history
Thanks to mythtv@cjo20.net (IRC: Seeker`) original patch

Fixes #10310
  • Loading branch information
jyavenard committed Jul 17, 2012
1 parent 11ea7ef commit fa1571c
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 13 deletions.
115 changes: 109 additions & 6 deletions mythtv/libs/libmythtv/AirPlay/mythairplayserver.cpp
Expand Up @@ -7,6 +7,7 @@
#include <QNetworkInterface>
#include <QCoreApplication>
#include <QKeyEvent>
#include <QCryptographicHash>

#include "mthread.h"
#include "mythdate.h"
Expand All @@ -30,6 +31,7 @@ QMutex* MythAirplayServer::gMythAirplayServerMutex = new QMutex(QMute
#define HTTP_STATUS_OK 200
#define HTTP_STATUS_SWITCHING_PROTOCOLS 101
#define HTTP_STATUS_NOT_IMPLEMENTED 501
#define HTTP_STATUS_UNAUTHORIZED 401

#define AIRPLAY_SERVER_VERSION_STR ""
#define SERVER_INFO QString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"\
Expand Down Expand Up @@ -125,6 +127,72 @@ QString AirPlayHardwareId()
return id;
}

QString GenerateNonce(void)
{
int nonceParts[4];
QString nonce;
QTime time = QTime::currentTime();
qsrand((uint)time.msec());
nonceParts[0] = qrand();
nonceParts[1] = qrand();
nonceParts[2] = qrand();
nonceParts[3] = qrand();

nonce = QString::number(nonceParts[0], 16).toUpper();
nonce += QString::number(nonceParts[1], 16).toUpper();
nonce += QString::number(nonceParts[2], 16).toUpper();
nonce += QString::number(nonceParts[3], 16).toUpper();
return nonce;
}

QByteArray DigestMd5Response(QString response, QString option,
QString nonce, QString password,
QByteArray &auth)
{
int authStart = response.indexOf("response=\"") + 10;
int authLength = response.indexOf("\"", authStart) - authStart;
auth = response.mid(authStart, authLength).toAscii();

int uriStart = response.indexOf("uri=\"") + 5;
int uriLength = response.indexOf("\"", uriStart) - uriStart;
QByteArray uri = response.mid(uriStart, uriLength).toAscii();

int userStart = response.indexOf("username=\"") + 10;
int userLength = response.indexOf("\"", userStart) - userStart;
QByteArray user = response.mid(userStart, userLength).toAscii();

int realmStart = response.indexOf("realm=\"") + 7;
int realmLength = response.indexOf("\"", realmStart) - realmStart;
QByteArray realm = response.mid(realmStart, realmLength).toAscii();

QByteArray passwd = password.toAscii();

QCryptographicHash hash(QCryptographicHash::Md5);
hash.addData(user);
hash.addData(":", 1);
hash.addData(realm);
hash.addData(":", 1);
hash.addData(passwd);
QByteArray ha1 = hash.result();
ha1 = ha1.toHex();

// calculate H(A2)
hash.reset();
hash.addData(option.toAscii());
hash.addData(":", 1);
hash.addData(uri);
QByteArray ha2 = hash.result().toHex();

// calculate response
hash.reset();
hash.addData(ha1);
hash.addData(":", 1);
hash.addData(nonce.toAscii());
hash.addData(":", 1);
hash.addData(ha2);
return hash.result().toHex();
}

class APHTTPRequest
{
public:
Expand Down Expand Up @@ -465,9 +533,10 @@ QByteArray MythAirplayServer::StatusToString(int status)
{
switch (status)
{
case HTTP_STATUS_OK: return "OK";
case HTTP_STATUS_SWITCHING_PROTOCOLS: return "Switching Protocols";
case HTTP_STATUS_NOT_IMPLEMENTED: return "Not Implemented";
case HTTP_STATUS_OK: return "OK";
case HTTP_STATUS_SWITCHING_PROTOCOLS: return "Switching Protocols";
case HTTP_STATUS_NOT_IMPLEMENTED: return "Not Implemented";
case HTTP_STATUS_UNAUTHORIZED: return "Unauthorized";
}
return "";
}
Expand Down Expand Up @@ -586,6 +655,38 @@ void MythAirplayServer::HandleResponse(APHTTPRequest *req,
m_connections[session].was_playing = playing;
}

if (gCoreContext->GetNumSetting("AirPlayPasswordEnabled", false))
{
if (m_nonce.isEmpty())
{
m_nonce = GenerateNonce();
}
header = QString("WWW-Authenticate: Digest realm=\"AirPlay\", "
"nonce=\"%1\"\r\n").arg(m_nonce).toAscii();
if (!req->GetHeaders().contains("Authorization"))
{
SendResponse(socket, HTTP_STATUS_UNAUTHORIZED,
header, content_type, body);
return;
}

QByteArray auth;
if (DigestMd5Response(req->GetHeaders()["Authorization"], req->GetMethod(), m_nonce,
gCoreContext->GetSetting("AirPlayPassword"),
auth) == auth)
{
LOG(VB_GENERAL, LOG_INFO, LOC + "AirPlay client authenticated");
}
else
{
LOG(VB_GENERAL, LOG_INFO, LOC + "AirPlay authentication failed");
SendResponse(socket, HTTP_STATUS_UNAUTHORIZED,
header, content_type, body);
return;
}
header = "";
}

if (req->GetURI() == "/server-info")
{
content_type = "text/x-apple-plist+xml\r\n";
Expand Down Expand Up @@ -751,10 +852,12 @@ void MythAirplayServer::SendResponse(QTcpSocket *socket,
reply.append(content_type);
reply.append("Content-Length: ");
reply.append(QString::number(body.size()));
reply.append("\r\n");
}

reply.append("\r\n");
else
{
reply.append("Content-Length: 0");
}
reply.append("\r\n\r\n");

if (body.size())
reply.append(body);
Expand Down
9 changes: 8 additions & 1 deletion mythtv/libs/libmythtv/AirPlay/mythairplayserver.h
Expand Up @@ -14,7 +14,11 @@ class BonjourRegister;

#define AIRPLAY_PORT_RANGE 100
#define AIRPLAY_HARDWARE_ID_SIZE 6
QString AirPlayHardwareId();
QString AirPlayHardwareId(void);
QString GenerateNonce(void);
QByteArray DigestMd5Response(QString response, QString option,
QString nonce, QString password,
QByteArray &auth);

enum AirplayEvent
{
Expand Down Expand Up @@ -104,6 +108,9 @@ class MTV_PUBLIC MythAirplayServer : public ServerPool
QList<QTcpSocket*> m_sockets;
QHash<QByteArray,AirplayConnection> m_connections;
QString m_pathname;

//Authentication
QString m_nonce;
};

#endif // MYTHAIRPLAYSERVER_H
53 changes: 48 additions & 5 deletions mythtv/libs/libmythtv/AirPlay/mythraopconnection.cpp
Expand Up @@ -79,8 +79,7 @@ MythRAOPConnection::MythRAOPConnection(QObject *parent, QTcpSocket *socket,
m_masterTimeStamp(0), m_deviceTimeStamp(0), m_networkLatency(0),
m_clockSkew(0),
m_audioTimer(NULL),
m_progressStart(0), m_progressCurrent(0), m_progressEnd(0),
m_authenticated(false)
m_progressStart(0), m_progressCurrent(0), m_progressEnd(0)
{
}

Expand Down Expand Up @@ -876,8 +875,6 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header,
return;
}

*m_textStream << "RTSP/1.0 200 OK\r\n";

QString option = header[0].left(header[0].indexOf(" "));

// process RTP-info field
Expand All @@ -904,6 +901,36 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header,
.arg(RTPseq).arg(RTPtimestamp));
}

if (gCoreContext->GetNumSetting("AirPlayPasswordEnabled", false))
{
if (m_nonce.isEmpty())
{
m_nonce = GenerateNonce();
}
if (!tags.contains("Authorization"))
{
// 60 seconds to enter password.
m_watchdogTimer->start(60000);
FinishAuthenticationResponse(m_textStream, m_socket, tags["CSeq"]);
return;
}

QByteArray auth;
if (DigestMd5Response(tags["Authorization"], option, m_nonce,
gCoreContext->GetSetting("AirPlayPassword"),
auth) == auth)
{
LOG(VB_GENERAL, LOG_INFO, LOC + "RAOP client authenticated");
}
else
{
LOG(VB_GENERAL, LOG_INFO, LOC + "RAOP authentication failed");
FinishAuthenticationResponse(m_textStream, m_socket, tags["CSeq"]);
return;
}
}
*m_textStream << "RTSP/1.0 200 OK\r\n";

if (option == "OPTIONS")
{
if (tags.contains("Apple-Challenge"))
Expand Down Expand Up @@ -1025,7 +1052,6 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header,
}
delete [] decryptedkey;
}

}
else if (line.startsWith("a=aesiv:"))
{
Expand Down Expand Up @@ -1324,6 +1350,23 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header,
FinishResponse(m_textStream, m_socket, option, tags["CSeq"]);
}

void MythRAOPConnection::FinishAuthenticationResponse(NetStream *stream,
QTcpSocket *socket,
QString &cseq)
{
if (!stream)
return;
*stream << "RTSP/1.0 401 Unauthorised\r\n";
*stream << "Server: AirTunes/130.14\r\n";
*stream << "WWW-Authenticate: Digest realm=\"raop\", ";
*stream << "nonce=\"" + m_nonce + "\"\r\n";
*stream << "CSeq: " << cseq << "\r\n";
*stream << "\r\n";
stream->flush();
LOG(VB_GENERAL, LOG_DEBUG, LOC +
QString("Finished Authentication request %2, Send: %3")
.arg(cseq).arg(socket->flush()));
}

void MythRAOPConnection::FinishResponse(NetStream *stream, QTcpSocket *socket,
QString &option, QString &cseq)
Expand Down
6 changes: 6 additions & 0 deletions mythtv/libs/libmythtv/AirPlay/mythraopconnection.h
Expand Up @@ -77,6 +77,9 @@ class MythRAOPConnection : public QObject
const QByteArray &content);
void FinishResponse(NetStream *stream, QTcpSocket *socket,
QString &option, QString &cseq);
void FinishAuthenticationResponse(NetStream *stream, QTcpSocket *socket,
QString &cseq);

RawHash FindTags(const QStringList &lines);
bool CreateDecoder(void);
void DestroyDecoder(void);
Expand Down Expand Up @@ -174,6 +177,9 @@ class MythRAOPConnection : public QObject
QByteArray m_artwork;
QByteArray m_dmap;

//Authentication
QString m_nonce;

private slots:
void ProcessAudio(void);
};
Expand Down
9 changes: 8 additions & 1 deletion mythtv/libs/libmythtv/AirPlay/mythraopdevice.cpp
Expand Up @@ -175,7 +175,14 @@ bool MythRAOPDevice::RegisterForBonjour(void)
txt.append(4); txt.append("ch=2"); // audio channels
txt.append(5); txt.append("ss=16"); // sample size
txt.append(8); txt.append("sr=44100"); // sample rate
txt.append(8); txt.append("pw=false"); // no password
if (gCoreContext->GetNumSetting("AirPlayPasswordEnabled"))
{
txt.append(7); txt.append("pw=true");
}
else
{
txt.append(8); txt.append("pw=false");
}
txt.append(4); txt.append("vn=3");
txt.append(9); txt.append("txtvers=1"); // TXT record version 1
txt.append(8); txt.append("md=0,1,2"); // metadata-type: text, artwork, progress
Expand Down

0 comments on commit fa1571c

Please sign in to comment.