Merge "res_pjsip: New endpoint option "refer_blind_progress""
[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, "Expires")) {
88                 return realsize;
89         }
90
91         value = ast_trim_blanks(ast_skip_blanks(value));
92         header = ast_str_to_lower(header);
93
94         ast_bucket_file_metadata_set(cb_data->bucket_file, header, value);
95
96         return realsize;
97 }
98
99 /*!
100  * \internal \brief The cURL body callback function
101  */
102 static size_t curl_body_callback(void *ptr, size_t size, size_t nitems, void *data)
103 {
104         struct curl_bucket_file_data *cb_data = data;
105         size_t realsize;
106
107         realsize = fwrite(ptr, size, nitems, cb_data->out_file);
108
109         return realsize;
110 }
111
112 /*!
113  * \internal \brief Set the expiration metadata on the bucket file based on HTTP caching rules
114  */
115 static void bucket_file_set_expiration(struct ast_bucket_file *bucket_file)
116 {
117         struct ast_bucket_metadata *metadata;
118         char time_buf[32];
119         struct timeval actual_expires = ast_tvnow();
120
121         metadata = ast_bucket_file_metadata_get(bucket_file, "cache-control");
122         if (metadata) {
123                 char *str_max_age;
124
125                 str_max_age = strstr(metadata->value, "s-maxage");
126                 if (!str_max_age) {
127                         str_max_age = strstr(metadata->value, "max-age");
128                 }
129
130                 if (str_max_age) {
131                         unsigned int max_age;
132                         char *equal = strchr(str_max_age, '=');
133                         if (equal && (sscanf(equal + 1, "%30u", &max_age) == 1)) {
134                                 actual_expires.tv_sec += max_age;
135                         }
136                 }
137                 ao2_ref(metadata, -1);
138         } else {
139                 metadata = ast_bucket_file_metadata_get(bucket_file, "expires");
140                 if (metadata) {
141                         struct tm expires_time;
142
143                         strptime(metadata->value, "%a, %d %b %Y %T %z", &expires_time);
144                         expires_time.tm_isdst = -1;
145                         actual_expires.tv_sec = mktime(&expires_time);
146
147                         ao2_ref(metadata, -1);
148                 }
149         }
150
151         /* Use 'now' if we didn't get an expiration time */
152         snprintf(time_buf, sizeof(time_buf), "%30lu", actual_expires.tv_sec);
153
154         ast_bucket_file_metadata_set(bucket_file, "__actual_expires", time_buf);
155 }
156
157 /*! \internal
158  * \brief Return whether or not we should always revalidate against the server
159  */
160 static int bucket_file_always_revalidate(struct ast_bucket_file *bucket_file)
161 {
162         RAII_VAR(struct ast_bucket_metadata *, metadata,
163                 ast_bucket_file_metadata_get(bucket_file, "cache-control"),
164                 ao2_cleanup);
165
166         if (!metadata) {
167                 return 0;
168         }
169
170         if (strstr(metadata->value, "no-cache")
171                 || strstr(metadata->value, "must-revalidate")) {
172                 return 1;
173         }
174
175         return 0;
176 }
177
178 /*! \internal
179  * \brief Return whether or not the item has expired
180  */
181 static int bucket_file_expired(struct ast_bucket_file *bucket_file)
182 {
183         RAII_VAR(struct ast_bucket_metadata *, metadata,
184                 ast_bucket_file_metadata_get(bucket_file, "__actual_expires"),
185                 ao2_cleanup);
186         struct timeval current_time = ast_tvnow();
187         struct timeval expires = { .tv_sec = 0, .tv_usec = 0 };
188
189         if (!metadata) {
190                 return 1;
191         }
192
193         if (sscanf(metadata->value, "%lu", &expires.tv_sec) != 1) {
194                 return 1;
195         }
196
197         return ast_tvcmp(current_time, expires) == -1 ? 0 : 1;
198 }
199
200 /*!
201  * \internal \brief Obtain a CURL handle with common setup options
202  */
203 static CURL *get_curl_instance(struct curl_bucket_file_data *cb_data)
204 {
205         CURL *curl;
206
207         curl = curl_easy_init();
208         if (!curl) {
209                 return NULL;
210         }
211
212         curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
213         curl_easy_setopt(curl, CURLOPT_TIMEOUT, 180);
214         curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback);
215         curl_easy_setopt(curl, CURLOPT_USERAGENT, GLOBAL_USERAGENT);
216         curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
217         curl_easy_setopt(curl, CURLOPT_URL, ast_sorcery_object_get_id(cb_data->bucket_file));
218         curl_easy_setopt(curl, CURLOPT_HEADERDATA, cb_data);
219
220         return curl;
221 }
222
223 /*!
224  * \brief Execute the CURL
225  */
226 static long execute_curl_instance(CURL *curl)
227 {
228         char curl_errbuf[CURL_ERROR_SIZE + 1];
229         long http_code;
230
231         curl_errbuf[CURL_ERROR_SIZE] = '\0';
232         curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
233
234         if (curl_easy_perform(curl)) {
235                 ast_log(LOG_WARNING, "%s\n", curl_errbuf);
236                 return -1;
237         }
238
239         curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
240
241         curl_easy_cleanup(curl);
242
243         return http_code;
244 }
245
246 /*!
247  * \internal \brief CURL the URI specified by the bucket_file and store it in the provided path
248  */
249 static int bucket_file_run_curl(struct ast_bucket_file *bucket_file)
250 {
251         struct curl_bucket_file_data cb_data = {
252                 .bucket_file = bucket_file,
253         };
254         long http_code;
255         CURL *curl;
256
257         cb_data.out_file = fopen(bucket_file->path, "wb");
258         if (!cb_data.out_file) {
259                 ast_log(LOG_WARNING, "Failed to open file '%s' for writing: %s (%d)\n",
260                         bucket_file->path, strerror(errno), errno);
261                 return -1;
262         }
263
264         curl = get_curl_instance(&cb_data);
265         if (!curl) {
266                 fclose(cb_data.out_file);
267                 return -1;
268         }
269
270         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_body_callback);
271         curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&cb_data);
272
273         http_code = execute_curl_instance(curl);
274
275         fclose(cb_data.out_file);
276
277         if (http_code / 100 == 2) {
278                 bucket_file_set_expiration(bucket_file);
279                 return 0;
280         } else {
281                 ast_log(LOG_WARNING, "Failed to retrieve URL '%s': server returned %ld\n",
282                         ast_sorcery_object_get_id(bucket_file), http_code);
283         }
284
285         return -1;
286 }
287
288 static int bucket_http_wizard_is_stale(const struct ast_sorcery *sorcery, void *data, void *object)
289 {
290         struct ast_bucket_file *bucket_file = object;
291         struct ast_bucket_metadata *metadata;
292         struct curl_slist *header_list = NULL;
293         long http_code;
294         CURL *curl;
295         struct curl_bucket_file_data cb_data = {
296                 .bucket_file = bucket_file
297         };
298         char etag_buf[256];
299
300         if (!bucket_file_expired(bucket_file) && !bucket_file_always_revalidate(bucket_file)) {
301                 return 0;
302         }
303
304         /* See if we have an ETag for this item. If not, it's stale. */
305         metadata = ast_bucket_file_metadata_get(bucket_file, "etag");
306         if (!metadata) {
307                 return 1;
308         }
309
310         curl = get_curl_instance(&cb_data);
311
312         /* Set the ETag header on our outgoing request */
313         snprintf(etag_buf, sizeof(etag_buf), "If-None-Match: %s", metadata->value);
314         header_list = curl_slist_append(header_list, etag_buf);
315         curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
316         curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
317         ao2_ref(metadata, -1);
318
319         http_code = execute_curl_instance(curl);
320
321         curl_slist_free_all(header_list);
322
323         if (http_code == 304) {
324                 bucket_file_set_expiration(bucket_file);
325                 return 0;
326         }
327
328         return 1;
329 }
330
331 static int bucket_http_wizard_create(const struct ast_sorcery *sorcery, void *data,
332         void *object)
333 {
334         struct ast_bucket_file *bucket_file = object;
335
336         return bucket_file_run_curl(bucket_file);
337 }
338
339 static void *bucket_http_wizard_retrieve_id(const struct ast_sorcery *sorcery,
340         void *data, const char *type, const char *id)
341 {
342         struct ast_bucket_file *bucket_file;
343
344         if (strcmp(type, "file")) {
345                 ast_log(LOG_WARNING, "Failed to create storage: invalid bucket type '%s'\n", type);
346                 return NULL;
347         }
348
349         if (ast_strlen_zero(id)) {
350                 ast_log(LOG_WARNING, "Failed to create storage: no URI\n");
351                 return NULL;
352         }
353
354         bucket_file = ast_bucket_file_alloc(id);
355         if (!bucket_file) {
356                 ast_log(LOG_WARNING, "Failed to create storage for '%s'\n", id);
357                 return NULL;
358         }
359
360         if (ast_bucket_file_temporary_create(bucket_file)) {
361                 ast_log(LOG_WARNING, "Failed to create temporary storage for '%s'\n", id);
362                 ast_sorcery_delete(sorcery, bucket_file);
363                 ao2_ref(bucket_file, -1);
364                 return NULL;
365         }
366
367         if (bucket_file_run_curl(bucket_file)) {
368                 ast_sorcery_delete(sorcery, bucket_file);
369                 ao2_ref(bucket_file, -1);
370                 return NULL;
371         }
372
373         return bucket_file;
374 }
375
376 static int bucket_http_wizard_delete(const struct ast_sorcery *sorcery, void *data,
377         void *object)
378 {
379         struct ast_bucket_file *bucket_file = object;
380
381         unlink(bucket_file->path);
382
383         return 0;
384 }
385
386 static struct ast_sorcery_wizard http_bucket_wizard = {
387         .name = "http",
388         .create = bucket_http_wizard_create,
389         .retrieve_id = bucket_http_wizard_retrieve_id,
390         .delete = bucket_http_wizard_delete,
391         .is_stale = bucket_http_wizard_is_stale,
392 };
393
394 static struct ast_sorcery_wizard http_bucket_file_wizard = {
395         .name = "http",
396         .create = bucket_http_wizard_create,
397         .retrieve_id = bucket_http_wizard_retrieve_id,
398         .delete = bucket_http_wizard_delete,
399         .is_stale = bucket_http_wizard_is_stale,
400 };
401
402 static struct ast_sorcery_wizard https_bucket_wizard = {
403         .name = "https",
404         .create = bucket_http_wizard_create,
405         .retrieve_id = bucket_http_wizard_retrieve_id,
406         .delete = bucket_http_wizard_delete,
407         .is_stale = bucket_http_wizard_is_stale,
408 };
409
410 static struct ast_sorcery_wizard https_bucket_file_wizard = {
411         .name = "https",
412         .create = bucket_http_wizard_create,
413         .retrieve_id = bucket_http_wizard_retrieve_id,
414         .delete = bucket_http_wizard_delete,
415         .is_stale = bucket_http_wizard_is_stale,
416 };
417
418 static int unload_module(void)
419 {
420         return 0;
421 }
422
423 static int load_module(void)
424 {
425         if (ast_bucket_scheme_register("http", &http_bucket_wizard, &http_bucket_file_wizard,
426                         NULL, NULL)) {
427                 ast_log(LOG_ERROR, "Failed to register Bucket HTTP wizard scheme implementation\n");
428                 return AST_MODULE_LOAD_DECLINE;
429         }
430
431         if (ast_bucket_scheme_register("https", &https_bucket_wizard, &https_bucket_file_wizard,
432                         NULL, NULL)) {
433                 ast_log(LOG_ERROR, "Failed to register Bucket HTTPS wizard scheme implementation\n");
434                 return AST_MODULE_LOAD_DECLINE;
435         }
436
437         return AST_MODULE_LOAD_SUCCESS;
438 }
439
440 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "HTTP Media Cache Backend",
441                 .support_level = AST_MODULE_SUPPORT_CORE,
442                 .load = load_module,
443                 .unload = unload_module,
444                 .load_pri = AST_MODPRI_DEFAULT,
445         );