Asterisk - The Open Source Telephony Project  18.5.0
res_http_media_cache.c
Go to the documentation of this file.
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2015, Matt Jordan
5  *
6  * Matt Jordan <[email protected]>
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 <[email protected]> \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 */
49  /*! The \c ast_bucket_file object that caused the operation */
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  */
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  */
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  */
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_MAXREDIRS, 8);
219  curl_easy_setopt(curl, CURLOPT_URL, ast_sorcery_object_get_id(cb_data->bucket_file));
220  curl_easy_setopt(curl, CURLOPT_HEADERDATA, cb_data);
221 
222  return curl;
223 }
224 
225 /*!
226  * \brief Execute the CURL
227  */
228 static long execute_curl_instance(CURL *curl)
229 {
230  char curl_errbuf[CURL_ERROR_SIZE + 1];
231  long http_code;
232 
233  curl_errbuf[CURL_ERROR_SIZE] = '\0';
234  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
235 
236  if (curl_easy_perform(curl)) {
237  ast_log(LOG_WARNING, "%s\n", curl_errbuf);
238  return -1;
239  }
240 
241  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
242 
243  curl_easy_cleanup(curl);
244 
245  return http_code;
246 }
247 
248 /*!
249  * \internal \brief CURL the URI specified by the bucket_file and store it in the provided path
250  */
252 {
253  struct curl_bucket_file_data cb_data = {
255  };
256  long http_code;
257  CURL *curl;
258 
259  cb_data.out_file = fopen(bucket_file->path, "wb");
260  if (!cb_data.out_file) {
261  ast_log(LOG_WARNING, "Failed to open file '%s' for writing: %s (%d)\n",
262  bucket_file->path, strerror(errno), errno);
263  return -1;
264  }
265 
266  curl = get_curl_instance(&cb_data);
267  if (!curl) {
268  fclose(cb_data.out_file);
269  return -1;
270  }
271 
272  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_body_callback);
273  curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&cb_data);
274 
275  http_code = execute_curl_instance(curl);
276 
277  fclose(cb_data.out_file);
278 
279  if (http_code / 100 == 2) {
280  bucket_file_set_expiration(bucket_file);
281  return 0;
282  } else {
283  ast_log(LOG_WARNING, "Failed to retrieve URL '%s': server returned %ld\n",
284  ast_sorcery_object_get_id(bucket_file), http_code);
285  }
286 
287  return -1;
288 }
289 
290 static int bucket_http_wizard_is_stale(const struct ast_sorcery *sorcery, void *data, void *object)
291 {
292  struct ast_bucket_file *bucket_file = object;
293  struct ast_bucket_metadata *metadata;
294  struct curl_slist *header_list = NULL;
295  long http_code;
296  CURL *curl;
297  struct curl_bucket_file_data cb_data = {
298  .bucket_file = bucket_file
299  };
300  char etag_buf[256];
301 
302  if (!bucket_file_expired(bucket_file) && !bucket_file_always_revalidate(bucket_file)) {
303  return 0;
304  }
305 
306  /* See if we have an ETag for this item. If not, it's stale. */
307  metadata = ast_bucket_file_metadata_get(bucket_file, "etag");
308  if (!metadata) {
309  return 1;
310  }
311 
312  curl = get_curl_instance(&cb_data);
313 
314  /* Set the ETag header on our outgoing request */
315  snprintf(etag_buf, sizeof(etag_buf), "If-None-Match: %s", metadata->value);
316  header_list = curl_slist_append(header_list, etag_buf);
317  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
318  curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
319  ao2_ref(metadata, -1);
320 
321  http_code = execute_curl_instance(curl);
322 
323  curl_slist_free_all(header_list);
324 
325  if (http_code == 304) {
326  bucket_file_set_expiration(bucket_file);
327  return 0;
328  }
329 
330  return 1;
331 }
332 
333 static int bucket_http_wizard_create(const struct ast_sorcery *sorcery, void *data,
334  void *object)
335 {
336  struct ast_bucket_file *bucket_file = object;
337 
338  return bucket_file_run_curl(bucket_file);
339 }
340 
342  void *data, const char *type, const char *id)
343 {
345 
346  if (strcmp(type, "file")) {
347  ast_log(LOG_WARNING, "Failed to create storage: invalid bucket type '%s'\n", type);
348  return NULL;
349  }
350 
351  if (ast_strlen_zero(id)) {
352  ast_log(LOG_WARNING, "Failed to create storage: no URI\n");
353  return NULL;
354  }
355 
356  bucket_file = ast_bucket_file_alloc(id);
357  if (!bucket_file) {
358  ast_log(LOG_WARNING, "Failed to create storage for '%s'\n", id);
359  return NULL;
360  }
361 
362  if (ast_bucket_file_temporary_create(bucket_file)) {
363  ast_log(LOG_WARNING, "Failed to create temporary storage for '%s'\n", id);
364  ast_sorcery_delete(sorcery, bucket_file);
365  ao2_ref(bucket_file, -1);
366  return NULL;
367  }
368 
369  if (bucket_file_run_curl(bucket_file)) {
370  ast_sorcery_delete(sorcery, bucket_file);
371  ao2_ref(bucket_file, -1);
372  return NULL;
373  }
374 
375  return bucket_file;
376 }
377 
378 static int bucket_http_wizard_delete(const struct ast_sorcery *sorcery, void *data,
379  void *object)
380 {
381  struct ast_bucket_file *bucket_file = object;
382 
383  unlink(bucket_file->path);
384 
385  return 0;
386 }
387 
389  .name = "http",
390  .create = bucket_http_wizard_create,
391  .retrieve_id = bucket_http_wizard_retrieve_id,
392  .delete = bucket_http_wizard_delete,
393  .is_stale = bucket_http_wizard_is_stale,
394 };
395 
397  .name = "http",
398  .create = bucket_http_wizard_create,
399  .retrieve_id = bucket_http_wizard_retrieve_id,
400  .delete = bucket_http_wizard_delete,
401  .is_stale = bucket_http_wizard_is_stale,
402 };
403 
405  .name = "https",
406  .create = bucket_http_wizard_create,
407  .retrieve_id = bucket_http_wizard_retrieve_id,
408  .delete = bucket_http_wizard_delete,
409  .is_stale = bucket_http_wizard_is_stale,
410 };
411 
413  .name = "https",
414  .create = bucket_http_wizard_create,
415  .retrieve_id = bucket_http_wizard_retrieve_id,
416  .delete = bucket_http_wizard_delete,
417  .is_stale = bucket_http_wizard_is_stale,
418 };
419 
420 static int unload_module(void)
421 {
422  return 0;
423 }
424 
425 static int load_module(void)
426 {
427  if (ast_bucket_scheme_register("http", &http_bucket_wizard, &http_bucket_file_wizard,
428  NULL, NULL)) {
429  ast_log(LOG_ERROR, "Failed to register Bucket HTTP wizard scheme implementation\n");
431  }
432 
433  if (ast_bucket_scheme_register("https", &https_bucket_wizard, &https_bucket_file_wizard,
434  NULL, NULL)) {
435  ast_log(LOG_ERROR, "Failed to register Bucket HTTPS wizard scheme implementation\n");
437  }
438 
440 }
441 
442 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "HTTP Media Cache Backend",
443  .support_level = AST_MODULE_SUPPORT_CORE,
444  .load = load_module,
445  .unload = unload_module,
446  .requires = "res_curl",
447  );
struct ast_bucket_metadata * ast_bucket_file_metadata_get(struct ast_bucket_file *file, const char *name)
Retrieve a metadata attribute from a file.
Definition: bucket.c:359
static void bucket_file_set_expiration(struct ast_bucket_file *bucket_file)
static const char type[]
Definition: chan_ooh323.c:109
Asterisk main include file. File version handling, generic pbx functions.
struct ast_bucket_file * bucket_file
int ast_bucket_file_metadata_set(struct ast_bucket_file *file, const char *name, const char *value)
Set a metadata attribute on a file to a specific value.
Definition: bucket.c:334
#define GLOBAL_USERAGENT
#define LOG_WARNING
Definition: logger.h:274
static int unload_module(void)
Full structure for sorcery.
Definition: sorcery.c:230
struct timeval ast_tvnow(void)
Returns current timeval. Meant to replace calls to gettimeofday().
Definition: time.h:150
static int bucket_http_wizard_create(const struct ast_sorcery *sorcery, void *data, void *object)
#define NULL
Definition: resample.c:96
struct ast_bucket_file * ast_bucket_file_alloc(const char *uri)
Allocate a new bucket file.
Definition: bucket.c:663
Definitions to aid in the use of thread local storage.
int value
Definition: syslog.c:37
const char * name
Name of the wizard.
Definition: sorcery.h:278
Bucket File API.
static int bucket_http_wizard_is_stale(const struct ast_sorcery *sorcery, void *data, void *object)
#define ast_strlen_zero(foo)
Definition: strings.h:52
const char * value
Value of the attribute.
Definition: bucket.h:51
#define ast_log
Definition: astobj2.c:42
#define RAII_VAR(vartype, varname, initval, dtor)
Declare a variable that will call a destructor function when it goes out of scope.
Definition: utils.h:911
#define ao2_ref(o, delta)
Definition: astobj2.h:464
const char * ast_sorcery_object_get_id(const void *object)
Get the unique identifier of a sorcery object.
Definition: sorcery.c:2312
static int bucket_file_expired(struct ast_bucket_file *bucket_file)
Bucket file structure, contains reference to file and information about it.
Definition: bucket.h:78
static size_t curl_header_callback(char *buffer, size_t size, size_t nitems, void *data)
static force_inline char * ast_str_to_lower(char *str)
Convert a string to all lower-case.
Definition: strings.h:1268
Data passed to cURL callbacks.
#define ast_alloca(size)
call __builtin_alloca to ensure we get gcc builtin semantics
Definition: astmm.h:290
int ast_sorcery_delete(const struct ast_sorcery *sorcery, void *object)
Delete an object.
Definition: sorcery.c:2233
static long execute_curl_instance(CURL *curl)
Execute the CURL.
#define LOG_ERROR
Definition: logger.h:285
int ast_tvcmp(struct timeval _a, struct timeval _b)
Compres two struct timeval instances returning -1, 0, 1 if the first arg is smaller, equal or greater to the second.
Definition: time.h:128
static int load_module(void)
static void * bucket_http_wizard_retrieve_id(const struct ast_sorcery *sorcery, void *data, const char *type, const char *id)
static struct ast_sorcery_wizard https_bucket_file_wizard
int errno
char * ast_skip_blanks(const char *str)
Gets a pointer to the first non-whitespace character in a string.
Definition: strings.h:157
char * ast_trim_blanks(char *str)
Trims trailing whitespace characters from a string.
Definition: strings.h:182
static int bucket_http_wizard_delete(const struct ast_sorcery *sorcery, void *data, void *object)
Bucket metadata structure, AO2 key value pair.
Definition: bucket.h:47
static int bucket_file_always_revalidate(struct ast_bucket_file *bucket_file)
Module has failed to load, may be in an inconsistent state.
Definition: module.h:78
static int bucket_file_run_curl(struct ast_bucket_file *bucket_file)
static struct ast_sorcery_wizard http_bucket_wizard
int ast_bucket_file_temporary_create(struct ast_bucket_file *file)
Common file snapshot creation callback for creating a temporary file.
Definition: bucket.c:899
AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS|AST_MODFLAG_LOAD_ORDER, "HTTP Phone Provisioning",.support_level=AST_MODULE_SUPPORT_EXTENDED,.load=load_module,.unload=unload_module,.reload=reload,.load_pri=AST_MODPRI_CHANNEL_DEPEND,.requires="http",)
Interface for a sorcery wizard.
Definition: sorcery.h:276
static struct ast_sorcery * sorcery
#define ao2_cleanup(obj)
Definition: astobj2.h:1958
#define MAX_HEADER_LENGTH
#define ast_bucket_scheme_register(name, bucket, file, create_cb, destroy_cb)
Register support for a specific scheme.
Definition: bucket.h:137
static CURL * get_curl_instance(struct curl_bucket_file_data *cb_data)
void(* load)(void *data, const struct ast_sorcery *sorcery, const char *type)
Optional callback for loading persistent objects.
Definition: sorcery.h:287
#define ASTERISK_GPL_KEY
The text the key() function should return.
Definition: module.h:46
static size_t curl_body_callback(void *ptr, size_t size, size_t nitems, void *data)
Asterisk module definitions.
static struct ast_sorcery_wizard https_bucket_wizard
static struct ast_sorcery_wizard http_bucket_file_wizard
char path[PATH_MAX]
Local path to this file.
Definition: bucket.h:95
Sorcery Data Access Layer API.