Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add AirPlay password authentication support.

Thanks to mythtv@cjo20.net (IRC: Seeker`) original patch

Fixes #10310
  • Loading branch information...
commit fa1571ce87401f34b64698d68f2770d74fa83969 1 parent 11ea7ef
@jyavenard jyavenard authored
View
115 mythtv/libs/libmythtv/AirPlay/mythairplayserver.cpp
@@ -7,6 +7,7 @@
#include <QNetworkInterface>
#include <QCoreApplication>
#include <QKeyEvent>
+#include <QCryptographicHash>
#include "mthread.h"
#include "mythdate.h"
@@ -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"\
@@ -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:
@@ -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 "";
}
@@ -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";
@@ -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);
View
9 mythtv/libs/libmythtv/AirPlay/mythairplayserver.h
@@ -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
{
@@ -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
View
53 mythtv/libs/libmythtv/AirPlay/mythraopconnection.cpp
@@ -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)
{
}
@@ -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
@@ -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"))
@@ -1025,7 +1052,6 @@ void MythRAOPConnection::ProcessRequest(const QStringList &header,
}
delete [] decryptedkey;
}
-
}
else if (line.startsWith("a=aesiv:"))
{
@@ -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)
View
6 mythtv/libs/libmythtv/AirPlay/mythraopconnection.h
@@ -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);
@@ -174,6 +177,9 @@ class MythRAOPConnection : public QObject
QByteArray m_artwork;
QByteArray m_dmap;
+ //Authentication
+ QString m_nonce;
+
private slots:
void ProcessAudio(void);
};
View
9 mythtv/libs/libmythtv/AirPlay/mythraopdevice.cpp
@@ -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
Please sign in to comment.
Something went wrong with that request. Please try again.