1 /*****************************************************************
2 |
3 |   Platinum - File Media Server Delegate
4 |
5 | Copyright (c) 2004-2010, Plutinosoft, LLC.
6 | All rights reserved.
7 | http://www.plutinosoft.com
8 |
9 | This program is free software; you can redistribute it and/or
10 | modify it under the terms of the GNU General Public License
11 | as published by the Free Software Foundation; either version 2
12 | of the License, or (at your option) any later version.
13 |
14 | OEMs, ISVs, VARs and other distributors that combine and
15 | distribute commercially licensed software with Platinum software
16 | and do not wish to distribute the source code for the commercially
17 | licensed software under version 2, or (at your option) any later
18 | version, of the GNU General Public License (the "GPL") must enter
19 | into a commercial license agreement with Plutinosoft, LLC.
20 | licensing@plutinosoft.com
21 |
22 | This program is distributed in the hope that it will be useful,
23 | but WITHOUT ANY WARRANTY; without even the implied warranty of
24 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25 | GNU General Public License for more details.
26 |
27 | You should have received a copy of the GNU General Public License
28 | along with this program; see the file LICENSE.txt. If not, write to
29 | the Free Software Foundation, Inc.,
30 | 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
31 | http://www.gnu.org/licenses/gpl-2.0.html
32 |
33 ****************************************************************/
34 
35 /*----------------------------------------------------------------------
36 |   includes
37 +---------------------------------------------------------------------*/
38 #include "PltUPnP.h"
39 #include "PltFileMediaServer.h"
40 #include "PltMediaItem.h"
41 #include "PltService.h"
42 #include "PltTaskManager.h"
43 #include "PltHttpServer.h"
44 #include "PltDidl.h"
45 #include "PltVersion.h"
46 #include "PltMimeType.h"
47 
48 NPT_SET_LOCAL_LOGGER("platinum.media.server.file.delegate")
49 
50 /*----------------------------------------------------------------------
51 |   PLT_FileMediaServerDelegate::PLT_FileMediaServerDelegate
52 +---------------------------------------------------------------------*/
PLT_FileMediaServerDelegate(const char * url_root,const char * file_root,bool use_cache)53 PLT_FileMediaServerDelegate::PLT_FileMediaServerDelegate(const char* url_root,
54                                                          const char* file_root,
55                                                          bool        use_cache) :
56     m_UrlRoot(url_root),
57     m_FileRoot(file_root),
58     m_FilterUnknownOut(false),
59     m_UseCache(use_cache)
60 {
61     /* Trim excess separators */
62     m_FileRoot.TrimRight("/\\");
63 }
64 
65 /*----------------------------------------------------------------------
66 |   PLT_FileMediaServerDelegate::~PLT_FileMediaServerDelegate
67 +---------------------------------------------------------------------*/
~PLT_FileMediaServerDelegate()68 PLT_FileMediaServerDelegate::~PLT_FileMediaServerDelegate()
69 {
70 }
71 
72 /*----------------------------------------------------------------------
73 |   PLT_FileMediaServerDelegate::ProcessFileRequest
74 +---------------------------------------------------------------------*/
75 NPT_Result
ProcessFileRequest(NPT_HttpRequest & request,const NPT_HttpRequestContext & context,NPT_HttpResponse & response)76 PLT_FileMediaServerDelegate::ProcessFileRequest(NPT_HttpRequest&              request,
77                                                 const NPT_HttpRequestContext& context,
78                                                 NPT_HttpResponse&             response)
79 {
80     NPT_HttpUrlQuery query(request.GetUrl().GetQuery());
81 
82     PLT_LOG_HTTP_REQUEST(NPT_LOG_LEVEL_FINE, "PLT_FileMediaServerDelegate::ProcessFileRequest:", &request);
83 
84     if (request.GetMethod().Compare("GET") && request.GetMethod().Compare("HEAD")) {
85         response.SetStatus(500, "Internal Server Error");
86         return NPT_SUCCESS;
87     }
88 
89     /* Extract file path from url */
90     NPT_String file_path;
91     NPT_CHECK_LABEL_WARNING(ExtractResourcePath(request.GetUrl(), file_path), failure);
92 
93     /* Serve file */
94     NPT_CHECK_WARNING(ServeFile(request, context, response, NPT_FilePath::Create(m_FileRoot, file_path)));
95     return NPT_SUCCESS;
96 
97 failure:
98     response.SetStatus(404, "File Not Found");
99     return NPT_SUCCESS;
100 }
101 
102 /*----------------------------------------------------------------------
103 |   PLT_FileMediaServerDelegate::ServeFile
104 +---------------------------------------------------------------------*/
105 NPT_Result
ServeFile(const NPT_HttpRequest & request,const NPT_HttpRequestContext & context,NPT_HttpResponse & response,const NPT_String & file_path)106 PLT_FileMediaServerDelegate::ServeFile(const NPT_HttpRequest&        request,
107                                        const NPT_HttpRequestContext& context,
108                                        NPT_HttpResponse&             response,
109                                        const NPT_String&             file_path)
110 {
111     NPT_CHECK_WARNING(PLT_HttpServer::ServeFile(request, context, response, file_path));
112     return NPT_SUCCESS;
113 }
114 
115 /*----------------------------------------------------------------------
116 |   PLT_FileMediaServerDelegate::OnBrowseMetadata
117 +---------------------------------------------------------------------*/
118 NPT_Result
OnBrowseMetadata(PLT_ActionReference & action,const char * object_id,const char * filter,NPT_UInt32 starting_index,NPT_UInt32 requested_count,const char * sort_criteria,const PLT_HttpRequestContext & context)119 PLT_FileMediaServerDelegate::OnBrowseMetadata(PLT_ActionReference&          action,
120                                               const char*                   object_id,
121                                               const char*                   filter,
122                                               NPT_UInt32                    starting_index,
123                                               NPT_UInt32                    requested_count,
124                                               const char*                   sort_criteria,
125                                               const PLT_HttpRequestContext& context)
126 {
127     NPT_COMPILER_UNUSED(sort_criteria);
128     NPT_COMPILER_UNUSED(requested_count);
129     NPT_COMPILER_UNUSED(starting_index);
130 
131     NPT_String didl;
132     PLT_MediaObjectReference item;
133 
134     /* locate the file from the object ID */
135     NPT_String filepath;
136     if (NPT_FAILED(GetFilePath(object_id, filepath))) {
137         /* error */
138         NPT_LOG_WARNING("PLT_FileMediaServerDelegate::OnBrowse - ObjectID not found.");
139         action->SetError(701, "No Such Object.");
140         return NPT_FAILURE;
141     }
142 
143     /* build the object didl */
144     item = BuildFromFilePath(filepath, context, true, false, (NPT_String(filter).Find("ALLIP")!=-1));
145     if (item.IsNull()) return NPT_FAILURE;
146     NPT_String tmp;
147     NPT_CHECK_SEVERE(PLT_Didl::ToDidl(*item.AsPointer(), filter, tmp));
148 
149     /* add didl header and footer */
150     didl = didl_header + tmp + didl_footer;
151 
152     NPT_CHECK_SEVERE(action->SetArgumentValue("Result", didl));
153     NPT_CHECK_SEVERE(action->SetArgumentValue("NumberReturned", "1"));
154     NPT_CHECK_SEVERE(action->SetArgumentValue("TotalMatches", "1"));
155 
156     /* update ID may be wrong here, it should be the one of the container?
157        TODO: We need to keep track of the overall updateID of the CDS */
158     NPT_CHECK_SEVERE(action->SetArgumentValue("UpdateId", "1"));
159 
160     return NPT_SUCCESS;
161 }
162 
163 /*----------------------------------------------------------------------
164 |   PLT_FileMediaServerDelegate::OnBrowseDirectChildren
165 +---------------------------------------------------------------------*/
166 NPT_Result
OnBrowseDirectChildren(PLT_ActionReference & action,const char * object_id,const char * filter,NPT_UInt32 starting_index,NPT_UInt32 requested_count,const char * sort_criteria,const PLT_HttpRequestContext & context)167 PLT_FileMediaServerDelegate::OnBrowseDirectChildren(PLT_ActionReference&          action,
168                                                     const char*                   object_id,
169                                                     const char*                   filter,
170                                                     NPT_UInt32                    starting_index,
171                                                     NPT_UInt32                    requested_count,
172                                                     const char*                   sort_criteria,
173                                                     const PLT_HttpRequestContext& context)
174 {
175     NPT_COMPILER_UNUSED(sort_criteria);
176 
177     /* locate the file from the object ID */
178     NPT_String dir;
179     NPT_FileInfo info;
180     if (NPT_FAILED(GetFilePath(object_id, dir)) || NPT_FAILED(NPT_File::GetInfo(dir, &info)) || info.m_Type != NPT_FileInfo::FILE_TYPE_DIRECTORY) {
181         /* error */
182         NPT_LOG_WARNING_1("ObjectID \'%s\' not found or not allowed", object_id);
183         action->SetError(701, "No such Object");
184         NPT_CHECK_WARNING(NPT_FAILURE);
185     }
186 
187     /* get uuid from device via action reference */
188     NPT_String uuid = action->GetActionDesc().GetService()->GetDevice()->GetUUID();
189 
190     /* Try to get list from cache if allowed */
191     NPT_Result res;
192     NPT_Reference<NPT_List<NPT_String> > entries;
193     NPT_TimeStamp cached_entries_time;
194     if (!m_UseCache || NPT_FAILED(m_DirCache.Get(uuid, dir, entries, &cached_entries_time)) || cached_entries_time < info.m_ModificationTime) {
195         /* if not found in cache or if current dir has newer modified time fetch fresh new list from source */
196         entries = new NPT_List<NPT_String>();
197         res = NPT_File::ListDir(dir, *entries);
198         if (NPT_FAILED(res)) {
199             NPT_LOG_WARNING_1("PLT_FileMediaServerDelegate::OnBrowseDirectChildren - failed to open dir %s", (const char*) dir);
200             NPT_CHECK_WARNING(res);
201         }
202 
203         /* sort results according to modification date */
204         res = entries->Sort(NPT_FileDateComparator(dir));
205         if (NPT_FAILED(res)) {
206             NPT_LOG_WARNING_1("PLT_FileMediaServerDelegate::OnBrowseDirectChildren - failed to open sort dir %s", (const char*) dir);
207             NPT_CHECK_WARNING(res);
208         }
209 
210         /* add new list to cache */
211         if (m_UseCache) {
212             m_DirCache.Put(uuid, dir, entries, &info.m_ModificationTime);
213         }
214     }
215 
216     unsigned long cur_index = 0;
217     unsigned long num_returned = 0;
218     unsigned long total_matches = 0;
219     NPT_String didl = didl_header;
220     bool allip = (NPT_String(filter).Find("ALLIP") != -1);
221 
222     PLT_MediaObjectReference item;
223     for (NPT_List<NPT_String>::Iterator it = entries->GetFirstItem();
224          it;
225          ++it) {
226         NPT_String filepath = NPT_FilePath::Create(dir, *it);
227 
228         /* verify we want to process this file first */
229         if (!ProcessFile(filepath, filter)) continue;
230 
231         /* build item object from file path */
232         item = BuildFromFilePath(filepath,
233                                  context,
234                                  true,
235                                  false,
236                                  allip);
237 
238         /* generate didl if within range requested */
239         if (!item.IsNull()) {
240             if ((cur_index >= starting_index) &&
241                 ((num_returned < requested_count) || (requested_count == 0))) {
242                 NPT_String tmp;
243                 NPT_CHECK_SEVERE(PLT_Didl::ToDidl(*item.AsPointer(), filter, tmp));
244 
245                 didl += tmp;
246                 ++num_returned;
247             }
248             ++cur_index;
249             ++total_matches;
250         }
251     };
252 
253     didl += didl_footer;
254 
255     NPT_LOG_FINE_6("BrowseDirectChildren from %s returning %d-%lu/%lu objects (%lu out of %d requested)",
256                    (const char*)context.GetLocalAddress().GetIpAddress().ToString(),
257                    starting_index, starting_index+num_returned, total_matches, num_returned, requested_count);
258 
259     NPT_CHECK_SEVERE(action->SetArgumentValue("Result", didl));
260     NPT_CHECK_SEVERE(action->SetArgumentValue("NumberReturned", NPT_String::FromInteger(num_returned)));
261     NPT_CHECK_SEVERE(action->SetArgumentValue("TotalMatches", NPT_String::FromInteger(total_matches))); // 0 means we don't know how many we have but most browsers don't like that!!
262     NPT_CHECK_SEVERE(action->SetArgumentValue("UpdateId", "1"));
263 
264     return NPT_SUCCESS;
265 }
266 
267 /*----------------------------------------------------------------------
268 |   PLT_FileMediaServerDelegate::OnSearchContainer
269 +---------------------------------------------------------------------*/
270 NPT_Result
OnSearchContainer(PLT_ActionReference & action,const char * object_id,const char * search_criteria,const char *,NPT_UInt32,NPT_UInt32,const char *,const PLT_HttpRequestContext &)271 PLT_FileMediaServerDelegate::OnSearchContainer(PLT_ActionReference&          action,
272                                                const char*                   object_id,
273                                                const char*                   search_criteria,
274                                                const char*                   /* filter */,
275                                                NPT_UInt32                    /* starting_index */,
276                                                NPT_UInt32                    /* requested_count */,
277                                                const char*                   /* sort_criteria */,
278                                                const PLT_HttpRequestContext& /* context */)
279 {
280     /* parse search criteria */
281 
282     /* TODO: HACK TO PASS DLNA */
283     if (search_criteria && NPT_StringsEqual(search_criteria, "Unknownfieldname")) {
284         /* error */
285         NPT_LOG_WARNING_1("Unsupported or invalid search criteria %s", search_criteria);
286         action->SetError(708, "Unsupported or invalid search criteria");
287         return NPT_FAILURE;
288     }
289 
290     /* locate the file from the object ID */
291     NPT_String dir;
292     if (NPT_FAILED(GetFilePath(object_id, dir))) {
293         /* error */
294         NPT_LOG_WARNING("ObjectID not found.");
295         action->SetError(710, "No Such Container.");
296         return NPT_FAILURE;
297     }
298 
299     /* retrieve the item type */
300     NPT_FileInfo info;
301     NPT_Result res = NPT_File::GetInfo(dir, &info);
302     if (NPT_FAILED(res) || (info.m_Type != NPT_FileInfo::FILE_TYPE_DIRECTORY)) {
303         /* error */
304         NPT_LOG_WARNING("No such container");
305         action->SetError(710, "No such container");
306         return NPT_FAILURE;
307     }
308 
309     return NPT_ERROR_NOT_IMPLEMENTED;
310 }
311 
312 /*----------------------------------------------------------------------
313 |   PLT_FileMediaServerDelegate::GetFilePath
314 +---------------------------------------------------------------------*/
315 NPT_Result
GetFilePath(const char * object_id,NPT_String & filepath)316 PLT_FileMediaServerDelegate::GetFilePath(const char* object_id,
317                                          NPT_String& filepath)
318 {
319     if (!object_id) return NPT_ERROR_INVALID_PARAMETERS;
320 
321     filepath = m_FileRoot;
322 
323     /* object id is formatted as 0/<filepath> */
324     if (NPT_StringLength(object_id) >= 1) {
325         filepath += (object_id + (object_id[0]=='0'?1:0));
326     }
327 
328     return NPT_SUCCESS;
329 }
330 
331 /*----------------------------------------------------------------------
332 |   PLT_FileMediaServerDelegate::BuildSafeResourceUri
333 +---------------------------------------------------------------------*/
334 NPT_String
BuildSafeResourceUri(const NPT_HttpUrl & base_uri,const char * host,const char * file_path)335 PLT_FileMediaServerDelegate::BuildSafeResourceUri(const NPT_HttpUrl& base_uri,
336                                                   const char*        host,
337                                                   const char*        file_path)
338 {
339     NPT_String result;
340     NPT_HttpUrl uri = base_uri;
341 
342     if (host) uri.SetHost(host);
343 
344     NPT_String uri_path = uri.GetPath();
345     if (!uri_path.EndsWith("/")) uri_path += "/";
346 
347 	/* some controllers (like WMP) will call us with an already urldecoded version.
348        We're intentionally prepending a known urlencoded string
349        to detect it when we receive the request urlencoded or already decoded to avoid double decoding*/
350     uri_path += "%/";
351     uri_path += file_path;
352 
353     /* set path */
354     uri.SetPath(uri_path);
355 
356     /* 360 hack: force inclusion of port in case it's 80 */
357     return uri.ToStringWithDefaultPort(0);
358 }
359 
360 /*----------------------------------------------------------------------
361 |   PLT_FileMediaServerDelegate::ExtractResourcePath
362 +---------------------------------------------------------------------*/
363 NPT_Result
ExtractResourcePath(const NPT_HttpUrl & url,NPT_String & file_path)364 PLT_FileMediaServerDelegate::ExtractResourcePath(const NPT_HttpUrl& url,
365                                                  NPT_String&        file_path)
366 {
367     /* Extract non decoded path, we need to autodetect urlencoding */
368     NPT_String uri_path = url.GetPath();
369     NPT_String url_root_encode = NPT_Uri::PercentEncode(m_UrlRoot, NPT_Uri::PathCharsToEncode);
370 
371     NPT_Ordinal skip = 0;
372     if (uri_path.StartsWith(m_UrlRoot)) {
373         skip = m_UrlRoot.GetLength();
374     } else if (uri_path.StartsWith(url_root_encode)) {
375         skip = url_root_encode.GetLength();
376     } else {
377         return NPT_FAILURE;
378     }
379 
380     /* account for extra slash */
381     skip += ((m_UrlRoot=="/")?0:1);
382     file_path = uri_path.SubString(skip);
383 
384     /* detect if client such as WMP sent a non urlencoded url */
385     if (file_path.StartsWith("%/")) {
386         NPT_LOG_FINE("Received a urldecoded version of our url!");
387         file_path.Erase(0, 2);
388     } else {
389         /* remove our prepended string we used to detect urldecoded version */
390         if (file_path.StartsWith("%25/")) file_path.Erase(0, 4);
391 
392         /* ok to urldecode */
393         file_path = NPT_Uri::PercentDecode(file_path);
394     }
395 
396     return NPT_SUCCESS;
397 }
398 
399 /*----------------------------------------------------------------------
400 |   PLT_FileMediaServerDelegate::BuildResourceUri
401 +---------------------------------------------------------------------*/
402 NPT_String
BuildResourceUri(const NPT_HttpUrl & base_uri,const char * host,const char * file_path)403 PLT_FileMediaServerDelegate::BuildResourceUri(const NPT_HttpUrl& base_uri,
404                                               const char*        host,
405                                               const char*        file_path)
406 {
407     return BuildSafeResourceUri(base_uri, host, file_path);
408 }
409 
410 /*----------------------------------------------------------------------
411 |   PLT_FileMediaServerDelegate::BuildFromFilePath
412 +---------------------------------------------------------------------*/
413 PLT_MediaObject*
BuildFromFilePath(const NPT_String & filepath,const PLT_HttpRequestContext & context,bool with_count,bool keep_extension_in_title,bool allip)414 PLT_FileMediaServerDelegate::BuildFromFilePath(const NPT_String&             filepath,
415                                                const PLT_HttpRequestContext& context,
416                                                bool                          with_count /* = true */,
417                                                bool                          keep_extension_in_title /* = false */,
418                                                bool                          allip /* = false */)
419 {
420     NPT_String            root = m_FileRoot;
421     PLT_MediaItemResource resource;
422     PLT_MediaObject*      object = NULL;
423 
424     NPT_LOG_FINEST_1("Building didl for file '%s'", (const char*)filepath);
425 
426     /* retrieve the entry type (directory or file) */
427     NPT_FileInfo info;
428     NPT_CHECK_LABEL_FATAL(NPT_File::GetInfo(filepath, &info), failure);
429 
430     if (info.m_Type == NPT_FileInfo::FILE_TYPE_REGULAR) {
431         object = new PLT_MediaItem();
432 
433         /* Set the title using the filename for now */
434         object->m_Title = NPT_FilePath::BaseName(filepath, keep_extension_in_title);
435         if (object->m_Title.GetLength() == 0) goto failure;
436 
437         /* make sure we return something with a valid mimetype */
438         if (m_FilterUnknownOut &&
439             NPT_StringsEqual(PLT_MimeType::GetMimeType(filepath, &context),
440                              "application/octet-stream")) {
441             goto failure;
442         }
443 
444         /* Set the protocol Info from the extension */
445         resource.m_ProtocolInfo = PLT_ProtocolInfo::GetProtocolInfo(filepath, true, &context);
446         if (!resource.m_ProtocolInfo.IsValid())  goto failure;
447 
448         /* Set the resource file size */
449         resource.m_Size = info.m_Size;
450 
451         /* format the resource URI */
452         NPT_String url = filepath.SubString(root.GetLength()+1);
453 
454         // get list of ip addresses
455         NPT_List<NPT_IpAddress> ips;
456         NPT_CHECK_LABEL_SEVERE(PLT_UPnPMessageHelper::GetIPAddresses(ips), failure);
457 
458         /* if we're passed an interface where we received the request from
459            move the ip to the top so that it is used for the first resource */
460         if (context.GetLocalAddress().GetIpAddress().ToString() != "0.0.0.0") {
461             ips.Remove(context.GetLocalAddress().GetIpAddress());
462             ips.Insert(ips.GetFirstItem(), context.GetLocalAddress().GetIpAddress());
463         } else if (!allip) {
464             NPT_LOG_WARNING("Couldn't determine local interface IP so we might return an unreachable IP");
465         }
466         object->m_ObjectClass.type = PLT_MediaItem::GetUPnPClass(filepath, &context);
467 
468         /* add as many resources as we have interfaces s*/
469         NPT_HttpUrl base_uri("127.0.0.1", context.GetLocalAddress().GetPort(), NPT_HttpUrl::PercentEncode(m_UrlRoot, NPT_Uri::PathCharsToEncode));
470         NPT_List<NPT_IpAddress>::Iterator ip = ips.GetFirstItem();
471         while (ip) {
472             resource.m_Uri = BuildResourceUri(base_uri, ip->ToString(), url);
473             object->m_Resources.Add(resource);
474             ++ip;
475 
476             /* if we only want the one resource reachable by client */
477             if (!allip) break;
478         }
479     } else {
480         object = new PLT_MediaContainer;
481 
482         /* Assign a title for this container */
483         if (filepath.Compare(root, true) == 0) {
484             object->m_Title = "Root";
485         } else {
486             object->m_Title = NPT_FilePath::BaseName(filepath, keep_extension_in_title);
487             if (object->m_Title.GetLength() == 0) goto failure;
488         }
489 
490         /* Get the number of children for this container */
491         NPT_LargeSize count = 0;
492         if (with_count && NPT_SUCCEEDED(NPT_File::GetSize(filepath, count))) {
493             ((PLT_MediaContainer*)object)->m_ChildrenCount = (NPT_Int32)count;
494         }
495 
496         object->m_ObjectClass.type = "object.container.storageFolder";
497     }
498 
499     /* is it the root? */
500     if (filepath.Compare(root, true) == 0) {
501         object->m_ParentID = "-1";
502         object->m_ObjectID = "0";
503     } else {
504         NPT_String directory = NPT_FilePath::DirName(filepath);
505         /* is the parent path the root? */
506         if (directory.GetLength() == root.GetLength()) {
507             object->m_ParentID = "0";
508         } else {
509             object->m_ParentID = "0" + filepath.SubString(root.GetLength(), directory.GetLength() - root.GetLength());
510         }
511         object->m_ObjectID = "0" + filepath.SubString(root.GetLength());
512     }
513 
514     return object;
515 
516 failure:
517     delete object;
518     return NULL;
519 }
520