Merge "res_pjsip_caller_id: Add "party" parameter to RPID header."
[asterisk/asterisk.git] / res / res_http_media_cache.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2015, Matt Jordan
5  *
6  * Matt Jordan <mjordan@digium.com>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*!
20  * \file
21  * \brief
22  *
23  * \author \verbatim Matt Jordan <mjordan@digium.com> \endverbatim
24  *
25  * HTTP backend for the core media cache
26  */
27
28 /*** MODULEINFO
29         <depend>curl</depend>
30         <depend>res_curl</depend>
31         <support_level>core</support_level>
32  ***/
33
34 #include "asterisk.h"
35
36 #include <curl/curl.h>
37
38 #include "asterisk/module.h"
39 #include "asterisk/bucket.h"
40 #include "asterisk/sorcery.h"
41 #include "asterisk/threadstorage.h"
42
43 #define GLOBAL_USERAGENT "asterisk-libcurl-agent/1.0"
44
45 #define MAX_HEADER_LENGTH 1023
46
47 /*! \brief Data passed to cURL callbacks */
48 struct curl_bucket_file_data {
49         /*! The \c ast_bucket_file object that caused the operation */
50         struct ast_bucket_file *bucket_file;
51         /*! File to write data to */
52         FILE *out_file;
53 };
54
55 /*!
56  * \internal \brief The cURL header callback function
57  */
58 static size_t curl_header_callback(char *buffer, size_t size, size_t nitems, void *data)
59 {
60         struct curl_bucket_file_data *cb_data = data;
61         size_t realsize;
62         char *value;
63         char *header;
64
65         realsize = size * nitems;
66
67         if (realsize > MAX_HEADER_LENGTH) {
68                 ast_log(LOG_WARNING, "cURL header length of '%zu' is too large: max %d\n",
69                         realsize, MAX_HEADER_LENGTH);
70                 return 0;
71         }
72
73         /* buffer may not be NULL terminated */
74         header = ast_alloca(realsize + 1);
75         memcpy(header, buffer, realsize);
76         header[realsize] = '\0';
77         value = strchr(header, ':');
78         if (!value) {
79                 /* Not a header we care about; bail */
80                 return realsize;
81         }
82         *value++ = '\0';
83
84         if (strcasecmp(header, "ETag")
85                 && strcasecmp(header, "Cache-Control")
86                 && strcasecmp(header, "Last-Modified")
87                 && strcasecmp(header, "Content-Type")
88                 && strcasecmp(header, "Expires")) {
89                 return realsize;
90         }
91
92         value = ast_trim_blanks(ast_skip_blanks(value));
93         header = ast_str_to_lower(header);
94
95         ast_bucket_file_metadata_set(cb_data->bucket_file, header, value);
96
97         return realsize;
98 }
99
100 /*!
101  * \internal \brief The cURL body callback function
102  */
103 static size_t curl_body_callback(void *ptr, size_t size, size_t nitems, void *data)
104 {
105         struct curl_bucket_file_data *cb_data = data;
106         size_t realsize;
107
108         realsize = fwrite(ptr, size, nitems, cb_data->out_file);
109
110         return realsize;
111 }
112
113 /*!
114  * \internal \brief Set the expiration metadata on the bucket file based on HTTP caching rules
115  */
116 static void bucket_file_set_expiration(struct ast_bucket_file *bucket_file)
117 {
118         struct ast_bucket_metadata *metadata;
119         char time_buf[32];
120         struct timeval actual_expires = ast_tvnow();
121
122         metadata = ast_bucket_file_metadata_get(bucket_file, "cache-control");
123         if (metadata) {
124                 char *str_max_age;
125
126                 str_max_age = strstr(metadata->value, "s-maxage");
127                 if (!str_max_age) {
128                         str_max_age = strstr(metadata->value, "max-age");
129                 }
130
131                 if (str_max_age) {
132                         unsigned int max_age;
133                         char *equal = strchr(str_max_age, '=');
134                         if (equal && (sscanf(equal + 1, "%30u", &max_age) == 1)) {
135                                 actual_expires.tv_sec += max_age;
136                         }
137                 }
138                 ao2_ref(metadata, -1);
139         } else {
140                 metadata = ast_bucket_file_metadata_get(bucket_file, "expires");
141                 if (metadata) {
142                         struct tm expires_time;
143
144                         strptime(metadata->value, "%a, %d %b %Y %T %z", &expires_time);
145                         expires_time.tm_isdst = -1;
146                         actual_expires.tv_sec = mktime(&expires_time);
147
148                         ao2_ref(metadata, -1);
149                 }
150         }
151
152         /* Use 'now' if we didn't get an expiration time */
153         snprintf(time_buf, sizeof(time_buf), "%30lu", actual_expires.tv_sec);
154
155         ast_bucket_file_metadata_set(bucket_file, "__actual_expires", time_buf);
156 }
157
158 /*! \internal
159  * \brief Return whether or not we should always revalidate against the server
160  */
161 static int bucket_file_always_revalidate(struct ast_bucket_file *bucket_file)
162 {
163         RAII_VAR(struct ast_bucket_metadata *, metadata,
164                 ast_bucket_file_metadata_get(bucket_file, "cache-control"),
165                 ao2_cleanup);
166
167         if (!metadata) {
168                 return 0;
169         }
170
171         if (strstr(metadata->value, "no-cache")
172                 || strstr(metadata->value, "must-revalidate")) {
173                 return 1;
174         }
175
176         return 0;
177 }
178
179 /*! \internal
180  * \brief Return whether or not the item has expired
181  */
182 static int bucket_file_expired(struct ast_bucket_file *bucket_file)
183 {
184         RAII_VAR(struct ast_bucket_metadata *, metadata,
185                 ast_bucket_file_metadata_get(bucket_file, "__actual_expires"),
186                 ao2_cleanup);
187         struct timeval current_time = ast_tvnow();
188         struct timeval expires = { .tv_sec = 0, .tv_usec = 0 };
189
190         if (!metadata) {
191                 return 1;
192         }
193
194         if (sscanf(metadata->value, "%lu", &expires.tv_sec) != 1) {
195                 return 1;
196         }
197
198         return ast_tvcmp(current_time, expires) == -1 ? 0 : 1;
199 }
200
201 /*!
202  * \internal \brief Obtain a CURL handle with common setup options
203  */
204 static CURL *get_curl_instance(struct curl_bucket_file_data *cb_data)
205 {
206         CURL *curl;
207
208         curl = curl_easy_init();
209         if (!curl) {
210                 return NULL;
211         }
212
213         curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
214         curl_easy_setopt(curl, CURLOPT_TIMEOUT, 180);
215         curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback);
216         curl_easy_setopt(curl, CURLOPT_USERAGENT, GLOBAL_USERAGENT);
217         curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
218         curl_easy_setopt(curl, CURLOPT_URL, ast_sorcery_object_get_id(cb_data->bucket_file));
219         curl_easy_setopt(curl, CURLOPT_HEADERDATA, cb_data);
220
221         return curl;
222 }
223
224 /*!
225  * \brief Execute the CURL
226  */
227 static long execute_curl_instance(CURL *curl)
228 {
229         char curl_errbuf[CURL_ERROR_SIZE + 1];
230         long http_code;
231
232         curl_errbuf[CURL_ERROR_SIZE] = '\0';
233         curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
234
235         if (curl_easy_perform(curl)) {
236                 ast_log(LOG_WARNING, "%s\n", curl_errbuf);
237                 return -1;
238         }
239
240         curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
241
242         curl_easy_cleanup(curl);
243
244         return http_code;
245 }
246
247 /*!
248  * \internal \brief CURL the URI specified by the bucket_file and store it in the provided path
249  */
250 static int bucket_file_run_curl(struct ast_bucket_file *bucket_file)
251 {
252         struct curl_bucket_file_data cb_data = {
253                 .bucket_file = bucket_file,
254         };
255         long http_code;
256         CURL *curl;
257
258         cb_data.out_file = fopen(bucket_file->path, "wb");
259         if (!cb_data.out_file) {
260                 ast_log(LOG_WARNING, "Failed to open file '%s' for writing: %s (%d)\n",
261                         bucket_file->path, strerror(errno), errno);
262                 return -1;
263         }
264
265         curl = get_curl_instance(&cb_data);
266         if (!curl) {
267                 fclose(cb_data.out_file);
268                 return -1;
269         }
270
271         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_body_callback);
272         curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&cb_data);
273
274         http_code = execute_curl_instance(curl);
275
276         fclose(cb_data.out_file);
277
278         if (http_code / 100 == 2) {
279                 bucket_file_set_expiration(bucket_file);
280                 return 0;
281         } else {
282                 ast_log(LOG_WARNING, "Failed to retrieve URL '%s': server returned %ld\n",
283                         ast_sorcery_object_get_id(bucket_file), http_code);
284         }
285
286         return -1;
287 }
288
289 static int bucket_http_wizard_is_stale(const struct ast_sorcery *sorcery, void *data, void *object)
290 {
291         struct ast_bucket_file *bucket_file = object;
292         struct ast_bucket_metadata *metadata;
293         struct curl_slist *header_list = NULL;
294         long http_code;
295         CURL *curl;
296         struct curl_bucket_file_data cb_data = {
297                 .bucket_file = bucket_file
298         };
299         char etag_buf[256];
300
301         if (!bucket_file_expired(bucket_file) && !bucket_file_always_revalidate(bucket_file)) {
302                 return 0;
303         }
304
305         /* See if we have an ETag for this item. If not, it's stale. */
306         metadata = ast_bucket_file_metadata_get(bucket_file, "etag");
307         if (!metadata) {
308                 return 1;
309         }
310
311         curl = get_curl_instance(&cb_data);
312
313         /* Set the ETag header on our outgoing request */
314         snprintf(etag_buf, sizeof(etag_buf), "If-None-Match: %s", metadata->value);
315         header_list = curl_slist_append(header_list, etag_buf);
316         curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
317         curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
318         ao2_ref(metadata, -1);
319
320         http_code = execute_curl_instance(curl);
321
322         curl_slist_free_all(header_list);
323
324         if (http_code == 304) {
325                 bucket_file_set_expiration(bucket_file);
326                 return 0;
327         }
328
329         return 1;
330 }
331
332 static int bucket_http_wizard_create(const struct ast_sorcery *sorcery, void *data,
333         void *object)
334 {
335         struct ast_bucket_file *bucket_file = object;
336
337         return bucket_file_run_curl(bucket_file);
338 }
339
340 static void *bucket_http_wizard_retrieve_id(const struct ast_sorcery *sorcery,
341         void *data, const char *type, const char *id)
342 {
343         struct ast_bucket_file *bucket_file;
344
345         if (strcmp(type, "file")) {
346                 ast_log(LOG_WARNING, "Failed to create storage: invalid bucket type '%s'\n", type);
347                 return NULL;
348         }
349
350         if (ast_strlen_zero(id)) {
351                 ast_log(LOG_WARNING, "Failed to create storage: no URI\n");
352                 return NULL;
353         }
354
355         bucket_file = ast_bucket_file_alloc(id);
356         if (!bucket_file) {
357                 ast_log(LOG_WARNING, "Failed to create storage for '%s'\n", id);
358                 return NULL;
359         }
360
361         if (ast_bucket_file_temporary_create(bucket_file)) {
362                 ast_log(LOG_WARNING, "Failed to create temporary storage for '%s'\n", id);
363                 ast_sorcery_delete(sorcery, bucket_file);
364                 ao2_ref(bucket_file, -1);
365                 return NULL;
366         }
367
368         if (bucket_file_run_curl(bucket_file)) {
369                 ast_sorcery_delete(sorcery, bucket_file);
370                 ao2_ref(bucket_file, -1);
371                 return NULL;
372         }
373
374         return bucket_file;
375 }
376
377 static int bucket_http_wizard_delete(const struct ast_sorcery *sorcery, void *data,
378         void *object)
379 {
380         struct ast_bucket_file *bucket_file = object;
381
382         unlink(bucket_file->path);
383
384         return 0;
385 }
386
387 static struct ast_sorcery_wizard http_bucket_wizard = {
388         .name = "http",
389         .create = bucket_http_wizard_create,
390         .retrieve_id = bucket_http_wizard_retrieve_id,
391         .delete = bucket_http_wizard_delete,
392         .is_stale = bucket_http_wizard_is_stale,
393 };
394
395 static struct ast_sorcery_wizard http_bucket_file_wizard = {
396         .name = "http",
397         .create = bucket_http_wizard_create,
398         .retrieve_id = bucket_http_wizard_retrieve_id,
399         .delete = bucket_http_wizard_delete,
400         .is_stale = bucket_http_wizard_is_stale,
401 };
402
403 static struct ast_sorcery_wizard https_bucket_wizard = {
404         .name = "https",
405         .create = bucket_http_wizard_create,
406         .retrieve_id = bucket_http_wizard_retrieve_id,
407         .delete = bucket_http_wizard_delete,
408         .is_stale = bucket_http_wizard_is_stale,
409 };
410
411 static struct ast_sorcery_wizard https_bucket_file_wizard = {
412         .name = "https",
413         .create = bucket_http_wizard_create,
414         .retrieve_id = bucket_http_wizard_retrieve_id,
415         .delete = bucket_http_wizard_delete,
416         .is_stale = bucket_http_wizard_is_stale,
417 };
418
419 static int unload_module(void)
420 {
421         return 0;
422 }
423
424 static int load_module(void)
425 {
426         if (ast_bucket_scheme_register("http", &http_bucket_wizard, &http_bucket_file_wizard,
427                         NULL, NULL)) {
428                 ast_log(LOG_ERROR, "Failed to register Bucket HTTP wizard scheme implementation\n");
429                 return AST_MODULE_LOAD_DECLINE;
430         }
431
432         if (ast_bucket_scheme_register("https", &https_bucket_wizard, &https_bucket_file_wizard,
433                         NULL, NULL)) {
434                 ast_log(LOG_ERROR, "Failed to register Bucket HTTPS wizard scheme implementation\n");
435                 return AST_MODULE_LOAD_DECLINE;
436         }
437
438         return AST_MODULE_LOAD_SUCCESS;
439 }
440
441 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "HTTP Media Cache Backend",
442                 .support_level = AST_MODULE_SUPPORT_CORE,
443                 .load = load_module,
444                 .unload = unload_module,
445                 .requires = "res_curl",
446         );