File indexing completed on 2025-01-05 04:37:28

0001 /*
0002     SPDX-FileCopyrightText: 2005-2007 Joris Guisson <joris.guisson@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "upnpmcastsocket.h"
0008 #include <QFile>
0009 #include <QStringList>
0010 #include <QTextStream>
0011 #include <QUrl>
0012 
0013 #include <netinet/in.h>
0014 #include <sys/socket.h>
0015 #include <unistd.h>
0016 #ifndef Q_WS_WIN
0017 #include <netinet/in_systm.h>
0018 #include <netinet/ip.h>
0019 #endif
0020 #include <arpa/inet.h>
0021 #include <util/log.h>
0022 
0023 namespace bt
0024 {
0025 static bool UrlCompare(const QUrl &a, const QUrl &b)
0026 {
0027     if (a == b)
0028         return true;
0029 
0030     return a.scheme() == b.scheme() && a.host() == b.host() && a.password() == b.password() && a.port(80) == b.port(80) && a.path() == b.path()
0031         && a.query() == b.query(); // TODO check if ported correctly
0032 }
0033 
0034 class UPnPMCastSocket::UPnPMCastSocketPrivate
0035 {
0036 public:
0037     UPnPMCastSocketPrivate(bool verbose);
0038     ~UPnPMCastSocketPrivate();
0039 
0040     UPnPRouter *parseResponse(const QByteArray &arr);
0041     void joinUPnPMCastGroup(int fd);
0042     void leaveUPnPMCastGroup(int fd);
0043     void onXmlFileDownloaded(UPnPRouter *r, bool success);
0044     UPnPRouter *findDevice(const QUrl &location);
0045 
0046     QSet<UPnPRouter *> routers;
0047     QSet<UPnPRouter *> pending_routers; // routers which we are downloading the XML file from
0048     bool verbose;
0049 };
0050 
0051 UPnPMCastSocket::UPnPMCastSocket(bool verbose)
0052     : d(new UPnPMCastSocketPrivate(verbose))
0053 {
0054     QObject::connect(this, &UPnPMCastSocket::readyRead, this, &UPnPMCastSocket::onReadyRead);
0055     QObject::connect(this, &UPnPMCastSocket::errorOccurred, this, &UPnPMCastSocket::error);
0056 
0057     for (Uint32 i = 0; i < 10; i++) {
0058         if (!bind(1900 + i, QUdpSocket::ShareAddress))
0059             Out(SYS_PNP | LOG_IMPORTANT) << "Cannot bind to UDP port 1900 : " << errorString() << endl;
0060         else
0061             break;
0062     }
0063 
0064     d->joinUPnPMCastGroup(socketDescriptor());
0065 }
0066 
0067 UPnPMCastSocket::~UPnPMCastSocket()
0068 {
0069     d->leaveUPnPMCastGroup(socketDescriptor());
0070     delete d;
0071 }
0072 
0073 void UPnPMCastSocket::discover()
0074 {
0075     Out(SYS_PNP | LOG_NOTICE) << "Trying to find UPnP devices on the local network" << endl;
0076 
0077     // send a HTTP M-SEARCH message to 239.255.255.250:1900
0078     const char *upnp_data =
0079         "M-SEARCH * HTTP/1.1\r\n"
0080         "HOST: 239.255.255.250:1900\r\n"
0081         "ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n"
0082         "MAN:\"ssdp:discover\"\r\n"
0083         "MX:3\r\n"
0084         "\r\n\0";
0085 
0086     const char *tr64_data =
0087         "M-SEARCH * HTTP/1.1\r\n"
0088         "HOST: 239.255.255.250:1900\r\n"
0089         "ST:urn:dslforum-org:device:InternetGatewayDevice:1\r\n"
0090         "MAN:\"ssdp:discover\"\r\n"
0091         "MX:3\r\n"
0092         "\r\n\0";
0093 
0094     if (d->verbose) {
0095         Out(SYS_PNP | LOG_NOTICE) << "Sending : " << endl;
0096         Out(SYS_PNP | LOG_NOTICE) << upnp_data << endl;
0097 
0098         Out(SYS_PNP | LOG_NOTICE) << "Sending : " << endl;
0099         Out(SYS_PNP | LOG_NOTICE) << tr64_data << endl;
0100     }
0101 
0102     writeDatagram(upnp_data, strlen(upnp_data), QHostAddress("239.255.255.250"), 1900);
0103     writeDatagram(tr64_data, strlen(tr64_data), QHostAddress("239.255.255.250"), 1900);
0104 }
0105 
0106 void UPnPMCastSocket::onXmlFileDownloaded(UPnPRouter *r, bool success)
0107 {
0108     d->pending_routers.remove(r);
0109     if (!success) {
0110         // we couldn't download and parse the XML file so
0111         // get rid of it
0112         r->deleteLater();
0113     } else {
0114         // add it to the list and emit the signal
0115         QUrl location = r->getLocation();
0116         if (d->findDevice(location)) {
0117             r->deleteLater();
0118         } else {
0119             d->routers.insert(r);
0120             discovered(r);
0121         }
0122     }
0123 }
0124 
0125 void UPnPMCastSocket::onReadyRead()
0126 {
0127     if (pendingDatagramSize() == 0) {
0128         Out(SYS_PNP | LOG_NOTICE) << "0 byte UDP packet " << endl;
0129         // KDatagramSocket wrongly handles UDP packets with no payload
0130         // so we need to deal with it oursleves
0131         int fd = socketDescriptor();
0132         char tmp;
0133         ::read(fd, &tmp, 1);
0134         return;
0135     }
0136 
0137     QByteArray data(pendingDatagramSize(), 0);
0138     if (readDatagram(data.data(), pendingDatagramSize()) == -1)
0139         return;
0140 
0141     if (d->verbose) {
0142         Out(SYS_PNP | LOG_NOTICE) << "Received : " << endl;
0143         Out(SYS_PNP | LOG_NOTICE) << QString(data) << endl;
0144     }
0145 
0146     // try to make a router of it
0147     UPnPRouter *r = d->parseResponse(data);
0148     if (r) {
0149         QObject::connect(r, &UPnPRouter::xmlFileDownloaded, this, &UPnPMCastSocket::onXmlFileDownloaded);
0150 
0151         // download it's xml file
0152         r->downloadXMLFile();
0153         d->pending_routers.insert(r);
0154     }
0155 }
0156 
0157 void UPnPMCastSocket::error(QAbstractSocket::SocketError)
0158 {
0159     Out(SYS_PNP | LOG_IMPORTANT) << "UPnPMCastSocket Error : " << errorString() << endl;
0160 }
0161 
0162 void UPnPMCastSocket::saveRouters(const QString &file)
0163 {
0164     QFile fptr(file);
0165     if (!fptr.open(QIODevice::WriteOnly)) {
0166         Out(SYS_PNP | LOG_IMPORTANT) << "Cannot open file " << file << " : " << fptr.errorString() << endl;
0167         return;
0168     }
0169 
0170     // file format is simple : 2 lines per router,
0171     // one containing the server, the other the location
0172     QTextStream fout(&fptr);
0173     for (UPnPRouter *r : std::as_const(d->routers)) {
0174         fout << r->getServer() << Qt::endl;
0175         fout << r->getLocation().toString() << Qt::endl;
0176     }
0177 }
0178 
0179 void UPnPMCastSocket::loadRouters(const QString &file)
0180 {
0181     QFile fptr(file);
0182     if (!fptr.open(QIODevice::ReadOnly)) {
0183         Out(SYS_PNP | LOG_IMPORTANT) << "Cannot open file " << file << " : " << fptr.errorString() << endl;
0184         return;
0185     }
0186 
0187     // file format is simple : 2 lines per router,
0188     // one containing the server, the other the location
0189     QTextStream fin(&fptr);
0190 
0191     while (!fin.atEnd()) {
0192         QString server, location;
0193         server = fin.readLine();
0194         location = fin.readLine();
0195 
0196         UPnPRouter *r = new UPnPRouter(server, QUrl(location));
0197         // download it's xml file
0198         QObject::connect(r, &UPnPRouter::xmlFileDownloaded, this, &UPnPMCastSocket::onXmlFileDownloaded);
0199         r->downloadXMLFile();
0200         d->pending_routers.insert(r);
0201     }
0202 }
0203 
0204 Uint32 UPnPMCastSocket::getNumDevicesDiscovered() const
0205 {
0206     return d->routers.count();
0207 }
0208 
0209 UPnPRouter *UPnPMCastSocket::findDevice(const QString &name)
0210 {
0211     QUrl location(name);
0212     return d->findDevice(location);
0213 }
0214 
0215 void UPnPMCastSocket::setVerbose(bool v)
0216 {
0217     d->verbose = v;
0218 }
0219 
0220 /////////////////////////////////////////////////////////////
0221 
0222 UPnPMCastSocket::UPnPMCastSocketPrivate::UPnPMCastSocketPrivate(bool verbose)
0223     : verbose(verbose)
0224 {
0225 }
0226 
0227 UPnPMCastSocket::UPnPMCastSocketPrivate::~UPnPMCastSocketPrivate()
0228 {
0229     qDeleteAll(pending_routers);
0230     qDeleteAll(routers);
0231 }
0232 
0233 void UPnPMCastSocket::UPnPMCastSocketPrivate::joinUPnPMCastGroup(int fd)
0234 {
0235     struct ip_mreq mreq;
0236     memset(&mreq, 0, sizeof(struct ip_mreq));
0237 
0238     inet_aton("239.255.255.250", &mreq.imr_multiaddr);
0239     mreq.imr_interface.s_addr = htonl(INADDR_ANY);
0240 
0241 #ifndef Q_WS_WIN
0242     if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)) < 0)
0243 #else
0244     if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&mreq, sizeof(struct ip_mreq)) < 0)
0245 #endif
0246     {
0247         Out(SYS_PNP | LOG_NOTICE) << "Failed to join multicast group 239.255.255.250" << endl;
0248     }
0249 }
0250 
0251 void UPnPMCastSocket::UPnPMCastSocketPrivate::leaveUPnPMCastGroup(int fd)
0252 {
0253     struct ip_mreq mreq;
0254     memset(&mreq, 0, sizeof(struct ip_mreq));
0255 
0256     inet_aton("239.255.255.250", &mreq.imr_multiaddr);
0257     mreq.imr_interface.s_addr = htonl(INADDR_ANY);
0258 
0259 #ifndef Q_WS_WIN
0260     if (setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)) < 0)
0261 #else
0262     if (setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char *)&mreq, sizeof(struct ip_mreq)) < 0)
0263 #endif
0264     {
0265         Out(SYS_PNP | LOG_NOTICE) << "Failed to leave multicast group 239.255.255.250" << endl;
0266     }
0267 }
0268 
0269 UPnPRouter *UPnPMCastSocket::UPnPMCastSocketPrivate::parseResponse(const QByteArray &arr)
0270 {
0271     const QString response = QString::fromLatin1(arr);
0272     QVector<QStringView> lines = QStringView(response).split(QStringLiteral("\r\n"));
0273     QString server;
0274     QUrl location;
0275 
0276     /*
0277     Out(SYS_PNP|LOG_DEBUG) << "Received : " << endl;
0278     for (Uint32 idx = 0;idx < lines.count(); idx++)
0279         Out(SYS_PNP|LOG_DEBUG) << lines[idx] << endl;
0280     */
0281 
0282     // first read first line and see if contains a HTTP 200 OK message
0283     auto line = lines.first();
0284     if (!line.contains(QLatin1String("HTTP"))) {
0285         // it is either a 200 OK or a NOTIFY
0286         if (!line.contains(QLatin1String("NOTIFY")) && !line.contains(QLatin1String("200")))
0287             return nullptr;
0288     } else if (line.contains(QLatin1String("M-SEARCH"))) // ignore M-SEARCH
0289         return nullptr;
0290 
0291     // quick check that the response being parsed is valid
0292     bool validDevice = false;
0293     for (int idx = 0; idx < lines.count() && !validDevice; idx++) {
0294         line = lines[idx];
0295         if ((line.contains(QLatin1String("ST:")) || line.contains(QLatin1String("NT:"))) && line.contains(QLatin1String("InternetGatewayDevice"))) {
0296             validDevice = true;
0297         }
0298     }
0299     if (!validDevice) {
0300         //  Out(SYS_PNP|LOG_IMPORTANT) << "Not a valid Internet Gateway Device" << endl;
0301         return nullptr;
0302     }
0303 
0304     // read all lines and try to find the server and location fields
0305     for (int i = 1; i < lines.count(); i++) {
0306         line = lines[i];
0307         if (line.startsWith(QLatin1String("location"), Qt::CaseInsensitive)) {
0308             location = QUrl(line.mid(line.indexOf(':') + 1).trimmed().toString()); // TODO fromLocalFile()?
0309             if (!location.isValid())
0310                 return nullptr;
0311         } else if (line.startsWith(QLatin1String("server"), Qt::CaseInsensitive)) {
0312             server = line.mid(line.indexOf(':') + 1).trimmed().toString();
0313             if (server.length() == 0)
0314                 return nullptr;
0315         }
0316     }
0317 
0318     if (findDevice(location)) {
0319         return nullptr;
0320     } else {
0321         Out(SYS_PNP | LOG_NOTICE) << "Detected IGD " << server << endl;
0322         // everything OK, make a new UPnPRouter
0323         return new UPnPRouter(server, location, verbose);
0324     }
0325 }
0326 
0327 UPnPRouter *UPnPMCastSocket::UPnPMCastSocketPrivate::findDevice(const QUrl &location)
0328 {
0329     for (UPnPRouter *r : std::as_const(routers)) {
0330         if (UrlCompare(r->getLocation(), location))
0331             return r;
0332     }
0333 
0334     return nullptr;
0335 }
0336 
0337 }
0338 
0339 #include "moc_upnpmcastsocket.cpp"