Files
neutrino/lib/libupnpclient/UPNPDevice.cpp
Hendi 89f7276185 upnp: fix url handling when server provides absolute urls
This happens with Jellyfin and possibly other DLNA servers.
2023-07-05 18:53:13 +02:00

500 lines
13 KiB
C++

/***********************************************************************************
* UPNPDevice.cpp
*
* Copyright (c) 2007 Jochen Friedrich
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
***********************************************************************************/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string.h>
#include <xmlinterface.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
#include "upnpclient.h"
#include <algorithm>
#include <errno.h>
#include <stdio.h>
struct ToLower
{
char operator() (char c) const { return std::tolower(c); }
};
static void trim(std::string &buf)
{
std::string::size_type pos1, pos2;
pos1 = buf.find_first_not_of(" \t\n\r");
pos2 = buf.find_last_not_of(" \t\n\r");
if ((pos1 == std::string::npos) || (pos2 == std::string::npos))
buf="";
else
buf = buf.substr(pos1, pos2 - pos1 + 1);
}
bool CUPnPDevice::check_response(std::string header, std::string& charset, std::string& rcode)
{
std::string::size_type pos;
std::string content, attrib, chars;
charset="ISO-8859-1";
pos = header.find("\r");
if (pos == std::string::npos)
return false;
rcode = header.substr(0, pos);
if (rcode.substr(0,5) != "HTTP/")
return false;
pos = rcode.find(" ");
if (pos == std::string::npos)
return false;
rcode.erase(0, pos);
trim(rcode);
pos = rcode.find(" ");
if (pos != std::string::npos)
{
rcode.erase(pos);
trim(rcode);
}
std::transform(header.begin(), header.end(), header.begin(), ToLower());
pos = header.find("content-type:");
if (pos == std::string::npos)
return false;
content = header.substr(pos);
content.erase(0,13);
pos = content.find("\n");
if (pos != std::string::npos)
content.erase(pos);
trim(content);
pos=content.find(";");
if (pos!=std::string::npos)
{
attrib=content.substr(pos+1);
content.erase(pos);
trim(content);
}
if (content != "text/xml")
return false;
trim(attrib);
if (attrib.substr(0,8) == "charset=")
{
attrib.erase(0,8);
pos = attrib.find("\"");
if (pos!=std::string::npos)
{
attrib.erase(0,pos+1);
pos = attrib.find("\"");
if (pos==std::string::npos)
return false;
chars=attrib.substr(0,pos);
}
else
chars=attrib;
if (!strcasecmp(chars.c_str(), "utf-8"))
charset = "UTF-8";
}
return true;
}
CUPnPDevice::CUPnPDevice(std::string url)
{
std::string result, head, body, charset, urlbase;
std::string curl, eurl, name, mimetype, iurl, rcode;
std::string::size_type pos;
xmlNodePtr root, device, service, node, snode, icon;
int width = 0;
int height = 0;
int depth = 0;
bool servicefound = false;
descurl = url;
urlbase = url.substr(7);
pos = urlbase.find("/");
if (pos != std::string::npos)
urlbase=url.substr(0,pos+7);
else
urlbase=url;
result = HTTP(url);
pos = result.find("\r\n\r\n");
if (pos == std::string::npos)
throw std::runtime_error(std::string("no desc body"));
head = result.substr(0,pos);
body = result.substr(pos+4);
if (body.empty())
throw std::runtime_error(std::string("desc body empty"));
if (!check_response(head, charset, rcode))
throw std::runtime_error(std::string("protocol error"));
if (rcode != "200")
throw std::runtime_error(std::string("description url returned ") + rcode);
xmlDocPtr parser = parseXml(body.c_str(),charset.c_str());
if(!parser)
throw std::runtime_error(std::string("XML: parser error"));
root = xmlDocGetRootElement(parser);
if (!root)
throw std::runtime_error(std::string("XML: no root node"));
if (strcmp(xmlGetName(root),"root"))
throw std::runtime_error(std::string("XML: no root"));
for (node = xmlChildrenNode(root); node; node=xmlNextNode(node))
{
if (!strcmp(xmlGetName(node),"URLBase"))
{
urlbase = std::string(xmlGetData(node));
if ((!urlbase.empty() ) && (urlbase[urlbase.length()-1] == '/'))
urlbase.erase(urlbase.length()-1);
}
}
node = xmlChildrenNode(root);
if (!node)
throw std::runtime_error(std::string("XML: no root child"));
while (strcmp(xmlGetName(node),"device"))
{
node = xmlNextNode(node);
if (!node)
throw std::runtime_error(std::string("XML: no device"));
}
device = node;
for (node=xmlChildrenNode(device); node; node=xmlNextNode(node))
{
if (!strcmp(xmlGetName(node),"deviceType"))
devicetype = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"friendlyName"))
friendlyname = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"manufacturer"))
manufacturer = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"manufacturerURL"))
manufacturerurl = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"modelDescription"))
modeldescription = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"modelName"))
modelname = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"modelNumber"))
modelnumber = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"modelURL"))
modelurl = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"serialNumber"))
serialnumber = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"UDN"))
udn = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"UPC"))
upc = std::string(xmlGetData(node)?xmlGetData(node):"");
if (!strcmp(xmlGetName(node),"iconList"))
{
for (icon=xmlChildrenNode(node); icon; icon=xmlNextNode(icon))
{
bool foundm = false;
bool foundw = false;
bool foundh = false;
bool foundd = false;
bool foundu = false;
if (strcmp(xmlGetName(icon),"icon"))
throw std::runtime_error(std::string("XML: no icon"));
for (snode=xmlChildrenNode(icon); snode; snode=xmlNextNode(snode))
{
if (!strcmp(xmlGetName(snode),"mimetype"))
{
mimetype=std::string(xmlGetData(snode)?xmlGetData(snode):"");
foundm = true;
}
if (!strcmp(xmlGetName(snode),"width"))
{
width=xmlGetData(snode)?atoi(xmlGetData(snode)):0;
foundw = true;
}
if (!strcmp(xmlGetName(snode),"height"))
{
height=xmlGetData(snode)?atoi(xmlGetData(snode)):0;
foundh = true;
}
if (!strcmp(xmlGetName(snode),"depth"))
{
depth=xmlGetData(snode)?atoi(xmlGetData(snode)):0;
foundd = true;
}
if (!strcmp(xmlGetName(snode),"url"))
{
url=std::string(xmlGetData(snode)?xmlGetData(snode):"");
foundu = true;
}
}
if (!foundm)
throw std::runtime_error(std::string("XML: icon without mime"));
if (!foundw)
throw std::runtime_error(std::string("XML: icon without width"));
if (!foundh)
throw std::runtime_error(std::string("XML: icon without height"));
if (!foundd)
throw std::runtime_error(std::string("XML: icon without depth"));
if (!foundu)
throw std::runtime_error(std::string("XML: icon without url"));
UPnPIcon e={mimetype, url, width, height, depth};
icons.push_back(e);
}
}
if (!strcmp(xmlGetName(node),"serviceList"))
{
servicefound = true;
for (service=xmlChildrenNode(node); service; service=xmlNextNode(service))
{
bool foundc = false;
bool founde = false;
bool foundn = false;
if (strcmp(xmlGetName(service),"service"))
throw std::runtime_error(std::string("XML: no service"));
for (snode=xmlChildrenNode(service); snode; snode=xmlNextNode(snode))
{
if (!strcmp(xmlGetName(snode),"serviceType"))
{
name=std::string(xmlGetData(snode)?xmlGetData(snode):"");
foundn = true;
}
if (!strcmp(xmlGetName(snode),"eventSubURL"))
{
const char *p = xmlGetData(snode);
if (!p)
eurl=urlbase + "/";
else if (p[0]=='/')
eurl=urlbase + std::string(p);
else if (!strncmp(p, "http", 4))
eurl=std::string(p);
else
eurl=urlbase + "/" + std::string(p);
founde = true;
}
if (!strcmp(xmlGetName(snode),"controlURL"))
{
const char *p = xmlGetData(snode);
if (!p)
curl=urlbase + "/";
else if (p[0]=='/')
curl=urlbase + std::string(p);
else if (!strncmp(p, "http", 4))
curl=std::string(p);
else
curl=urlbase + "/" + std::string(p);
foundc = true;
}
}
if (!foundn)
throw std::runtime_error(std::string("XML: no service type"));
if (!founde)
throw std::runtime_error(std::string("XML: no event url"));
if (!foundc)
throw std::runtime_error(std::string("XML: no control url"));
//try
{
services.push_back(CUPnPService(this, curl, eurl, name));
}
//catch (std::runtime_error error)
//{
// std::cout << "error " << error.what() << "\n";
//}
}
}
}
if (!servicefound)
throw std::runtime_error(std::string("XML: no service list"));
xmlFreeDoc(parser);
}
CUPnPDevice::~CUPnPDevice()
{
}
std::string CUPnPDevice::HTTP(std::string url, std::string post, std::string action)
{
std::string portname;
std::string hostname;
std::string path;
int port, t_socket, received;
std::stringstream command, reply;
std::string commandstr, line;
struct sockaddr_in socktcp;
struct hostent *hp;
std::string::size_type pos1, pos2;
char buf[4096];
if (url.substr(0,7) != "http://")
return "";
url.erase(0,7);
pos1 = url.find(":", 0);
pos2 = url.find("/", 0);
if (pos2 == std::string::npos)
return "";
if ((pos1 == std::string::npos) || (pos1 > pos2))
{
hostname = url.substr(0,pos2);
port=80;
}
else
{
hostname = url.substr(0,pos1);
portname = url.substr(pos1+1, pos2-pos1-1);
port = atoi(portname.c_str());
}
path = url.substr(pos2);
if (!post.empty())
command << "POST " << path << " HTTP/1.0\r\n";
else
command << "GET " << path << " HTTP/1.0\r\n";
command << "Host: " << hostname << ":" << port << "\r\n";
command << "User-Agent: TuxBox\r\n";
command << "Accept: text/xml\r\n";
command << "Connection: Close\r\n";
if (!post.empty())
{
command << "Content-Length: " << post.size() << "\r\n";
command << "Content-Type: text/xml\r\n";
}
if (!action.empty())
command << "SOAPAction: \"" << action << "\"\r\n";
command << "\r\n";
if (!post.empty())
command << post;
t_socket = socket(PF_INET, SOCK_STREAM, 0);
if (!t_socket)
throw std::runtime_error(std::string("create TCP socket"));
hp = gethostbyname(hostname.c_str());
if (!hp)
throw std::runtime_error(std::string("resolve name"));
memset(&socktcp, 0, sizeof(struct sockaddr_in));
socktcp.sin_family = AF_INET;
socktcp.sin_port = htons(port);
memmove((char *)&socktcp.sin_addr, hp->h_addr, hp->h_length);
if (connect(t_socket, (struct sockaddr*) &socktcp, sizeof(struct sockaddr_in)))
{
close(t_socket);
throw std::runtime_error(std::string("connect"));
}
commandstr = command.str();
send(t_socket, commandstr.c_str(), commandstr.size(), 0);
#if 0
while ((received = recv(t_socket, buf, sizeof(buf)-1, 0)) > 0)
{
buf[received] = 0;
reply << buf;
}
#endif
struct pollfd fds[1];
fds[0].fd = t_socket;
fds[0].events = POLLIN;
while (true) {
int result = poll(fds, 1, 4000);
if (result < 0) {
printf("CUPnPDevice::HTTP: poll error %s\n", strerror(errno));
break;
}
if (result == 0) {
printf("CUPnPDevice::HTTP: poll timeout\n");
break;
}
received = recv(t_socket, buf, sizeof(buf)-1, 0);
if (received <= 0)
break;
buf[received] = 0;
reply << buf;
}
close(t_socket);
return reply.str();
}
std::list<UPnPAttribute> CUPnPDevice::SendSOAP(std::string servicename, std::string action, std::list<UPnPAttribute> attribs)
{
std::list<CUPnPService>::iterator i;
std::list<UPnPAttribute> empty;
for (i=services.begin(); i != services.end(); ++i)
{
if (i->servicename == servicename)
return i->SendSOAP(action, attribs);
}
return empty;
}
std::list<std::string> CUPnPDevice::GetServices(void)
{
std::list<CUPnPService>::iterator i;
std::list<std::string> result;
for (i=services.begin(); i != services.end(); ++i)
result.push_back(i->servicename);
return result;
}