Add a function doing a service rescan.
[profile/ivi/GSSDP.git] / libgssdp / gssdp-resource-browser.c
1 /* 
2  * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
3  *
4  * Author: Jorn Baayen <jorn@openedhand.com>
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Library General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Library General Public License for more details.
15  *
16  * You should have received a copy of the GNU Library General Public
17  * License along with this library; if not, write to the
18  * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19  * Boston, MA 02110-1301, USA.
20  */
21
22 /**
23  * SECTION:gssdp-resource-browser
24  * @short_description: Class handling resource discovery.
25  *
26  * #GSSDPResourceBrowser handles resource discovery. After creating a browser
27  * and activating it, the ::resource-available and ::resource-unavailable
28  * signals will be emitted whenever the availability of a resource matching the
29  * specified discovery target changes. A discovery request is sent out
30  * automatically when activating the browser.
31  */
32
33 #include <config.h>
34 #include <libsoup/soup.h>
35 #include <string.h>
36 #include <stdio.h>
37 #include <stdlib.h>
38
39 #include "gssdp-resource-browser.h"
40 #include "gssdp-client-private.h"
41 #include "gssdp-protocol.h"
42 #include "gssdp-marshal.h"
43
44 #define RESCAN_TIMEOUT 5 /* 5 seconds */
45 #define MAX_DISCOVERY_MESSAGES 3
46 #define DISCOVERY_FREQUENCY    500 /* 500 ms */
47
48 G_DEFINE_TYPE (GSSDPResourceBrowser,
49                gssdp_resource_browser,
50                G_TYPE_OBJECT);
51
52 struct _GSSDPResourceBrowserPrivate {
53         GSSDPClient *client;
54
55         char        *target;
56         GRegex      *target_regex;
57
58         gushort      mx;
59
60         gboolean     active;
61
62         gulong       message_received_id;
63
64         GHashTable  *resources;
65                         
66         GSource     *timeout_src;
67         guint        num_discovery;
68         guint        version;
69
70         GSource     *refresh_cache_src;
71         GHashTable  *fresh_resources;
72 };
73
74 enum {
75         PROP_0,
76         PROP_CLIENT,
77         PROP_TARGET,
78         PROP_MX,
79         PROP_ACTIVE
80 };
81
82 enum {
83         RESOURCE_AVAILABLE,
84         RESOURCE_UNAVAILABLE,
85         LAST_SIGNAL
86 };
87
88 static guint signals[LAST_SIGNAL];
89
90 typedef struct {
91         GSSDPResourceBrowser *resource_browser;
92         char                 *usn;
93         GSource              *timeout_src;
94 } Resource;
95
96 /* Function prototypes */
97 static void
98 gssdp_resource_browser_set_client (GSSDPResourceBrowser *resource_browser,
99                                    GSSDPClient          *client);
100 static void
101 message_received_cb              (GSSDPClient          *client,
102                                   const char           *from_ip,
103                                   gushort               from_port,
104                                   _GSSDPMessageType     type,
105                                   SoupMessageHeaders   *headers,
106                                   gpointer              user_data);
107 static void
108 resource_free                    (gpointer              data);
109 static void
110 clear_cache                      (GSSDPResourceBrowser *resource_browser);
111 static void
112 send_discovery_request            (GSSDPResourceBrowser *resource_browser);
113 static gboolean
114 discovery_timeout                (gpointer              data);
115 static void
116 start_discovery                  (GSSDPResourceBrowser *resource_browser);
117 static void
118 stop_discovery                   (GSSDPResourceBrowser *resource_browser);
119 static gboolean
120 refresh_cache                    (gpointer data);
121
122 static void
123 gssdp_resource_browser_init (GSSDPResourceBrowser *resource_browser)
124 {
125         resource_browser->priv = G_TYPE_INSTANCE_GET_PRIVATE
126                                         (resource_browser,
127                                          GSSDP_TYPE_RESOURCE_BROWSER,
128                                          GSSDPResourceBrowserPrivate);
129
130         resource_browser->priv->mx = SSDP_DEFAULT_MX;
131
132         resource_browser->priv->resources =
133                 g_hash_table_new_full (g_str_hash,
134                                        g_str_equal,
135                                        g_free,
136                                        resource_free);
137 }
138
139 static void
140 gssdp_resource_browser_get_property (GObject    *object,
141                                      guint       property_id,
142                                      GValue     *value,
143                                      GParamSpec *pspec)
144 {
145         GSSDPResourceBrowser *resource_browser;
146
147         resource_browser = GSSDP_RESOURCE_BROWSER (object);
148
149         switch (property_id) {
150         case PROP_CLIENT:
151                 g_value_set_object
152                         (value,
153                          gssdp_resource_browser_get_client (resource_browser));
154                 break;
155         case PROP_TARGET:
156                 g_value_set_string
157                         (value,
158                          gssdp_resource_browser_get_target (resource_browser));
159                 break;
160         case PROP_MX:
161                 g_value_set_uint
162                         (value,
163                          gssdp_resource_browser_get_mx (resource_browser));
164                 break;
165         case PROP_ACTIVE:
166                 g_value_set_boolean
167                         (value,
168                          gssdp_resource_browser_get_active (resource_browser));
169                 break;
170         default:
171                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
172                 break;
173         }
174 }
175
176 static void
177 gssdp_resource_browser_set_property (GObject      *object,
178                                      guint         property_id,
179                                      const GValue *value,
180                                      GParamSpec   *pspec)
181 {
182         GSSDPResourceBrowser *resource_browser;
183
184         resource_browser = GSSDP_RESOURCE_BROWSER (object);
185
186         switch (property_id) {
187         case PROP_CLIENT:
188                 gssdp_resource_browser_set_client (resource_browser,
189                                                    g_value_get_object (value));
190                 break;
191         case PROP_TARGET:
192                 gssdp_resource_browser_set_target (resource_browser,
193                                                    g_value_get_string (value));
194                 break;
195         case PROP_MX:
196                 gssdp_resource_browser_set_mx (resource_browser,
197                                                g_value_get_uint (value));
198                 break;
199         case PROP_ACTIVE:
200                 gssdp_resource_browser_set_active (resource_browser,
201                                                    g_value_get_boolean (value));
202                 break;
203         default:
204                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
205                 break;
206         }
207 }
208
209 static void
210 gssdp_resource_browser_dispose (GObject *object)
211 {
212         GSSDPResourceBrowser *resource_browser;
213
214         resource_browser = GSSDP_RESOURCE_BROWSER (object);
215
216         if (resource_browser->priv->client) {
217                 if (g_signal_handler_is_connected
218                         (resource_browser->priv->client,
219                          resource_browser->priv->message_received_id)) {
220                         g_signal_handler_disconnect
221                                 (resource_browser->priv->client,
222                                  resource_browser->priv->message_received_id);
223                 }
224
225                 stop_discovery (resource_browser);
226
227                 g_object_unref (resource_browser->priv->client);
228                 resource_browser->priv->client = NULL;
229         }
230
231         clear_cache (resource_browser);
232
233         G_OBJECT_CLASS (gssdp_resource_browser_parent_class)->dispose (object);
234 }
235
236 static void
237 gssdp_resource_browser_finalize (GObject *object)
238 {
239         GSSDPResourceBrowser *resource_browser;
240
241         resource_browser = GSSDP_RESOURCE_BROWSER (object);
242
243         if (resource_browser->priv->target_regex)
244                 g_regex_unref (resource_browser->priv->target_regex);
245
246         g_free (resource_browser->priv->target);
247
248         g_hash_table_destroy (resource_browser->priv->resources);
249
250         G_OBJECT_CLASS (gssdp_resource_browser_parent_class)->finalize (object);
251 }
252
253 static void
254 gssdp_resource_browser_class_init (GSSDPResourceBrowserClass *klass)
255 {
256         GObjectClass *object_class;
257
258         object_class = G_OBJECT_CLASS (klass);
259
260         object_class->set_property = gssdp_resource_browser_set_property;
261         object_class->get_property = gssdp_resource_browser_get_property;
262         object_class->dispose      = gssdp_resource_browser_dispose;
263         object_class->finalize     = gssdp_resource_browser_finalize;
264
265         g_type_class_add_private (klass, sizeof (GSSDPResourceBrowserPrivate));
266
267         /**
268          * GSSDPResourceBrowser:client:
269          *
270          * The #GSSDPClient to use.
271          **/
272         g_object_class_install_property
273                 (object_class,
274                  PROP_CLIENT,
275                  g_param_spec_object
276                          ("client",
277                           "Client",
278                           "The associated client.",
279                           GSSDP_TYPE_CLIENT,
280                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
281                           G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
282                           G_PARAM_STATIC_BLURB));
283
284         /**
285          * GSSDPResourceBrowser:target:
286          *
287          * The discovery target.
288          **/
289         g_object_class_install_property
290                 (object_class,
291                  PROP_TARGET,
292                  g_param_spec_string
293                          ("target",
294                           "Target",
295                           "The discovery target.",
296                           NULL,
297                           G_PARAM_READWRITE |
298                           G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
299                           G_PARAM_STATIC_BLURB));
300
301         /**
302          * GSSDPResourceBrowser:mx:
303          *
304          * The maximum number of seconds in which to request other parties
305          * to respond.
306          **/
307         g_object_class_install_property
308                 (object_class,
309                  PROP_MX,
310                  g_param_spec_uint
311                          ("mx",
312                           "MX",
313                           "The maximum number of seconds in which to request "
314                           "other parties to respond.",
315                           1,
316                           G_MAXUSHORT,
317                           SSDP_DEFAULT_MX,
318                           G_PARAM_READWRITE |
319                           G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
320                           G_PARAM_STATIC_BLURB));
321
322         /**
323          * GSSDPResourceBrowser:active:
324          *
325          * Whether this browser is active or not.
326          **/
327         g_object_class_install_property
328                 (object_class,
329                  PROP_ACTIVE,
330                  g_param_spec_boolean
331                          ("active",
332                           "Active",
333                           "TRUE if the resource browser is active.",
334                           FALSE,
335                           G_PARAM_READWRITE |
336                           G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
337                           G_PARAM_STATIC_BLURB));
338
339         /**
340          * GSSDPResourceBrowser::resource-available:
341          * @resource_browser: The #GSSDPResourceBrowser that received the
342          * signal
343          * @usn: The USN of the discovered resource
344          * @locations: (type GList*) (transfer none) (element-type utf8): A #GList of strings describing the locations of the
345          * discovered resource.
346          *
347          * The ::resource-available signal is emitted whenever a new resource
348          * has become available.
349          **/
350         signals[RESOURCE_AVAILABLE] =
351                 g_signal_new ("resource-available",
352                               GSSDP_TYPE_RESOURCE_BROWSER,
353                               G_SIGNAL_RUN_LAST,
354                               G_STRUCT_OFFSET (GSSDPResourceBrowserClass,
355                                                resource_available),
356                               NULL, NULL,
357                               gssdp_marshal_VOID__STRING_POINTER,
358                               G_TYPE_NONE,
359                               2,
360                               G_TYPE_STRING,
361                               G_TYPE_POINTER);
362
363         /**
364          * GSSDPResourceBrowser::resource-unavailable:
365          * @resource_browser: The #GSSDPResourceBrowser that received the
366          * signal
367          * @usn: The USN of the resource
368          *
369          * The ::resource-unavailable signal is emitted whenever a resource
370          * is not available any more.
371          **/
372         signals[RESOURCE_UNAVAILABLE] =
373                 g_signal_new ("resource-unavailable",
374                               GSSDP_TYPE_RESOURCE_BROWSER,
375                               G_SIGNAL_RUN_LAST,
376                               G_STRUCT_OFFSET (GSSDPResourceBrowserClass,
377                                                resource_unavailable),
378                               NULL, NULL,
379                               gssdp_marshal_VOID__STRING,
380                               G_TYPE_NONE,
381                               1,
382                               G_TYPE_STRING);
383 }
384
385 /**
386  * gssdp_resource_browser_new:
387  * @client: The #GSSDPClient to associate with
388  *
389  * Return value: A new #GSSDPResourceBrowser object.
390  **/
391 GSSDPResourceBrowser *
392 gssdp_resource_browser_new (GSSDPClient *client,
393                             const char  *target)
394 {
395         return g_object_new (GSSDP_TYPE_RESOURCE_BROWSER,
396                              "client", client,
397                              "target", target,
398                              NULL);
399 }
400
401 /*
402  * Sets the #GSSDPClient @resource_browser is associated with to @client
403  */
404 static void
405 gssdp_resource_browser_set_client (GSSDPResourceBrowser *resource_browser,
406                                    GSSDPClient          *client)
407 {
408         g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
409         g_return_if_fail (GSSDP_IS_CLIENT (client));
410
411         resource_browser->priv->client = g_object_ref (client);
412
413         resource_browser->priv->message_received_id =
414                 g_signal_connect_object (resource_browser->priv->client,
415                                          "message-received",
416                                          G_CALLBACK (message_received_cb),
417                                          resource_browser,
418                                          0);
419
420         g_object_notify (G_OBJECT (resource_browser), "client");
421 }
422
423 /**
424  * gssdp_resource_browser_get_client:
425  * @resource_browser: A #GSSDPResourceBrowser
426  *
427  * Returns: (transfer none): The #GSSDPClient @resource_browser is associated with.
428  **/
429 GSSDPClient *
430 gssdp_resource_browser_get_client (GSSDPResourceBrowser *resource_browser)
431 {
432         g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser),
433                               NULL);
434
435         return resource_browser->priv->client;
436 }
437
438 /**
439  * gssdp_resource_browser_set_target:
440  * @resource_browser: A #GSSDPResourceBrowser
441  * @target: The browser target
442  *
443  * Sets the browser target of @resource_browser to @target.
444  **/
445 void
446 gssdp_resource_browser_set_target (GSSDPResourceBrowser *resource_browser,
447                                    const char           *target)
448 {
449         char *pattern;
450         char *version;
451         char *version_pattern;
452         GError *error;
453
454         g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
455         g_return_if_fail (target != NULL);
456         g_return_if_fail (!resource_browser->priv->active);
457         
458         g_free (resource_browser->priv->target);
459         resource_browser->priv->target = g_strdup (target);
460
461         if (resource_browser->priv->target_regex)
462                 g_regex_unref (resource_browser->priv->target_regex);
463
464         version_pattern = "([0-9]+)";
465         /* Make sure we have enough room for version pattern */
466         pattern = g_strndup (target,
467                              strlen (target) + strlen (version_pattern));
468
469         version = g_strrstr (pattern, ":");
470         if (version != NULL &&
471             (g_strstr_len (pattern, -1, "uuid:") != pattern ||
472              version != g_strstr_len (pattern, -1, ":")) &&
473             g_regex_match_simple (version_pattern,
474                                   version + 1,
475                                   G_REGEX_ANCHORED,
476                                   G_REGEX_MATCH_ANCHORED)) {
477                 resource_browser->priv->version = atoi (version + 1);
478                 strcpy (version + 1, version_pattern);
479         }
480
481         error = NULL;
482         resource_browser->priv->target_regex = g_regex_new (pattern,
483                                                             0,
484                                                             0,
485                                                             &error);
486         if (error) {
487                 g_warning ("Error compiling regular expression '%s': %s",
488                            pattern,
489                            error->message);
490
491                 g_error_free (error);
492         }
493
494         g_free (pattern);
495         g_object_notify (G_OBJECT (resource_browser), "target");
496 }
497
498 /**
499  * gssdp_resource_browser_get_target:
500  * @resource_browser: A #GSSDPResourceBrowser
501  *
502  * Return value: The browser target.
503  **/
504 const char *
505 gssdp_resource_browser_get_target (GSSDPResourceBrowser *resource_browser)
506 {
507         g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser),
508                               NULL);
509
510         return resource_browser->priv->target;
511 }
512
513 /**
514  * gssdp_resource_browser_set_mx:
515  * @resource_browser: A #GSSDPResourceBrowser
516  * @mx: The to be used MX value
517  *
518  * Sets the used MX value of @resource_browser to @mx.
519  **/
520 void
521 gssdp_resource_browser_set_mx (GSSDPResourceBrowser *resource_browser,
522                                gushort               mx)
523 {
524         g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
525
526         if (resource_browser->priv->mx == mx)
527                 return;
528
529         resource_browser->priv->mx = mx;
530         
531         g_object_notify (G_OBJECT (resource_browser), "mx");
532 }
533
534 /**
535  * gssdp_resource_browser_get_mx:
536  * @resource_browser: A #GSSDPResourceBrowser
537  *
538  * Return value: The used MX value.
539  **/
540 gushort
541 gssdp_resource_browser_get_mx (GSSDPResourceBrowser *resource_browser)
542 {
543         g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser), 0);
544
545         return resource_browser->priv->mx;
546 }
547
548 /**
549  * gssdp_resource_browser_set_active:
550  * @resource_browser: A #GSSDPResourceBrowser
551  * @active: TRUE to activate @resource_browser
552  *
553  * (De)activates @resource_browser.
554  **/
555 void
556 gssdp_resource_browser_set_active (GSSDPResourceBrowser *resource_browser,
557                                    gboolean              active)
558 {
559         g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
560
561         if (resource_browser->priv->active == active)
562                 return;
563
564         resource_browser->priv->active = active;
565
566         if (active) {
567                 start_discovery (resource_browser);
568         } else {
569                 stop_discovery (resource_browser);
570
571                 clear_cache (resource_browser);
572         }
573         
574         g_object_notify (G_OBJECT (resource_browser), "active");
575 }
576
577 /**
578  * gssdp_resource_browser_get_active:
579  * @resource_browser: A #GSSDPResourceBrowser
580  *
581  * Return value: TRUE if @resource_browser is active.
582  **/
583 gboolean
584 gssdp_resource_browser_get_active (GSSDPResourceBrowser *resource_browser)
585 {
586         g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser), 0);
587
588         return resource_browser->priv->active;
589 }
590
591 /**
592  * gssdp_resource_browser_rescan:
593  * @resource_browser: A #GSSDPResourceBrowser
594  *
595  * Begins discovery if @resource_browser is active and no discovery is
596  * performed. Otherwise does nothing.
597  *
598  * Return value: %TRUE if rescaning has been started.
599  **/
600 gboolean
601 gssdp_resource_browser_rescan (GSSDPResourceBrowser *resource_browser)
602 {
603         g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser), 0);
604
605         if (resource_browser->priv->active &&
606             resource_browser->priv->timeout_src == NULL &&
607             resource_browser->priv->refresh_cache_src == NULL) {
608                 start_discovery (resource_browser);
609                 return TRUE;
610         }
611
612         return FALSE;
613 }
614
615 /*
616  * Resource expired: Remove
617  */
618 static gboolean
619 resource_expire (gpointer user_data)
620 {
621         GSSDPResourceBrowser *resource_browser;
622         Resource *resource;
623         char *usn;
624         char *canonical_usn;
625
626         resource = user_data;
627         resource_browser = resource->resource_browser;
628
629         /* Steal the USN pointer from the resource as we need it for the signal
630          * emission.
631          */
632         usn = resource->usn;
633         resource->usn = NULL;
634
635         if (resource_browser->priv->version > 0) {
636                 char *version;
637
638                 version = g_strrstr (usn, ":");
639                 canonical_usn = g_strndup (usn, version - usn);
640         } else {
641                 canonical_usn = g_strdup (usn);
642         }
643
644         g_hash_table_remove (resource->resource_browser->priv->resources,
645                              canonical_usn);
646
647         g_signal_emit (resource_browser,
648                        signals[RESOURCE_UNAVAILABLE],
649                        0,
650                        usn);
651         g_free (usn);
652         g_free (canonical_usn);
653
654         return FALSE;
655 }
656
657 static void
658 resource_available (GSSDPResourceBrowser *resource_browser,
659                     SoupMessageHeaders   *headers)
660 {
661         const char *usn;
662         const char *header;
663         Resource *resource;
664         gboolean was_cached;
665         guint timeout;
666         GList *locations;
667         char *canonical_usn;
668
669         usn = soup_message_headers_get_one (headers, "USN");
670         if (!usn)
671                 return; /* No USN specified */
672
673         if (resource_browser->priv->version > 0) {
674                 char *version;
675
676                 version = g_strrstr (usn, ":");
677                 canonical_usn = g_strndup (usn, version - usn);
678         } else {
679                 canonical_usn = g_strdup (usn);
680         }
681
682         /* Get from cache, if possible */
683         resource = g_hash_table_lookup (resource_browser->priv->resources,
684                                         canonical_usn);
685         /* Put usn into fresh resources, so this resource will not be
686          * removed on cache refreshing. */
687         if (resource_browser->priv->fresh_resources != NULL) {
688                 char *usn_copy = g_strdup (canonical_usn);
689
690                 g_hash_table_insert (resource_browser->priv->fresh_resources,
691                                      usn_copy,
692                                      usn_copy);
693         }
694
695         if (resource) {
696                 /* Remove old timeout */
697                 g_source_destroy (resource->timeout_src);
698
699                 was_cached = TRUE;
700         } else {
701                 /* Create new Resource data structure */
702                 resource = g_slice_new (Resource);
703
704                 resource->resource_browser = resource_browser;
705                 resource->usn              = g_strdup (usn);
706                 
707                 g_hash_table_insert (resource_browser->priv->resources,
708                                      canonical_usn,
709                                      resource);
710                 
711                 was_cached = FALSE;
712
713                 /* hash-table takes ownership of this */
714                 canonical_usn = NULL;
715         }
716
717         if (canonical_usn != NULL)
718                 g_free (canonical_usn);
719
720         /* Calculate new timeout */
721         header = soup_message_headers_get_one (headers, "Cache-Control");
722         if (header) {
723                 GSList *list;
724                 int res;
725
726                 res = 0;
727
728                 for (list = soup_header_parse_list (header);
729                      list;
730                      list = list->next) {
731                         res = sscanf (list->data,
732                                       "max-age = %d",
733                                       &timeout);
734                         if (res == 1)
735                                 break;
736                 }
737
738                 if (res != 1) {
739                         g_warning ("Invalid 'Cache-Control' header. Assuming "
740                                    "default max-age of %d.\n"
741                                    "Header was:\n%s",
742                                    SSDP_DEFAULT_MAX_AGE,
743                                    header);
744
745                         timeout = SSDP_DEFAULT_MAX_AGE;
746                 }
747
748                 soup_header_free_list (list);
749         } else {
750                 const char *expires;
751
752                 expires = soup_message_headers_get_one (headers, "Expires");
753                 if (expires) {
754                         SoupDate *soup_exp_time;
755                         time_t exp_time, cur_time;
756
757                         soup_exp_time = soup_date_new_from_string (expires);
758                         exp_time = soup_date_to_time_t (soup_exp_time);
759                         soup_date_free (soup_exp_time);
760
761                         cur_time = time (NULL);
762
763                         if (exp_time > cur_time)
764                                 timeout = exp_time - cur_time;
765                         else {
766                                 g_warning ("Invalid 'Expires' header. Assuming "
767                                            "default max-age of %d.\n"
768                                            "Header was:\n%s",
769                                            SSDP_DEFAULT_MAX_AGE,
770                                            expires);
771
772                                 timeout = SSDP_DEFAULT_MAX_AGE;
773                         }
774                 } else {
775                         g_warning ("No 'Cache-Control' nor any 'Expires' "
776                                    "header was specified. Assuming default "
777                                    "max-age of %d.", SSDP_DEFAULT_MAX_AGE);
778
779                         timeout = SSDP_DEFAULT_MAX_AGE;
780                 }
781         }
782
783         resource->timeout_src = g_timeout_source_new_seconds (timeout);
784         g_source_set_callback (resource->timeout_src,
785                                resource_expire,
786                                resource, NULL);
787
788         g_source_attach (resource->timeout_src,
789                          g_main_context_get_thread_default ());
790
791         g_source_unref (resource->timeout_src);
792
793         /* Only continue with signal emission if this resource was not
794          * cached already */
795         if (was_cached)
796                 return;
797
798         /* Build list of locations */
799         locations = NULL;
800
801         header = soup_message_headers_get_one (headers, "Location");
802         if (header)
803                 locations = g_list_append (locations, g_strdup (header));
804
805         header = soup_message_headers_get_one (headers, "AL");
806         if (header) {
807                 /* Parse AL header. The format is:
808                  * <uri1><uri2>... */
809                 const char *start, *end;
810                 char *uri;
811                 
812                 start = header;
813                 while ((start = strchr (start, '<'))) {
814                         start += 1;
815                         if (!start || !*start)
816                                 break;
817
818                         end = strchr (start, '>');
819                         if (!end || !*end)
820                                 break;
821
822                         uri = g_strndup (start, end - start);
823                         locations = g_list_append (locations, uri);
824
825                         start = end;
826                 }
827         }
828
829         /* Emit signal */
830         g_signal_emit (resource_browser,
831                        signals[RESOURCE_AVAILABLE],
832                        0,
833                        usn,
834                        locations);
835
836         /* Cleanup */
837         while (locations) {
838                 g_free (locations->data);
839
840                 locations = g_list_delete_link (locations, locations);
841         }
842 }
843
844 static void
845 resource_unavailable (GSSDPResourceBrowser *resource_browser,
846                       SoupMessageHeaders   *headers)
847 {
848         const char *usn;
849         char *canonical_usn;
850
851         usn = soup_message_headers_get_one (headers, "USN");
852         if (!usn)
853                 return; /* No USN specified */
854
855         if (resource_browser->priv->version > 0) {
856                 char *version;
857
858                 version = g_strrstr (usn, ":");
859                 canonical_usn = g_strndup (usn, version - usn);
860         } else {
861                 canonical_usn = g_strdup (usn);
862         }
863
864         /* Only process if we were cached */
865         if (!g_hash_table_lookup (resource_browser->priv->resources,
866                                   canonical_usn))
867                 goto out;
868
869         g_hash_table_remove (resource_browser->priv->resources,
870                              canonical_usn);
871
872         g_signal_emit (resource_browser,
873                        signals[RESOURCE_UNAVAILABLE],
874                        0,
875                        usn);
876
877 out:
878         g_free (canonical_usn);
879 }
880
881 static gboolean
882 check_target_compat (GSSDPResourceBrowser *resource_browser,
883                      const char           *st)
884 {
885         GMatchInfo *info;
886         int         version;
887         char       *tmp;
888
889         if (strcmp (resource_browser->priv->target,
890                     GSSDP_ALL_RESOURCES) == 0)
891                 return TRUE;
892
893         if (!g_regex_match (resource_browser->priv->target_regex,
894                             st,
895                             0,
896                             &info)) {
897                 g_match_info_free (info);
898
899                 return FALSE;
900         }
901
902         /* If there was no version to match, we're done */
903         if (resource_browser->priv->version == 0) {
904                 g_match_info_free (info);
905
906                 return TRUE;
907         }
908
909         if (g_match_info_get_match_count (info) != 2) {
910                 g_match_info_free (info);
911
912                 return FALSE;
913         }
914
915         version = atoi ((tmp = g_match_info_fetch (info, 1)));
916         g_free (tmp);
917         g_match_info_free (info);
918
919         return version >= resource_browser->priv->version;
920 }
921
922 static void
923 received_discovery_response (GSSDPResourceBrowser *resource_browser,
924                              SoupMessageHeaders   *headers)
925 {
926         const char *st;
927
928         st = soup_message_headers_get_one (headers, "ST");
929         if (!st)
930                 return; /* No target specified */
931
932         if (!check_target_compat (resource_browser, st))
933                 return; /* Target doesn't match */
934
935         resource_available (resource_browser, headers);
936 }
937
938 static void
939 received_announcement (GSSDPResourceBrowser *resource_browser,
940                        SoupMessageHeaders   *headers)
941 {
942         const char *header;
943
944         header = soup_message_headers_get_one (headers, "NT");
945         if (!header)
946                 return; /* No target specified */
947
948         if (!check_target_compat (resource_browser, header))
949                 return; /* Target doesn't match */
950
951         header = soup_message_headers_get_one (headers, "NTS");
952         if (!header)
953                 return; /* No announcement type specified */
954
955         /* Check announcement type */
956         if      (strncmp (header,
957                           SSDP_ALIVE_NTS,
958                           strlen (SSDP_ALIVE_NTS)) == 0)
959                 resource_available (resource_browser, headers);
960         else if (strncmp (header,
961                           SSDP_BYEBYE_NTS,
962                           strlen (SSDP_BYEBYE_NTS)) == 0)
963                 resource_unavailable (resource_browser, headers);
964 }
965
966 /*
967  * Received a message
968  */
969 static void
970 message_received_cb (GSSDPClient        *client,
971                      const char         *from_ip,
972                      gushort             from_port,
973                      _GSSDPMessageType   type,
974                      SoupMessageHeaders *headers,
975                      gpointer            user_data)
976 {
977         GSSDPResourceBrowser *resource_browser;
978
979         resource_browser = GSSDP_RESOURCE_BROWSER (user_data);
980
981         if (!resource_browser->priv->active)
982                 return;
983
984         switch (type) {
985         case _GSSDP_DISCOVERY_RESPONSE:
986                 received_discovery_response (resource_browser, headers);
987                 break;
988         case _GSSDP_ANNOUNCEMENT:
989                 received_announcement (resource_browser, headers);
990                 break;
991         default:
992                 break;
993         }
994 }
995
996 /*
997  * Free a Resource structure and its contained data
998  */
999 static void
1000 resource_free (gpointer data)
1001 {
1002         Resource *resource;
1003
1004         resource = data;
1005
1006         g_free (resource->usn);
1007
1008         g_source_destroy (resource->timeout_src);
1009
1010         g_slice_free (Resource, resource);
1011 }
1012
1013 static gboolean
1014 clear_cache_helper (gpointer key, gpointer value, gpointer data)
1015 {
1016         Resource *resource;
1017
1018         resource = value;
1019
1020         g_signal_emit (resource->resource_browser,
1021                        signals[RESOURCE_UNAVAILABLE],
1022                        0,
1023                        resource->usn);
1024
1025         return TRUE;
1026 }
1027
1028 /*
1029  * Clears the cached resources hash
1030  */
1031 static void
1032 clear_cache (GSSDPResourceBrowser *resource_browser)
1033 {
1034         /* Clear cache */
1035         g_hash_table_foreach_remove (resource_browser->priv->resources,
1036                                      clear_cache_helper,
1037                                      NULL);
1038 }
1039
1040 /* Sends discovery request */
1041 static void
1042 send_discovery_request (GSSDPResourceBrowser *resource_browser)
1043 {
1044         char *message;
1045
1046         message = g_strdup_printf (SSDP_DISCOVERY_REQUEST,
1047                                    resource_browser->priv->target,
1048                                    resource_browser->priv->mx,
1049                                    g_get_application_name () ?: "");
1050
1051         _gssdp_client_send_message (resource_browser->priv->client,
1052                                     NULL,
1053                                     0,
1054                                     message,
1055                                     _GSSDP_DISCOVERY_REQUEST);
1056
1057         g_free (message);
1058 }
1059
1060 static gboolean
1061 discovery_timeout (gpointer data)
1062 {
1063         GSSDPResourceBrowser *resource_browser;
1064
1065         resource_browser = GSSDP_RESOURCE_BROWSER (data);
1066
1067         send_discovery_request (resource_browser);
1068
1069         resource_browser->priv->num_discovery += 1;
1070
1071         if (resource_browser->priv->num_discovery >= MAX_DISCOVERY_MESSAGES) {
1072                 resource_browser->priv->timeout_src = NULL;
1073                 resource_browser->priv->num_discovery = 0;
1074
1075                 /* Setup cache refreshing */
1076                 resource_browser->priv->refresh_cache_src =
1077                                   g_timeout_source_new_seconds (RESCAN_TIMEOUT);
1078                 g_source_set_callback
1079                                      (resource_browser->priv->refresh_cache_src,
1080                                       refresh_cache,
1081                                       resource_browser,
1082                                       NULL);
1083                 g_source_attach (resource_browser->priv->refresh_cache_src,
1084                                  g_main_context_get_thread_default ());
1085                 g_source_unref (resource_browser->priv->refresh_cache_src);
1086
1087                 return FALSE;
1088         } else
1089                 return TRUE;
1090 }
1091
1092 /* Starts sending discovery requests */
1093 static void
1094 start_discovery (GSSDPResourceBrowser *resource_browser)
1095 {
1096         /* Send one now */
1097         send_discovery_request (resource_browser);
1098
1099         /* And schedule the rest for later */
1100         resource_browser->priv->num_discovery = 1;
1101         resource_browser->priv->timeout_src =
1102                 g_timeout_source_new (DISCOVERY_FREQUENCY);
1103         g_source_set_callback (resource_browser->priv->timeout_src,
1104                                discovery_timeout,
1105                                resource_browser, NULL);
1106
1107         g_source_attach (resource_browser->priv->timeout_src,
1108                          g_main_context_get_thread_default ());
1109
1110         g_source_unref (resource_browser->priv->timeout_src);
1111
1112         /* Setup a set of responsive resources for cache refreshing */
1113         resource_browser->priv->fresh_resources = g_hash_table_new_full
1114                                         (g_str_hash,
1115                                          g_str_equal,
1116                                          g_free,
1117                                          NULL);
1118 }
1119
1120 /* Stops the sending of discovery messages */
1121 static void
1122 stop_discovery (GSSDPResourceBrowser *resource_browser)
1123 {
1124         if (resource_browser->priv->timeout_src) {
1125                 g_source_destroy (resource_browser->priv->timeout_src);
1126                 resource_browser->priv->timeout_src = NULL;
1127                 resource_browser->priv->num_discovery = 0;
1128         }
1129         if (resource_browser->priv->refresh_cache_src) {
1130                 g_source_destroy (resource_browser->priv->refresh_cache_src);
1131                 resource_browser->priv->refresh_cache_src = NULL;
1132         }
1133         if (resource_browser->priv->fresh_resources) {
1134                 g_hash_table_unref (resource_browser->priv->fresh_resources);
1135                 resource_browser->priv->fresh_resources = NULL;
1136         }
1137 }
1138
1139 static gboolean
1140 refresh_cache_helper (gpointer key, gpointer value, gpointer data)
1141 {
1142         Resource *resource;
1143         GHashTable *fresh_resources;
1144
1145         resource = value;
1146         fresh_resources = data;
1147
1148         if (g_hash_table_lookup_extended (fresh_resources, key, NULL, NULL))
1149                 return FALSE;
1150         else {
1151                 g_signal_emit (resource->resource_browser,
1152                                signals[RESOURCE_UNAVAILABLE],
1153                                0,
1154                                resource->usn);
1155
1156                 return TRUE;
1157         }
1158 }
1159
1160 /* Removes non-responsive resources */
1161 static gboolean
1162 refresh_cache (gpointer data)
1163 {
1164         GSSDPResourceBrowser *resource_browser;
1165
1166         resource_browser = GSSDP_RESOURCE_BROWSER (data);
1167         g_hash_table_foreach_remove (resource_browser->priv->resources,
1168                                      refresh_cache_helper,
1169                                      resource_browser->priv->fresh_resources);
1170         g_hash_table_unref (resource_browser->priv->fresh_resources);
1171         resource_browser->priv->fresh_resources = NULL;
1172         resource_browser->priv->refresh_cache_src = NULL;
1173
1174         return FALSE;
1175 }