Asterisk - The Open Source Telephony Project  GIT-master-e8cda4b
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 <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/file.h"
39 #include "asterisk/module.h"
40 #include "asterisk/bucket.h"
41 #include "asterisk/sorcery.h"
42 #include "asterisk/threadstorage.h"
43 #include "asterisk/uri.h"
44 
45 #define GLOBAL_USERAGENT "asterisk-libcurl-agent/1.0"
46 
47 #define MAX_HEADER_LENGTH 1023
48 
49 /*! \brief Data passed to cURL callbacks */
51  /*! The \c ast_bucket_file object that caused the operation */
53  /*! File to write data to */
54  FILE *out_file;
55 };
56 
57 /*!
58  * \internal \brief The cURL header callback function
59  */
60 static size_t curl_header_callback(char *buffer, size_t size, size_t nitems, void *data)
61 {
62  struct curl_bucket_file_data *cb_data = data;
63  size_t realsize;
64  char *value;
65  char *header;
66 
67  realsize = size * nitems;
68 
69  if (realsize > MAX_HEADER_LENGTH) {
70  ast_log(LOG_WARNING, "cURL header length of '%zu' is too large: max %d\n",
71  realsize, MAX_HEADER_LENGTH);
72  return 0;
73  }
74 
75  /* buffer may not be NULL terminated */
76  header = ast_alloca(realsize + 1);
77  memcpy(header, buffer, realsize);
78  header[realsize] = '\0';
79  value = strchr(header, ':');
80  if (!value) {
81  /* Not a header we care about; bail */
82  return realsize;
83  }
84  *value++ = '\0';
85 
86  if (strcasecmp(header, "ETag")
87  && strcasecmp(header, "Cache-Control")
88  && strcasecmp(header, "Last-Modified")
89  && strcasecmp(header, "Content-Type")
90  && strcasecmp(header, "Expires")) {
91  return realsize;
92  }
93 
94  value = ast_trim_blanks(ast_skip_blanks(value));
95  header = ast_str_to_lower(header);
96 
97  ast_bucket_file_metadata_set(cb_data->bucket_file, header, value);
98 
99  return realsize;
100 }
101 
102 /*!
103  * \internal \brief The cURL body callback function
104  */
105 static size_t curl_body_callback(void *ptr, size_t size, size_t nitems, void *data)
106 {
107  struct curl_bucket_file_data *cb_data = data;
108  size_t realsize;
109 
110  realsize = fwrite(ptr, size, nitems, cb_data->out_file);
111 
112  return realsize;
113 }
114 
115 /*!
116  * \internal \brief Set the expiration metadata on the bucket file based on HTTP caching rules
117  */
119 {
120  struct ast_bucket_metadata *metadata;
121  char time_buf[32];
122  struct timeval actual_expires = ast_tvnow();
123 
124  metadata = ast_bucket_file_metadata_get(bucket_file, "cache-control");
125  if (metadata) {
126  char *str_max_age;
127 
128  str_max_age = strstr(metadata->value, "s-maxage");
129  if (!str_max_age) {
130  str_max_age = strstr(metadata->value, "max-age");
131  }
132 
133  if (str_max_age) {
134  unsigned int max_age;
135  char *equal = strchr(str_max_age, '=');
136  if (equal && (sscanf(equal + 1, "%30u", &max_age) == 1)) {
137  actual_expires.tv_sec += max_age;
138  }
139  }
140  ao2_ref(metadata, -1);
141  } else {
142  metadata = ast_bucket_file_metadata_get(bucket_file, "expires");
143  if (metadata) {
144  struct tm expires_time;
145 
146  strptime(metadata->value, "%a, %d %b %Y %T %z", &expires_time);
147  expires_time.tm_isdst = -1;
148  actual_expires.tv_sec = mktime(&expires_time);
149 
150  ao2_ref(metadata, -1);
151  }
152  }
153 
154  /* Use 'now' if we didn't get an expiration time */
155  snprintf(time_buf, sizeof(time_buf), "%30lu", actual_expires.tv_sec);
156 
157  ast_bucket_file_metadata_set(bucket_file, "__actual_expires", time_buf);
158 }
159 
160 static char *file_extension_from_string(const char *str, char *buffer, size_t capacity)
161 {
162  const char *ext;
163 
164  ext = strrchr(str, '.');
165  if (ext && ast_get_format_for_file_ext(ext + 1)) {
166  ast_debug(3, "Found extension '%s' at end of string\n", ext);
167  ast_copy_string(buffer, ext, capacity);
168  return buffer;
169  }
170 
171  return NULL;
172 }
173 
174 static char *file_extension_from_url(struct ast_bucket_file *bucket_file, char *buffer, size_t capacity)
175 {
176  return file_extension_from_string(ast_sorcery_object_get_id(bucket_file), buffer, capacity);
177 }
178 
179 /*!
180  * \internal
181  * \brief Normalize the value of a Content-Type header
182  *
183  * This will trim off any optional parameters after the type/subtype.
184  */
185 static void normalize_content_type_header(char *content_type)
186 {
187  char *params = strchr(content_type, ';');
188 
189  if (params) {
190  *params-- = 0;
191  while (params > content_type && (*params == ' ' || *params == '\t')) {
192  *params-- = 0;
193  }
194  }
195 }
196 
197 static char *file_extension_from_content_type(struct ast_bucket_file *bucket_file, char *buffer, size_t capacity)
198 {
199  /* Check for the extension based on the MIME type passed in the Content-Type
200  * header.
201  *
202  * If a match is found then retrieve the extension from the supported list
203  * corresponding to the mime-type and use that to rename the file */
204 
205  struct ast_bucket_metadata *header;
206  char *mime_type;
207 
208  header = ast_bucket_file_metadata_get(bucket_file, "content-type");
209  if (!header) {
210  return NULL;
211  }
212 
213  mime_type = ast_strdup(header->value);
214  if (mime_type) {
216  if (!ast_strlen_zero(mime_type)) {
217  if (ast_get_extension_for_mime_type(mime_type, buffer, sizeof(buffer))) {
218  ast_debug(3, "Derived extension '%s' from MIME type %s\n",
219  buffer,
220  mime_type);
221  ast_free(mime_type);
222  ao2_ref(header, -1);
223  return buffer;
224  }
225  }
226  }
227  ast_free(mime_type);
228  ao2_ref(header, -1);
229 
230  return NULL;
231 }
232 
233 static char *file_extension_from_url_path(struct ast_bucket_file *bucket_file, char *buffer, size_t capacity)
234 {
235  struct ast_uri *uri;
236 
237  uri = ast_uri_parse(ast_sorcery_object_get_id(bucket_file));
238  if (!uri) {
239  ast_log(LOG_ERROR, "Failed to parse URI: %s\n",
240  ast_sorcery_object_get_id(bucket_file));
241  return NULL;
242  }
243 
244  /* Just parse it as a string like before, but without the extra cruft */
245  buffer = file_extension_from_string(ast_uri_path(uri), buffer, capacity);
246  ao2_cleanup(uri);
247  return buffer;
248 }
249 
251 {
252  /* We will attempt to determine an extension in the following order for backwards
253  * compatibility:
254  *
255  * 1. Look at tail end of URL for extension
256  * 2. Use the Content-Type header if present
257  * 3. Parse the URL (assuming we can) and look at the tail of the path
258  */
259 
260  char buffer[64];
261 
262  if (file_extension_from_url(bucket_file, buffer, sizeof(buffer))
263  || file_extension_from_content_type(bucket_file, buffer, sizeof(buffer))
264  || file_extension_from_url_path(bucket_file, buffer, sizeof(buffer))) {
265  ast_bucket_file_metadata_set(bucket_file, "ext", buffer);
266  }
267 }
268 
269 /*! \internal
270  * \brief Return whether or not we should always revalidate against the server
271  */
273 {
274  RAII_VAR(struct ast_bucket_metadata *, metadata,
275  ast_bucket_file_metadata_get(bucket_file, "cache-control"),
276  ao2_cleanup);
277 
278  if (!metadata) {
279  return 0;
280  }
281 
282  if (strstr(metadata->value, "no-cache")
283  || strstr(metadata->value, "must-revalidate")) {
284  return 1;
285  }
286 
287  return 0;
288 }
289 
290 /*! \internal
291  * \brief Return whether or not the item has expired
292  */
294 {
295  RAII_VAR(struct ast_bucket_metadata *, metadata,
296  ast_bucket_file_metadata_get(bucket_file, "__actual_expires"),
297  ao2_cleanup);
298  struct timeval current_time = ast_tvnow();
299  struct timeval expires = { .tv_sec = 0, .tv_usec = 0 };
300 
301  if (!metadata) {
302  return 1;
303  }
304 
305  if (sscanf(metadata->value, "%lu", &expires.tv_sec) != 1) {
306  return 1;
307  }
308 
309  return ast_tvcmp(current_time, expires) == -1 ? 0 : 1;
310 }
311 
312 /*!
313  * \internal \brief Obtain a CURL handle with common setup options
314  */
315 static CURL *get_curl_instance(struct curl_bucket_file_data *cb_data)
316 {
317  CURL *curl;
318 
319  curl = curl_easy_init();
320  if (!curl) {
321  return NULL;
322  }
323 
324  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
325  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 180);
326  curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_header_callback);
327  curl_easy_setopt(curl, CURLOPT_USERAGENT, GLOBAL_USERAGENT);
328  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
329  curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 8);
330  curl_easy_setopt(curl, CURLOPT_URL, ast_sorcery_object_get_id(cb_data->bucket_file));
331  curl_easy_setopt(curl, CURLOPT_HEADERDATA, cb_data);
332 
333  return curl;
334 }
335 
336 /*!
337  * \brief Execute the CURL
338  */
339 static long execute_curl_instance(CURL *curl)
340 {
341  char curl_errbuf[CURL_ERROR_SIZE + 1];
342  long http_code;
343 
344  curl_errbuf[CURL_ERROR_SIZE] = '\0';
345  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
346 
347  if (curl_easy_perform(curl)) {
348  ast_log(LOG_WARNING, "%s\n", curl_errbuf);
349  return -1;
350  }
351 
352  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
353 
354  curl_easy_cleanup(curl);
355 
356  return http_code;
357 }
358 
359 /*!
360  * \internal \brief CURL the URI specified by the bucket_file and store it in the provided path
361  */
363 {
364  struct curl_bucket_file_data cb_data = {
366  };
367  long http_code;
368  CURL *curl;
369 
370  cb_data.out_file = fopen(bucket_file->path, "wb");
371  if (!cb_data.out_file) {
372  ast_log(LOG_WARNING, "Failed to open file '%s' for writing: %s (%d)\n",
373  bucket_file->path, strerror(errno), errno);
374  return -1;
375  }
376 
377  curl = get_curl_instance(&cb_data);
378  if (!curl) {
379  fclose(cb_data.out_file);
380  return -1;
381  }
382 
383  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_body_callback);
384  curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&cb_data);
385 
386  http_code = execute_curl_instance(curl);
387 
388  fclose(cb_data.out_file);
389 
390  if (http_code / 100 == 2) {
391  bucket_file_set_expiration(bucket_file);
392  bucket_file_set_extension(bucket_file);
393  return 0;
394  } else {
395  ast_log(LOG_WARNING, "Failed to retrieve URL '%s': server returned %ld\n",
396  ast_sorcery_object_get_id(bucket_file), http_code);
397  }
398 
399  return -1;
400 }
401 
402 static int bucket_http_wizard_is_stale(const struct ast_sorcery *sorcery, void *data, void *object)
403 {
404  struct ast_bucket_file *bucket_file = object;
405  struct ast_bucket_metadata *metadata;
406  struct curl_slist *header_list = NULL;
407  long http_code;
408  CURL *curl;
409  struct curl_bucket_file_data cb_data = {
410  .bucket_file = bucket_file
411  };
412  char etag_buf[256];
413 
414  if (!bucket_file_expired(bucket_file) && !bucket_file_always_revalidate(bucket_file)) {
415  return 0;
416  }
417 
418  /* See if we have an ETag for this item. If not, it's stale. */
419  metadata = ast_bucket_file_metadata_get(bucket_file, "etag");
420  if (!metadata) {
421  return 1;
422  }
423 
424  curl = get_curl_instance(&cb_data);
425 
426  /* Set the ETag header on our outgoing request */
427  snprintf(etag_buf, sizeof(etag_buf), "If-None-Match: %s", metadata->value);
428  header_list = curl_slist_append(header_list, etag_buf);
429  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
430  curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
431  ao2_ref(metadata, -1);
432 
433  http_code = execute_curl_instance(curl);
434 
435  curl_slist_free_all(header_list);
436 
437  if (http_code == 304) {
438  bucket_file_set_expiration(bucket_file);
439  return 0;
440  }
441 
442  return 1;
443 }
444 
445 static int bucket_http_wizard_create(const struct ast_sorcery *sorcery, void *data,
446  void *object)
447 {
448  struct ast_bucket_file *bucket_file = object;
449 
450  return bucket_file_run_curl(bucket_file);
451 }
452 
454  void *data, const char *type, const char *id)
455 {
457 
458  if (strcmp(type, "file")) {
459  ast_log(LOG_WARNING, "Failed to create storage: invalid bucket type '%s'\n", type);
460  return NULL;
461  }
462 
463  if (ast_strlen_zero(id)) {
464  ast_log(LOG_WARNING, "Failed to create storage: no URI\n");
465  return NULL;
466  }
467 
468  bucket_file = ast_bucket_file_alloc(id);
469  if (!bucket_file) {
470  ast_log(LOG_WARNING, "Failed to create storage for '%s'\n", id);
471  return NULL;
472  }
473 
474  if (ast_bucket_file_temporary_create(bucket_file)) {
475  ast_log(LOG_WARNING, "Failed to create temporary storage for '%s'\n", id);
476  ast_sorcery_delete(sorcery, bucket_file);
477  ao2_ref(bucket_file, -1);
478  return NULL;
479  }
480 
481  if (bucket_file_run_curl(bucket_file)) {
482  ast_sorcery_delete(sorcery, bucket_file);
483  ao2_ref(bucket_file, -1);
484  return NULL;
485  }
486 
487  return bucket_file;
488 }
489 
490 static int bucket_http_wizard_delete(const struct ast_sorcery *sorcery, void *data,
491  void *object)
492 {
493  struct ast_bucket_file *bucket_file = object;
494 
495  unlink(bucket_file->path);
496 
497  return 0;
498 }
499 
501  .name = "http",
502  .create = bucket_http_wizard_create,
503  .retrieve_id = bucket_http_wizard_retrieve_id,
504  .delete = bucket_http_wizard_delete,
505  .is_stale = bucket_http_wizard_is_stale,
506 };
507 
509  .name = "http",
510  .create = bucket_http_wizard_create,
511  .retrieve_id = bucket_http_wizard_retrieve_id,
512  .delete = bucket_http_wizard_delete,
513  .is_stale = bucket_http_wizard_is_stale,
514 };
515 
517  .name = "https",
518  .create = bucket_http_wizard_create,
519  .retrieve_id = bucket_http_wizard_retrieve_id,
520  .delete = bucket_http_wizard_delete,
521  .is_stale = bucket_http_wizard_is_stale,
522 };
523 
525  .name = "https",
526  .create = bucket_http_wizard_create,
527  .retrieve_id = bucket_http_wizard_retrieve_id,
528  .delete = bucket_http_wizard_delete,
529  .is_stale = bucket_http_wizard_is_stale,
530 };
531 
532 static int unload_module(void)
533 {
534  return 0;
535 }
536 
537 static int load_module(void)
538 {
539  if (ast_bucket_scheme_register("http", &http_bucket_wizard, &http_bucket_file_wizard,
540  NULL, NULL)) {
541  ast_log(LOG_ERROR, "Failed to register Bucket HTTP wizard scheme implementation\n");
543  }
544 
545  if (ast_bucket_scheme_register("https", &https_bucket_wizard, &https_bucket_file_wizard,
546  NULL, NULL)) {
547  ast_log(LOG_ERROR, "Failed to register Bucket HTTPS wizard scheme implementation\n");
549  }
550 
552 }
553 
554 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_DEFAULT, "HTTP Media Cache Backend",
555  .support_level = AST_MODULE_SUPPORT_CORE,
556  .load = load_module,
557  .unload = unload_module,
558  .requires = "res_curl",
559  );
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
const char * ast_uri_path(const struct ast_uri *uri)
Retrieve the uri path.
Definition: uri.c:135
#define LOG_WARNING
Definition: logger.h:274
static int unload_module(void)
Stores parsed uri information.
Definition: uri.c:30
Full structure for sorcery.
Definition: sorcery.c:230
struct ast_uri * ast_uri_parse(const char *uri)
Parse the given uri into a structure.
Definition: uri.c:195
struct timeval ast_tvnow(void)
Returns current timeval. Meant to replace calls to gettimeofday().
Definition: time.h:150
#define ast_strdup(str)
A wrapper for strdup()
Definition: astmm.h:243
const char * str
Definition: app_jack.c:147
Generic File Format Support. Should be included by clients of the file handling routines. File service providers should instead include mod_format.h.
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
const char * ext
Definition: http.c:147
Bucket File API.
static int bucket_http_wizard_is_stale(const struct ast_sorcery *sorcery, void *data, void *object)
static char * file_extension_from_string(const char *str, char *buffer, size_t capacity)
const char * value
Value of the attribute.
Definition: bucket.h:51
#define ast_debug(level,...)
Log a DEBUG message.
Definition: logger.h:444
#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
static void bucket_file_set_extension(struct ast_bucket_file *bucket_file)
#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.
static char * file_extension_from_url_path(struct ast_bucket_file *bucket_file, char *buffer, size_t capacity)
#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 char * file_extension_from_url(struct ast_bucket_file *bucket_file, char *buffer, size_t capacity)
#define ast_strlen_zero(a)
Definition: muted.c:73
static char * file_extension_from_content_type(struct ast_bucket_file *bucket_file, char *buffer, size_t capacity)
#define ast_free(a)
Definition: astmm.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)
static void normalize_content_type_header(char *content_type)
Module has failed to load, may be in an inconsistent state.
Definition: module.h:78
struct ast_format * ast_get_format_for_file_ext(const char *file_ext)
Get the ast_format associated with the given file extension.
Definition: file.c:1938
char uri[0]
Definition: uri.c:44
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
void ast_copy_string(char *dst, const char *src, size_t size)
Size-limited null-terminating string copy.
Definition: strings.h:401
#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
int ast_get_extension_for_mime_type(const char *mime_type, char *buffer, size_t capacity)
Get a suitable filename extension for the given MIME type.
Definition: file.c:1951
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.