/* MiniDLNA project * http://minidlna.sourceforge.net/ * (c) 2008 Justin Maggard * * This software is subject to the conditions detailed * in the LICENCE file provided within the distribution * * Portions of the code from the MiniUPnP Project * (c) Thomas Bernard licensed under BSD revised license * detailed in the LICENSE.miniupnpd file provided within * the distribution. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "upnpglobalvars.h" #include "upnphttp.h" #include "upnpsoap.h" #include "upnpreplyparse.h" #include "getifaddr.h" #include "metadata.h" #include "sql.h" static void BuildSendAndCloseSoapResp(struct upnphttp * h, const char * body, int bodylen) { static const char beforebody[] = "\r\n" "" ""; static const char afterbody[] = "" "\r\n"; BuildHeader_upnphttp(h, 200, "OK", sizeof(beforebody) - 1 + sizeof(afterbody) - 1 + bodylen ); memcpy(h->res_buf + h->res_buflen, beforebody, sizeof(beforebody) - 1); h->res_buflen += sizeof(beforebody) - 1; memcpy(h->res_buf + h->res_buflen, body, bodylen); h->res_buflen += bodylen; memcpy(h->res_buf + h->res_buflen, afterbody, sizeof(afterbody) - 1); h->res_buflen += sizeof(afterbody) - 1; SendResp_upnphttp(h); CloseSocket_upnphttp(h); } static void GetSystemUpdateID(struct upnphttp * h, const char * action) { static const char resp[] = "" "%d" ""; char body[512]; int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ContentDirectory:1", 1, action); BuildSendAndCloseSoapResp(h, body, bodylen); } static void IsAuthorizedValidated(struct upnphttp * h, const char * action) { static const char resp[] = "" "%d" ""; char body[512]; int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1", 1, action); BuildSendAndCloseSoapResp(h, body, bodylen); } static void GetProtocolInfo(struct upnphttp * h, const char * action) { static const char resp[] = "" "" "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN," "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=01;DLNA.ORG_CI=0," "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=01;DLNA.ORG_CI=0," "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=01;DLNA.ORG_CI=0," "http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_NTSC;DLNA.ORG_OP=01;DLNA.ORG_CI=0," "http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=01;DLNA.ORG_CI=0," "http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_HD_NA_ISO;DLNA.ORG_OP=01;DLNA.ORG_CI=0," "http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_MP_HD_AC3_T;DLNA.ORG_OP=01;DLNA.ORG_CI=0," "http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVHIGH_PRO;DLNA.ORG_OP=01;DLNA.ORG_CI=0," "http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01," "http-get:*:audio/x-ms-wma:*," "http-get:*:audio/wav:*," "http-get:*:audio/mp4:*," "http-get:*:audio/x-aiff:*," "http-get:*:audio/x-flac:*," "http-get:*:application/ogg:*," "http-get:*:image/jpeg:*," "http-get:*:image/gif:*," "http-get:*:audio/x-mpegurl:*," "http-get:*:video/mpeg:*," "http-get:*:video/x-msvideo:*," "http-get:*:video/avi:*," "http-get:*:video/mpeg2:*," "http-get:*:video/dvd:*," "http-get:*:video/x-ms-wmv:*" "" "" ""; char body[1536]; int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ConnectionManager:1", action); BuildSendAndCloseSoapResp(h, body, bodylen); } static void GetSortCapabilities(struct upnphttp * h, const char * action) { static const char resp[] = "" "" ""; char body[512]; int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ContentDirectory:1", action); BuildSendAndCloseSoapResp(h, body, bodylen); } static void GetSearchCapabilities(struct upnphttp * h, const char * action) { static const char resp[] = "" "dc:title,dc:creator,upnp:class,upnp:artist,upnp:album,@refID" ""; char body[512]; int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ContentDirectory:1", action); BuildSendAndCloseSoapResp(h, body, bodylen); } static void GetCurrentConnectionIDs(struct upnphttp * h, const char * action) { /* TODO: Use real data. - JM */ static const char resp[] = "" "0" ""; char body[512]; int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ConnectionManager:1", action); BuildSendAndCloseSoapResp(h, body, bodylen); } static void GetCurrentConnectionInfo(struct upnphttp * h, const char * action) { /* TODO: Use real data. - JM */ static const char resp[] = "" "-1" "-1" "" "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN," "" "0" "-1" "0" "0" ""; char body[sizeof(resp)+128]; int bodylen; bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:service:ConnectionManager:1", action); BuildSendAndCloseSoapResp(h, body, bodylen); } static int callback(void *args, int argc, char **argv, char **azColName) { struct Response { char *resp; int returned; int requested; int total; char *filter; } *passed_args = (struct Response *)args; char *id = argv[1], *parent = argv[2], *refID = argv[3], *class = argv[4], *size = argv[9], *title = argv[10], *duration = argv[11], *bitrate = argv[12], *sampleFrequency = argv[13], *artist = argv[14], *album = argv[15], *genre = argv[16], *comment = argv[17], *nrAudioChannels = argv[18], *track = argv[19], *date = argv[20], *resolution = argv[21], *tn = argv[22], *creator = argv[23], *dlna_pn = argv[24], *mime = argv[25]; char dlna_buf[64]; char str_buf[4096]; char **result; int ret; passed_args->total++; if( passed_args->requested && (passed_args->returned >= passed_args->requested) ) return 0; passed_args->returned++; if( dlna_pn ) sprintf(dlna_buf, "DLNA.ORG_PN=%s", dlna_pn); else strcpy(dlna_buf, "*"); if( strncmp(class, "item", 4) == 0 ) { sprintf(str_buf, "<item id=\"%s\" parentID=\"%s\" restricted=\"1\"", id, parent); strcat(passed_args->resp, str_buf); if( refID && (!passed_args->filter || strstr(passed_args->filter, "@refID")) ) { sprintf(str_buf, " refID=\"%s\"", refID); strcat(passed_args->resp, str_buf); } sprintf(str_buf, ">" "<dc:title>%s</dc:title>" "<upnp:class>object.%s</upnp:class>", title, class); strcat(passed_args->resp, str_buf); if( comment && (!passed_args->filter || strstr(passed_args->filter, "dc:description")) ) { sprintf(str_buf, "<dc:description>%s</dc:description>", comment); strcat(passed_args->resp, str_buf); } if( creator && (!passed_args->filter || strstr(passed_args->filter, "dc:creator")) ) { sprintf(str_buf, "<dc:creator>%s</dc:creator>", creator); strcat(passed_args->resp, str_buf); } if( date && (!passed_args->filter || strstr(passed_args->filter, "dc:date")) ) { sprintf(str_buf, "<dc:date>%s</dc:date>", date); strcat(passed_args->resp, str_buf); } if( artist && (!passed_args->filter || strstr(passed_args->filter, "upnp:artist")) ) { sprintf(str_buf, "<upnp:artist>%s</upnp:artist>", artist); strcat(passed_args->resp, str_buf); } if( album && (!passed_args->filter || strstr(passed_args->filter, "upnp:album")) ) { sprintf(str_buf, "<upnp:album>%s</upnp:album>", album); strcat(passed_args->resp, str_buf); } if( genre && (!passed_args->filter || strstr(passed_args->filter, "upnp:genre")) ) { sprintf(str_buf, "<upnp:genre>%s</upnp:genre>", genre); strcat(passed_args->resp, str_buf); } if( track && atoi(track) && (!passed_args->filter || strstr(passed_args->filter, "upnp:originalTrackNumber")) ) { sprintf(str_buf, "<upnp:originalTrackNumber>%s</upnp:originalTrackNumber>", track); strcat(passed_args->resp, str_buf); } if( !passed_args->filter || strstr(passed_args->filter, "res") ) { strcat(passed_args->resp, "<res "); if( size && (!passed_args->filter || strstr(passed_args->filter, "res@size")) ) { sprintf(str_buf, "size=\"%s\" ", size); strcat(passed_args->resp, str_buf); } if( duration && (!passed_args->filter || strstr(passed_args->filter, "res@duration")) ) { sprintf(str_buf, "duration=\"%s\" ", duration); strcat(passed_args->resp, str_buf); } if( bitrate && (!passed_args->filter || strstr(passed_args->filter, "res@bitrate")) ) { sprintf(str_buf, "bitrate=\"%s\" ", bitrate); strcat(passed_args->resp, str_buf); } if( sampleFrequency && (!passed_args->filter || strstr(passed_args->filter, "res@sampleFrequency")) ) { sprintf(str_buf, "sampleFrequency=\"%s\" ", sampleFrequency); strcat(passed_args->resp, str_buf); } if( nrAudioChannels && (!passed_args->filter || strstr(passed_args->filter, "res@nrAudioChannels")) ) { sprintf(str_buf, "nrAudioChannels=\"%s\" ", nrAudioChannels); strcat(passed_args->resp, str_buf); } if( resolution && (!passed_args->filter || strstr(passed_args->filter, "res@resolution")) ) { sprintf(str_buf, "resolution=\"%s\" ", resolution); strcat(passed_args->resp, str_buf); } sprintf(str_buf, "protocolInfo=\"http-get:*:%s:%s\">" "http://%s:5555/MediaItems/%s" "</res>", mime, dlna_buf, lan_addr[0].str, id); #if 0 //JPEG_RESIZE if( dlna_pn && (strncmp(dlna_pn, "JPEG_LRG", 8) == 0) ) { strcat(passed_args->resp, str_buf); sprintf(str_buf, "<res " "protocolInfo=\"http-get:*:%s:%s\">" "http://%s:5555/Resized/%s" "</res>", mime, "DLNA.ORG_PN=JPEG_SM", lan_addr[0].str, id); } #endif if( tn && atoi(tn) && dlna_pn ) { strcat(passed_args->resp, str_buf); strcat(passed_args->resp, "<res "); sprintf(str_buf, "protocolInfo=\"http-get:*:%s:%s\">" "http://%s:5555/Thumbnails/%s" "</res>", mime, "DLNA.ORG_PN=JPEG_TN", lan_addr[0].str, id); } strcat(passed_args->resp, str_buf); } strcpy(str_buf, "</item>"); } else if( strncmp(class, "container", 9) == 0 ) { sprintf(str_buf, "SELECT count(*) from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID) where PARENT_ID = '%s' order by d.TRACK, d.TITLE, o.NAME;", id); ret = sqlite3_get_table(db, str_buf, &result, 0, 0, 0); sprintf(str_buf, "<container id=\"%s\" parentID=\"%s\" restricted=\"1\" ", id, parent); strcat(passed_args->resp, str_buf); if( !passed_args->filter || strstr(passed_args->filter, "@childCount")) { sprintf(str_buf, "childCount=\"%s\"", result[1]); strcat(passed_args->resp, str_buf); } /* If the client calls for BrowseMetadata on root, we have to include our "upnp:searchClass"'s, unless they're filtered out */ if( (passed_args->requested == 1) && (strcmp(id, "0") == 0) ) { if( !passed_args->filter || strstr(passed_args->filter, "upnp:searchClass") ) { strcat(passed_args->resp, ">" "<upnp:searchClass includeDerived=\"1\">object.item.audioItem</upnp:searchClass>" "<upnp:searchClass includeDerived=\"1\">object.item.imageItem</upnp:searchClass>" "<upnp:searchClass includeDerived=\"1\">object.item.videoItem</upnp:searchClass"); } } sprintf(str_buf, ">" "<dc:title>%s</dc:title>" "<upnp:class>object.%s</upnp:class>" "</container>", title, class); sqlite3_free_table(result); } strcat(passed_args->resp, str_buf); return 0; } static void BrowseContentDirectory(struct upnphttp * h, const char * action) { static const char resp0[] = "" "" "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">\n"; static const char resp1[] = "</DIDL-Lite>"; static const char resp2[] = "0"; char *resp = calloc(1, 1048576); char str_buf[4096]; char *zErrMsg = 0; char *sql; int ret; struct Response { char *resp; int returned; int requested; int total; char *filter; } args; struct NameValueParserData data; ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data); int RequestedCount = atoi( GetValueFromNameValueList(&data, "RequestedCount") ); int StartingIndex = atoi( GetValueFromNameValueList(&data, "StartingIndex") ); char * ObjectId = GetValueFromNameValueList(&data, "ObjectID"); char * Filter = GetValueFromNameValueList(&data, "Filter"); char * BrowseFlag = GetValueFromNameValueList(&data, "BrowseFlag"); char * SortCriteria = GetValueFromNameValueList(&data, "SortCriteria"); if( !ObjectId ) ObjectId = GetValueFromNameValueList(&data, "ContainerID"); memset(str_buf, '\0', sizeof(str_buf)); memset(&args, 0, sizeof(args)); strcpy(resp, resp0); args.total = StartingIndex; args.returned = 0; args.requested = RequestedCount; args.resp = NULL; args.filter = NULL; printf("Asked for ObjectID: %s\n", ObjectId); printf("Asked for Count: %d\n", RequestedCount); printf("Asked for StartingIndex: %d\n", StartingIndex); printf("Asked for BrowseFlag: %s\n", BrowseFlag); printf("Asked for Filter: %s\n", Filter); if( SortCriteria ) printf("Asked for SortCriteria: %s\n", SortCriteria); if( !Filter ) { ClearNameValueList(&data); SoapError(h, 402, "Invalid Args"); return; } if( strlen(Filter) > 1 ) args.filter = Filter; args.resp = resp; if( strcmp(BrowseFlag, "BrowseMetadata") == 0 ) { args.requested = 1; sql = sqlite3_mprintf("SELECT * from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID) where OBJECT_ID = '%s';", ObjectId); ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg); } else { sql = sqlite3_mprintf("SELECT * from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)" " where PARENT_ID = '%s' order by d.TRACK, d.TITLE, o.NAME limit %d, -1;", ObjectId, StartingIndex); ret = sqlite3_exec(db, sql, callback, (void *) &args, &zErrMsg); } sqlite3_free(sql); if( ret != SQLITE_OK ){ printf("SQL error: %s\n", zErrMsg); sqlite3_free(zErrMsg); } strcat(resp, resp1); sprintf(str_buf, "\n%u\n%u\n", args.returned, args.total); strcat(resp, str_buf); strcat(resp, resp2); BuildSendAndCloseSoapResp(h, resp, strlen(resp)); ClearNameValueList(&data); free(resp); } static void SearchContentDirectory(struct upnphttp * h, const char * action) { static const char resp0[] = "" "" "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">\n"; static const char resp1[] = "</DIDL-Lite>"; static const char resp2[] = "0"; char *resp = calloc(1, 1048576); char *zErrMsg = 0; char sql_buf[4096]; char str_buf[4096]; int ret; struct Response { char *resp; int returned; int requested; int total; char *filter; } args; struct NameValueParserData data; ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data); int RequestedCount = atoi( GetValueFromNameValueList(&data, "RequestedCount") ); int StartingIndex = atoi( GetValueFromNameValueList(&data, "StartingIndex") ); char * ContainerID = GetValueFromNameValueList(&data, "ContainerID"); char * Filter = GetValueFromNameValueList(&data, "Filter"); char * SearchCriteria = GetValueFromNameValueList(&data, "SearchCriteria"); char * SortCriteria = GetValueFromNameValueList(&data, "SortCriteria"); memset(str_buf, '\0', sizeof(str_buf)); memset(&args, 0, sizeof(args)); args.total = 0; args.returned = 0; args.requested = RequestedCount; args.resp = NULL; args.filter = NULL; printf("Asked for ContainerID: %s\n", ContainerID); printf("Asked for Count: %d\n", RequestedCount); printf("Asked for StartingIndex: %d\n", StartingIndex); printf("Asked for SearchCriteria: %s\n", SearchCriteria); printf("Asked for Filter: %s\n", Filter); if( SortCriteria ) printf("Asked for SortCriteria: %s\n", SortCriteria); strcpy(resp, resp0); if( !Filter ) { ClearNameValueList(&data); SoapError(h, 402, "Invalid Args"); return; } if( strlen(Filter) > 1 ) args.filter = Filter; if( strcmp(ContainerID, "0") == 0 ) *ContainerID = '%'; if( !SearchCriteria ) { asprintf(&SearchCriteria, "1 = 1"); } else { SearchCriteria = modifyString(SearchCriteria, """, "\"", 0); SearchCriteria = modifyString(SearchCriteria, "'", "'", 0); SearchCriteria = modifyString(SearchCriteria, "derivedfrom", "like", 1); SearchCriteria = modifyString(SearchCriteria, "contains", "like", 1); SearchCriteria = modifyString(SearchCriteria, "dc:title", "d.TITLE", 0); SearchCriteria = modifyString(SearchCriteria, "dc:creator", "d.CREATOR", 0); SearchCriteria = modifyString(SearchCriteria, "upnp:class", "o.CLASS", 0); SearchCriteria = modifyString(SearchCriteria, "upnp:artist", "d.ARTIST", 0); SearchCriteria = modifyString(SearchCriteria, "upnp:album", "d.ALBUM", 0); SearchCriteria = modifyString(SearchCriteria, "exists true", "is not NULL", 0); SearchCriteria = modifyString(SearchCriteria, "exists false", "is NULL", 0); SearchCriteria = modifyString(SearchCriteria, "@refID", "REF_ID", 0); SearchCriteria = modifyString(SearchCriteria, "object.", "", 0); } printf("Translated SearchCriteria: %s\n", SearchCriteria); args.resp = resp; sprintf(sql_buf, "SELECT * from OBJECTS o left join DETAILS d on (d.ID = o.DETAIL_ID)" " where OBJECT_ID like '%s$%%' and (%s) order by d.TRACK, d.TITLE, o.NAME limit %d, -1;", ContainerID, SearchCriteria, StartingIndex); printf("Search SQL: %s\n", sql_buf); ret = sqlite3_exec(db, sql_buf, callback, (void *) &args, &zErrMsg); if( ret != SQLITE_OK ){ printf("SQL error: %s\n", zErrMsg); sqlite3_free(zErrMsg); } strcat(resp, resp1); sprintf(str_buf, "\n%u\n%u\n", args.returned, args.total); strcat(resp, str_buf); strcat(resp, resp2); BuildSendAndCloseSoapResp(h, resp, strlen(resp)); ClearNameValueList(&data); free(resp); } /* If a control point calls QueryStateVariable on a state variable that is not buffered in memory within (or otherwise available from) the service, the service must return a SOAP fault with an errorCode of 404 Invalid Var. QueryStateVariable remains useful as a limited test tool but may not be part of some future versions of UPnP. */ static void QueryStateVariable(struct upnphttp * h, const char * action) { static const char resp[] = "" "%s" ""; char body[512]; int bodylen; struct NameValueParserData data; const char * var_name; ParseNameValue(h->req_buf + h->req_contentoff, h->req_contentlen, &data); /*var_name = GetValueFromNameValueList(&data, "QueryStateVariable"); */ /*var_name = GetValueFromNameValueListIgnoreNS(&data, "varName");*/ var_name = GetValueFromNameValueList(&data, "varName"); /*syslog(LOG_INFO, "QueryStateVariable(%.40s)", var_name); */ if(!var_name) { SoapError(h, 402, "Invalid Args"); } else if(strcmp(var_name, "ConnectionStatus") == 0) { bodylen = snprintf(body, sizeof(body), resp, action, "urn:schemas-upnp-org:control-1-0", "Connected", action); BuildSendAndCloseSoapResp(h, body, bodylen); } #if 0 /* not useful */ else if(strcmp(var_name, "ConnectionType") == 0) { bodylen = snprintf(body, sizeof(body), resp, "IP_Routed"); BuildSendAndCloseSoapResp(h, body, bodylen); } else if(strcmp(var_name, "LastConnectionError") == 0) { bodylen = snprintf(body, sizeof(body), resp, "ERROR_NONE"); BuildSendAndCloseSoapResp(h, body, bodylen); } #endif else { syslog(LOG_NOTICE, "%s: Unknown: %s", action, var_name?var_name:""); SoapError(h, 404, "Invalid Var"); } ClearNameValueList(&data); } static const struct { const char * methodName; void (*methodImpl)(struct upnphttp *, const char *); } soapMethods[] = { { "QueryStateVariable", QueryStateVariable}, { "Browse", BrowseContentDirectory}, { "Search", SearchContentDirectory}, { "GetSearchCapabilities", GetSearchCapabilities}, { "GetSortCapabilities", GetSortCapabilities}, { "GetSystemUpdateID", GetSystemUpdateID}, { "GetProtocolInfo", GetProtocolInfo}, { "GetCurrentConnectionIDs", GetCurrentConnectionIDs}, { "GetCurrentConnectionInfo", GetCurrentConnectionInfo}, { "IsAuthorized", IsAuthorizedValidated}, { "IsValidated", IsAuthorizedValidated}, { 0, 0 } }; void ExecuteSoapAction(struct upnphttp * h, const char * action, int n) { char * p; char * p2; int i, len, methodlen; i = 0; p = strchr(action, '#'); if(p) { p++; p2 = strchr(p, '"'); if(p2) methodlen = p2 - p; else methodlen = n - (p - action); /*syslog(LOG_DEBUG, "SoapMethod: %.*s", methodlen, p);*/ while(soapMethods[i].methodName) { len = strlen(soapMethods[i].methodName); if(strncmp(p, soapMethods[i].methodName, len) == 0) { soapMethods[i].methodImpl(h, soapMethods[i].methodName); return; } i++; } syslog(LOG_NOTICE, "SoapMethod: Unknown: %.*s", methodlen, p); } SoapError(h, 401, "Invalid Action"); } /* Standard Errors: * * errorCode errorDescription Description * -------- ---------------- ----------- * 401 Invalid Action No action by that name at this service. * 402 Invalid Args Could be any of the following: not enough in args, * too many in args, no in arg by that name, * one or more in args are of the wrong data type. * 403 Out of Sync Out of synchronization. * 501 Action Failed May be returned in current state of service * prevents invoking that action. * 600-699 TBD Common action errors. Defined by UPnP Forum * Technical Committee. * 700-799 TBD Action-specific errors for standard actions. * Defined by UPnP Forum working committee. * 800-899 TBD Action-specific errors for non-standard actions. * Defined by UPnP vendor. */ void SoapError(struct upnphttp * h, int errCode, const char * errDesc) { static const char resp[] = "" "" "" "s:Client" "UPnPError" "" "" "%d" "%s" "" "" "" "" ""; char body[2048]; int bodylen; syslog(LOG_INFO, "Returning UPnPError %d: %s", errCode, errDesc); bodylen = snprintf(body, sizeof(body), resp, errCode, errDesc); BuildResp2_upnphttp(h, 500, "Internal Server Error", body, bodylen); SendResp_upnphttp(h); CloseSocket_upnphttp(h); }