blob: 75dcf0045f3e83c194a079487ec5c7f2b18b96d1 [file] [log] [blame]
scientiamobiled0027ed2016-11-04 10:55:08 +01001#include <stdio.h>
2#include <stdarg.h>
3
4#include <common/cfgparse.h>
5#include <common/chunk.h>
6#include <common/buffer.h>
Willy Tarreaudc2ed472016-12-21 20:20:17 +01007#include <common/errors.h>
Willy Tarreau0108d902018-11-25 19:14:37 +01008#include <common/initcall.h>
Willy Tarreau80713382018-11-26 10:19:54 +01009#include <types/global.h>
scientiamobiled0027ed2016-11-04 10:55:08 +010010#include <proto/arg.h>
11#include <proto/log.h>
12#include <proto/proto_http.h>
13#include <proto/sample.h>
14#include <ebsttree.h>
15#include <ebmbtree.h>
scientiamobiled0027ed2016-11-04 10:55:08 +010016
Willy Tarreaue5d31692016-11-08 18:47:25 +010017#include <wurfl/wurfl.h>
18
Willy Tarreau350c1c62016-12-21 14:57:34 +010019static struct {
20 char *data_file; /* the WURFL data file */
21 char *cache_size; /* the WURFL cache parameters */
22 int engine_mode; /* the WURFL engine mode */
23 int useragent_priority; /* the WURFL ua priority */
24 struct list patch_file_list; /* the list of WURFL patch file to use */
25 char information_list_separator; /* the separator used in request to separate values */
26 struct list information_list; /* the list of WURFL data to return into request */
27 void *handle; /* the handle to WURFL engine */
28 struct eb_root btree; /* btree containing info (name/type) on WURFL data to return */
29} global_wurfl = {
30 .data_file = NULL,
31 .cache_size = NULL,
32 .engine_mode = -1,
33 .useragent_priority = -1,
34 .information_list_separator = ',',
35 .information_list = LIST_HEAD_INIT(global_wurfl.information_list),
36 .patch_file_list = LIST_HEAD_INIT(global_wurfl.patch_file_list),
37 .handle = NULL,
38};
scientiamobiled0027ed2016-11-04 10:55:08 +010039
40#ifdef WURFL_DEBUG
41inline static void ha_wurfl_log(char * message, ...)
42{
43 char logbuf[256];
44 va_list argp;
45
46 va_start(argp, message);
47 vsnprintf(logbuf, sizeof(logbuf), message, argp);
48 va_end(argp);
49 send_log(NULL, LOG_NOTICE, logbuf, NULL);
50}
51#else
52inline static void ha_wurfl_log(char * message, ...)
53{
54}
55#endif
56
57#define HA_WURFL_MAX_HEADER_LENGTH 1024
58
Willy Tarreaue5d31692016-11-08 18:47:25 +010059typedef char *(*PROP_CALLBACK_FUNC)(wurfl_handle wHandle, wurfl_device_handle dHandle);
60
61enum wurfl_data_type {
62 HA_WURFL_DATA_TYPE_UNKNOWN = 0,
63 HA_WURFL_DATA_TYPE_CAP = 100,
64 HA_WURFL_DATA_TYPE_VCAP = 200,
65 HA_WURFL_DATA_TYPE_PROPERTY = 300
66};
67
68typedef struct {
69 char *name;
70 enum wurfl_data_type type;
71 PROP_CALLBACK_FUNC func_callback;
72 struct ebmb_node nd;
73} wurfl_data_t;
74
scientiamobiled0027ed2016-11-04 10:55:08 +010075static const char HA_WURFL_MODULE_VERSION[] = "1.0";
76static const char HA_WURFL_ISDEVROOT_FALSE[] = "FALSE";
77static const char HA_WURFL_ISDEVROOT_TRUE[] = "TRUE";
78static const char HA_WURFL_TARGET_ACCURACY[] = "accuracy";
79static const char HA_WURFL_TARGET_PERFORMANCE[] = "performance";
80static const char HA_WURFL_PRIORITY_PLAIN[] = "plain";
81static const char HA_WURFL_PRIORITY_SIDELOADED_BROWSER[] = "sideloaded_browser";
82static const char HA_WURFL_MIN_ENGINE_VERSION_MANDATORY[] = "1.8.0.0";
83
84static const char HA_WURFL_DATA_TYPE_UNKNOWN_STRING[] = "unknown";
85static const char HA_WURFL_DATA_TYPE_CAP_STRING[] = "capability";
86static const char HA_WURFL_DATA_TYPE_VCAP_STRING[] = "virtual_capability";
87static const char HA_WURFL_DATA_TYPE_PROPERTY_STRING[] = "property";
88
89static const char *ha_wurfl_retrieve_header(const char *header_name, const void *wh);
90static const char *ha_wurfl_get_wurfl_root_id (wurfl_handle wHandle, wurfl_device_handle dHandle);
91static const char *ha_wurfl_get_wurfl_id (wurfl_handle wHandle, wurfl_device_handle dHandle);
92static const char *ha_wurfl_get_wurfl_isdevroot (wurfl_handle wHandle, wurfl_device_handle dHandle);
93static const char *ha_wurfl_get_wurfl_useragent (wurfl_handle wHandle, wurfl_device_handle dHandle);
94static const char *ha_wurfl_get_wurfl_api_version (wurfl_handle wHandle, wurfl_device_handle dHandle);
95static const char *ha_wurfl_get_wurfl_engine_target (wurfl_handle wHandle, wurfl_device_handle dHandle);
96static const char *ha_wurfl_get_wurfl_info (wurfl_handle wHandle, wurfl_device_handle dHandle);
97static const char *ha_wurfl_get_wurfl_last_load_time (wurfl_handle wHandle, wurfl_device_handle dHandle);
98static const char *ha_wurfl_get_wurfl_normalized_useragent (wurfl_handle wHandle, wurfl_device_handle dHandle);
99static const char *ha_wurfl_get_wurfl_useragent_priority (wurfl_handle wHandle, wurfl_device_handle dHandle);
100static const char *(*ha_wurfl_get_property_callback(char *name)) (wurfl_handle wHandle, wurfl_device_handle dHandle);
101
102// ordered property=>function map, suitable for binary search
103static const struct {
104 const char *name;
105 const char *(*func)(wurfl_handle wHandle, wurfl_device_handle dHandle);
106} wurfl_properties_function_map [] = {
107 {"wurfl_api_version", ha_wurfl_get_wurfl_api_version},
108 {"wurfl_engine_target", ha_wurfl_get_wurfl_engine_target},
109 {"wurfl_id", ha_wurfl_get_wurfl_id },
110 {"wurfl_info", ha_wurfl_get_wurfl_info },
111 {"wurfl_isdevroot", ha_wurfl_get_wurfl_isdevroot},
112 {"wurfl_last_load_time", ha_wurfl_get_wurfl_last_load_time},
113 {"wurfl_normalized_useragent", ha_wurfl_get_wurfl_normalized_useragent},
114 {"wurfl_useragent", ha_wurfl_get_wurfl_useragent},
115 {"wurfl_useragent_priority", ha_wurfl_get_wurfl_useragent_priority },
116 {"wurfl_root_id", ha_wurfl_get_wurfl_root_id},
117};
118static const int HA_WURFL_PROPERTIES_NBR = 10;
119
120typedef struct {
121 struct list list;
122 wurfl_data_t data;
123} wurfl_information_t;
124
125typedef struct {
126 struct list list;
127 char *patch_file_path;
128} wurfl_patches_t;
129
130typedef struct {
131 struct sample *wsmp;
132 char header_value[HA_WURFL_MAX_HEADER_LENGTH + 1];
133} ha_wurfl_header_t;
134
135/*
136 * configuration parameters parsing functions
137 */
138static int ha_wurfl_cfg_data_file(char **args, int section_type, struct proxy *curpx,
139 struct proxy *defpx, const char *file, int line,
140 char **err)
141{
142
143 if (*(args[1]) == 0) {
144 memprintf(err, "WURFL: %s expects a value.\n", args[0]);
145 return -1;
146 }
147
Willy Tarreau350c1c62016-12-21 14:57:34 +0100148 global_wurfl.data_file = strdup(args[1]);
scientiamobiled0027ed2016-11-04 10:55:08 +0100149 return 0;
150}
151
152static int ha_wurfl_cfg_cache(char **args, int section_type, struct proxy *curpx,
153 struct proxy *defpx, const char *file, int line,
154 char **err)
155{
156 if (*(args[1]) == 0) {
157 memprintf(err, "WURFL: %s expects a value.\n", args[0]);
158 return -1;
159 }
160
Willy Tarreau350c1c62016-12-21 14:57:34 +0100161 global_wurfl.cache_size = strdup(args[1]);
scientiamobiled0027ed2016-11-04 10:55:08 +0100162 return 0;
163}
164
165static int ha_wurfl_cfg_engine_mode(char **args, int section_type, struct proxy *curpx,
166 struct proxy *defpx, const char *file, int line,
167 char **err)
168{
169 if (*(args[1]) == 0) {
170 memprintf(err, "WURFL: %s expects a value.\n", args[0]);
171 return -1;
172 }
173
174 if (!strcmp(args[1],HA_WURFL_TARGET_ACCURACY)) {
Willy Tarreau350c1c62016-12-21 14:57:34 +0100175 global_wurfl.engine_mode = WURFL_ENGINE_TARGET_HIGH_ACCURACY;
scientiamobiled0027ed2016-11-04 10:55:08 +0100176 return 0;
177 }
178
179 if (!strcmp(args[1],HA_WURFL_TARGET_PERFORMANCE)) {
Willy Tarreau350c1c62016-12-21 14:57:34 +0100180 global_wurfl.engine_mode = WURFL_ENGINE_TARGET_HIGH_PERFORMANCE;
scientiamobiled0027ed2016-11-04 10:55:08 +0100181 return 0;
182 }
183
184 memprintf(err, "WURFL: %s valid values are %s or %s.\n", args[0], HA_WURFL_TARGET_PERFORMANCE, HA_WURFL_TARGET_ACCURACY);
185 return -1;
186}
187
188static int ha_wurfl_cfg_information_list_separator(char **args, int section_type, struct proxy *curpx,
189 struct proxy *defpx, const char *file, int line,
190 char **err)
191{
192 if (*(args[1]) == 0) {
193 memprintf(err, "WURFL: %s expects a single character.\n", args[0]);
194 return -1;
195 }
196
197 if (strlen(args[1]) > 1) {
198 memprintf(err, "WURFL: %s expects a single character, got %s.\n", args[0], args[1]);
199 return -1;
200 }
201
Willy Tarreau350c1c62016-12-21 14:57:34 +0100202 global_wurfl.information_list_separator = *args[1];
scientiamobiled0027ed2016-11-04 10:55:08 +0100203 return 0;
204}
205
206static int ha_wurfl_cfg_information_list(char **args, int section_type, struct proxy *curpx,
207 struct proxy *defpx, const char *file, int line,
208 char **err)
209{
210 int argIdx = 1;
211 wurfl_information_t *wi;
212
213 if (*(args[argIdx]) == 0) {
214 memprintf(err, "WURFL: %s expects a value.\n", args[0]);
215 return -1;
216 }
217
218 while (*(args[argIdx])) {
219 wi = calloc(1, sizeof(*wi));
220
221 if (wi == NULL) {
222 memprintf(err, "WURFL: Error allocating memory for %s element.\n", args[0]);
223 return -1;
224 }
225
226 wi->data.name = strdup(args[argIdx]);
227 wi->data.type = HA_WURFL_DATA_TYPE_UNKNOWN;
228 wi->data.func_callback = NULL;
Willy Tarreau350c1c62016-12-21 14:57:34 +0100229 LIST_ADDQ(&global_wurfl.information_list, &wi->list);
scientiamobiled0027ed2016-11-04 10:55:08 +0100230 ++argIdx;
231 }
232
233 return 0;
234}
235
236static int ha_wurfl_cfg_patch_file_list(char **args, int section_type, struct proxy *curpx,
237 struct proxy *defpx, const char *file, int line,
238 char **err)
239{
240 int argIdx = 1;
241 wurfl_patches_t *wp;
242
243 if (*(args[argIdx]) == 0) {
244 memprintf(err, "WURFL: %s expects a value.\n", args[0]);
245 return -1;
246 }
247
248 while (*(args[argIdx])) {
249 wp = calloc(1, sizeof(*wp));
250
251 if (wp == NULL) {
252 memprintf(err, "WURFL: Error allocating memory for %s element.\n", args[0]);
253 return -1;
254 }
255
256 wp->patch_file_path = strdup(args[argIdx]);
Willy Tarreau350c1c62016-12-21 14:57:34 +0100257 LIST_ADDQ(&global_wurfl.patch_file_list, &wp->list);
scientiamobiled0027ed2016-11-04 10:55:08 +0100258 ++argIdx;
259 }
260
261 return 0;
262}
263
264static int ha_wurfl_cfg_useragent_priority(char **args, int section_type, struct proxy *curpx,
265 struct proxy *defpx, const char *file, int line,
266 char **err)
267{
268 if (*(args[1]) == 0) {
269 memprintf(err, "WURFL: %s expects a value.\n", args[0]);
270 return -1;
271 }
272
273 if (!strcmp(args[1],HA_WURFL_PRIORITY_PLAIN)) {
Willy Tarreau350c1c62016-12-21 14:57:34 +0100274 global_wurfl.useragent_priority = WURFL_USERAGENT_PRIORITY_USE_PLAIN_USERAGENT;
scientiamobiled0027ed2016-11-04 10:55:08 +0100275 return 0;
276 }
277
278 if (!strcmp(args[1],HA_WURFL_PRIORITY_SIDELOADED_BROWSER)) {
Willy Tarreau350c1c62016-12-21 14:57:34 +0100279 global_wurfl.useragent_priority = WURFL_USERAGENT_PRIORITY_OVERRIDE_SIDELOADED_BROWSER_USERAGENT;
scientiamobiled0027ed2016-11-04 10:55:08 +0100280 return 0;
281 }
282
283 memprintf(err, "WURFL: %s valid values are %s or %s.\n", args[0], HA_WURFL_PRIORITY_PLAIN, HA_WURFL_PRIORITY_SIDELOADED_BROWSER);
284 return -1;
285}
286
287/*
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100288 * module init / deinit functions. Returns 0 if OK, or a combination of ERR_*.
scientiamobiled0027ed2016-11-04 10:55:08 +0100289 */
290
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100291static int ha_wurfl_init(void)
scientiamobiled0027ed2016-11-04 10:55:08 +0100292{
293 wurfl_information_t *wi;
294 wurfl_patches_t *wp;
295 wurfl_data_t * wn;
296 int wurfl_result_code = WURFL_OK;
297 int len;
298
299 send_log(NULL, LOG_NOTICE, "WURFL: Loading module v.%s\n", HA_WURFL_MODULE_VERSION);
300 // creating WURFL handler
Willy Tarreau350c1c62016-12-21 14:57:34 +0100301 global_wurfl.handle = wurfl_create();
scientiamobiled0027ed2016-11-04 10:55:08 +0100302
Willy Tarreau350c1c62016-12-21 14:57:34 +0100303 if (global_wurfl.handle == NULL) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100304 ha_warning("WURFL: Engine handler creation failed");
scientiamobiled0027ed2016-11-04 10:55:08 +0100305 send_log(NULL, LOG_WARNING, "WURFL: Engine handler creation failed\n");
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100306 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100307 }
308
309 send_log(NULL, LOG_NOTICE, "WURFL: Engine handler created - API version %s\n", wurfl_get_api_version() );
310
311 // set wurfl data file
Willy Tarreau350c1c62016-12-21 14:57:34 +0100312 if (global_wurfl.data_file == NULL) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100313 ha_warning("WURFL: missing wurfl-data-file parameter in global configuration\n");
scientiamobiled0027ed2016-11-04 10:55:08 +0100314 send_log(NULL, LOG_WARNING, "WURFL: missing wurfl-data-file parameter in global configuration\n");
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100315 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100316 }
317
Christopher Faulete8ca4342017-10-25 17:23:02 +0200318 if (global.nbthread > 1) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100319 ha_alert("WURFL: multithreading is not supported for now.\n");
Christopher Faulete8ca4342017-10-25 17:23:02 +0200320 return (ERR_FATAL | ERR_ALERT);
321 }
322
Willy Tarreau350c1c62016-12-21 14:57:34 +0100323 if (wurfl_set_root(global_wurfl.handle, global_wurfl.data_file) != WURFL_OK) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100324 ha_warning("WURFL: Engine setting root file failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreau350c1c62016-12-21 14:57:34 +0100325 send_log(NULL, LOG_WARNING, "WURFL: Engine setting root file failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100326 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100327 }
328
Willy Tarreau350c1c62016-12-21 14:57:34 +0100329 send_log(NULL, LOG_NOTICE, "WURFL: Engine root file set to %s\n", global_wurfl.data_file);
scientiamobiled0027ed2016-11-04 10:55:08 +0100330 // just a log to inform which separator char has to be used
Willy Tarreau350c1c62016-12-21 14:57:34 +0100331 send_log(NULL, LOG_NOTICE, "WURFL: Information list separator set to '%c'\n", global_wurfl.information_list_separator);
scientiamobiled0027ed2016-11-04 10:55:08 +0100332
333 // load wurfl data needed ( and filter whose are supposed to be capabilities )
Willy Tarreau350c1c62016-12-21 14:57:34 +0100334 if (LIST_ISEMPTY(&global_wurfl.information_list)) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100335 ha_warning("WURFL: missing wurfl-information-list parameter in global configuration\n");
scientiamobiled0027ed2016-11-04 10:55:08 +0100336 send_log(NULL, LOG_WARNING, "WURFL: missing wurfl-information-list parameter in global configuration\n");
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100337 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100338 } else {
339 // ebtree initialization
Willy Tarreau350c1c62016-12-21 14:57:34 +0100340 global_wurfl.btree = EB_ROOT;
scientiamobiled0027ed2016-11-04 10:55:08 +0100341
342 // checking if informations are valid WURFL data ( cap, vcaps, properties )
Willy Tarreau350c1c62016-12-21 14:57:34 +0100343 list_for_each_entry(wi, &global_wurfl.information_list, list) {
scientiamobiled0027ed2016-11-04 10:55:08 +0100344 // check if information is already loaded looking into btree
Willy Tarreau350c1c62016-12-21 14:57:34 +0100345 if (ebst_lookup(&global_wurfl.btree, wi->data.name) == NULL) {
scientiamobiled0027ed2016-11-04 10:55:08 +0100346 if ((wi->data.func_callback = (PROP_CALLBACK_FUNC) ha_wurfl_get_property_callback(wi->data.name)) != NULL) {
347 wi->data.type = HA_WURFL_DATA_TYPE_PROPERTY;
348 ha_wurfl_log("WURFL: [%s] is a valid wurfl data [property]\n",wi->data.name);
Willy Tarreau350c1c62016-12-21 14:57:34 +0100349 } else if (wurfl_has_virtual_capability(global_wurfl.handle, wi->data.name)) {
scientiamobiled0027ed2016-11-04 10:55:08 +0100350 wi->data.type = HA_WURFL_DATA_TYPE_VCAP;
351 ha_wurfl_log("WURFL: [%s] is a valid wurfl data [virtual capability]\n",wi->data.name);
352 } else {
353 // by default a cap type is assumed to be and we control it on engine load
354 wi->data.type = HA_WURFL_DATA_TYPE_CAP;
355
Willy Tarreau350c1c62016-12-21 14:57:34 +0100356 if (wurfl_add_requested_capability(global_wurfl.handle, wi->data.name) != WURFL_OK) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100357 ha_warning("WURFL: capability filtering failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreau350c1c62016-12-21 14:57:34 +0100358 send_log(NULL, LOG_WARNING, "WURFL: capability filtering failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100359 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100360 }
361
362 ha_wurfl_log("WURFL: [%s] treated as wurfl capability. Will check its validity later, on engine load\n",wi->data.name);
363 }
364
365 // ebtree insert here
366 len = strlen(wi->data.name);
367
368 wn = malloc(sizeof(wurfl_data_t) + len + 1);
369
370 if (wn == NULL) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100371 ha_warning("WURFL: Error allocating memory for information tree element.\n");
scientiamobiled0027ed2016-11-04 10:55:08 +0100372 send_log(NULL, LOG_WARNING, "WURFL: Error allocating memory for information tree element.\n");
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100373 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100374 }
375
376 wn->name = wi->data.name;
377 wn->type = wi->data.type;
378 wn->func_callback = wi->data.func_callback;
379 memcpy(wn->nd.key, wi->data.name, len);
380 wn->nd.key[len] = 0;
381
Willy Tarreau350c1c62016-12-21 14:57:34 +0100382 if (!ebst_insert(&global_wurfl.btree, &wn->nd)) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100383 ha_warning("WURFL: [%s] not inserted in btree\n",wn->name);
scientiamobiled0027ed2016-11-04 10:55:08 +0100384 send_log(NULL, LOG_WARNING, "WURFL: [%s] not inserted in btree\n",wn->name);
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100385 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100386 }
387
388 } else {
389 ha_wurfl_log("WURFL: [%s] already loaded\n",wi->data.name);
390 }
391
392 }
393
394 }
395
396 // filtering mandatory capabilities if engine version < 1.8.0.0
397 if (strcmp(wurfl_get_api_version(), HA_WURFL_MIN_ENGINE_VERSION_MANDATORY) < 0) {
398 wurfl_capability_enumerator_handle hmandatorycapabilityenumerator;
399 ha_wurfl_log("WURFL: Engine version %s < %s - Filtering mandatory capabilities\n", wurfl_get_api_version(), HA_WURFL_MIN_ENGINE_VERSION_MANDATORY);
Willy Tarreau350c1c62016-12-21 14:57:34 +0100400 hmandatorycapabilityenumerator = wurfl_get_mandatory_capability_enumerator(global_wurfl.handle);
scientiamobiled0027ed2016-11-04 10:55:08 +0100401
402 while (wurfl_capability_enumerator_is_valid(hmandatorycapabilityenumerator)) {
403 char *name = (char *)wurfl_capability_enumerator_get_name(hmandatorycapabilityenumerator);
404
Willy Tarreau350c1c62016-12-21 14:57:34 +0100405 if (ebst_lookup(&global_wurfl.btree, name) == NULL) {
406 if (wurfl_add_requested_capability(global_wurfl.handle, name) != WURFL_OK) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100407 ha_warning("WURFL: Engine adding mandatory capability [%s] failed - %s\n", name, wurfl_get_error_message(global_wurfl.handle));
Willy Tarreau350c1c62016-12-21 14:57:34 +0100408 send_log(NULL, LOG_WARNING, "WURFL: Adding mandatory capability [%s] failed - %s\n", name, wurfl_get_error_message(global_wurfl.handle));
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100409 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100410 }
411
412 ha_wurfl_log("WURFL: Mandatory capability [%s] added\n", name);
413 } else {
414 ha_wurfl_log("WURFL: Mandatory capability [%s] already filtered\n", name);
415 }
416
417 wurfl_capability_enumerator_move_next(hmandatorycapabilityenumerator);
418 }
419
420 wurfl_capability_enumerator_destroy(hmandatorycapabilityenumerator);
421 }
422
423 // adding WURFL patches if needed
Willy Tarreau350c1c62016-12-21 14:57:34 +0100424 if (!LIST_ISEMPTY(&global_wurfl.patch_file_list)) {
scientiamobiled0027ed2016-11-04 10:55:08 +0100425
Willy Tarreau350c1c62016-12-21 14:57:34 +0100426 list_for_each_entry(wp, &global_wurfl.patch_file_list, list) {
427 if (wurfl_add_patch(global_wurfl.handle, wp->patch_file_path) != WURFL_OK) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100428 ha_warning("WURFL: Engine adding patch file failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreau350c1c62016-12-21 14:57:34 +0100429 send_log(NULL, LOG_WARNING, "WURFL: Adding engine patch file failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100430 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100431 }
432 send_log(NULL, LOG_NOTICE, "WURFL: Engine patch file added %s\n", wp->patch_file_path);
433
434 }
435
436 }
437
438 // setting cache provider if specified in cfg, otherwise let engine choose
Willy Tarreau350c1c62016-12-21 14:57:34 +0100439 if (global_wurfl.cache_size != NULL) {
440 if (strpbrk(global_wurfl.cache_size, ",") != NULL) {
441 wurfl_result_code = wurfl_set_cache_provider(global_wurfl.handle, WURFL_CACHE_PROVIDER_DOUBLE_LRU, global_wurfl.cache_size) ;
scientiamobiled0027ed2016-11-04 10:55:08 +0100442 } else {
Willy Tarreau350c1c62016-12-21 14:57:34 +0100443 if (strcmp(global_wurfl.cache_size, "0")) {
444 wurfl_result_code = wurfl_set_cache_provider(global_wurfl.handle, WURFL_CACHE_PROVIDER_LRU, global_wurfl.cache_size) ;
scientiamobiled0027ed2016-11-04 10:55:08 +0100445 } else {
Willy Tarreau350c1c62016-12-21 14:57:34 +0100446 wurfl_result_code = wurfl_set_cache_provider(global_wurfl.handle, WURFL_CACHE_PROVIDER_NONE, 0);
scientiamobiled0027ed2016-11-04 10:55:08 +0100447 }
448
449 }
450
451 if (wurfl_result_code != WURFL_OK) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100452 ha_warning("WURFL: Setting cache to [%s] failed - %s\n", global_wurfl.cache_size, wurfl_get_error_message(global_wurfl.handle));
Willy Tarreau350c1c62016-12-21 14:57:34 +0100453 send_log(NULL, LOG_WARNING, "WURFL: Setting cache to [%s] failed - %s\n", global_wurfl.cache_size, wurfl_get_error_message(global_wurfl.handle));
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100454 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100455 }
456
Willy Tarreau350c1c62016-12-21 14:57:34 +0100457 send_log(NULL, LOG_NOTICE, "WURFL: Cache set to [%s]\n", global_wurfl.cache_size);
scientiamobiled0027ed2016-11-04 10:55:08 +0100458 }
459
460 // setting engine mode if specified in cfg, otherwise let engine choose
Willy Tarreau350c1c62016-12-21 14:57:34 +0100461 if (global_wurfl.engine_mode != -1) {
462 if (wurfl_set_engine_target(global_wurfl.handle, global_wurfl.engine_mode) != WURFL_OK ) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100463 ha_warning("WURFL: Setting engine target failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreau350c1c62016-12-21 14:57:34 +0100464 send_log(NULL, LOG_WARNING, "WURFL: Setting engine target failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100465 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100466 }
467
468 }
469
Willy Tarreau350c1c62016-12-21 14:57:34 +0100470 send_log(NULL, LOG_NOTICE, "WURFL: Engine target set to [%s]\n", (global_wurfl.engine_mode == WURFL_ENGINE_TARGET_HIGH_PERFORMANCE) ? (HA_WURFL_TARGET_PERFORMANCE) : (HA_WURFL_TARGET_ACCURACY) );
scientiamobiled0027ed2016-11-04 10:55:08 +0100471
472 // setting ua priority if specified in cfg, otherwise let engine choose
Willy Tarreau350c1c62016-12-21 14:57:34 +0100473 if (global_wurfl.useragent_priority != -1) {
474 if (wurfl_set_useragent_priority(global_wurfl.handle, global_wurfl.useragent_priority) != WURFL_OK ) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100475 ha_warning("WURFL: Setting engine useragent priority failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreau350c1c62016-12-21 14:57:34 +0100476 send_log(NULL, LOG_WARNING, "WURFL: Setting engine useragent priority failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100477 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100478 }
479
480 }
481
Willy Tarreau350c1c62016-12-21 14:57:34 +0100482 send_log(NULL, LOG_NOTICE, "WURFL: Engine useragent priority set to [%s]\n", (global_wurfl.useragent_priority == WURFL_USERAGENT_PRIORITY_USE_PLAIN_USERAGENT) ? (HA_WURFL_PRIORITY_PLAIN) : (HA_WURFL_PRIORITY_SIDELOADED_BROWSER) );
scientiamobiled0027ed2016-11-04 10:55:08 +0100483
484 // loading WURFL engine
Willy Tarreau350c1c62016-12-21 14:57:34 +0100485 if (wurfl_load(global_wurfl.handle) != WURFL_OK) {
Christopher Faulet767a84b2017-11-24 16:50:31 +0100486 ha_warning("WURFL: Engine load failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreau350c1c62016-12-21 14:57:34 +0100487 send_log(NULL, LOG_WARNING, "WURFL: Engine load failed - %s\n", wurfl_get_error_message(global_wurfl.handle));
Willy Tarreaudc2ed472016-12-21 20:20:17 +0100488 return ERR_WARN;
scientiamobiled0027ed2016-11-04 10:55:08 +0100489 }
490
491 send_log(NULL, LOG_NOTICE, "WURFL: Engine loaded\n");
492 send_log(NULL, LOG_NOTICE, "WURFL: Module load completed\n");
493 return 0;
494}
495
Willy Tarreau800f93f2016-12-21 20:52:38 +0100496static void ha_wurfl_deinit(void)
scientiamobiled0027ed2016-11-04 10:55:08 +0100497{
498 wurfl_information_t *wi, *wi2;
499 wurfl_patches_t *wp, *wp2;
500
501 send_log(NULL, LOG_NOTICE, "WURFL: Unloading module v.%s\n", HA_WURFL_MODULE_VERSION);
Willy Tarreau350c1c62016-12-21 14:57:34 +0100502 wurfl_destroy(global_wurfl.handle);
503 global_wurfl.handle = NULL;
504 free(global_wurfl.data_file);
505 global_wurfl.data_file = NULL;
506 free(global_wurfl.cache_size);
507 global_wurfl.cache_size = NULL;
scientiamobiled0027ed2016-11-04 10:55:08 +0100508
Willy Tarreau350c1c62016-12-21 14:57:34 +0100509 list_for_each_entry_safe(wi, wi2, &global_wurfl.information_list, list) {
scientiamobiled0027ed2016-11-04 10:55:08 +0100510 LIST_DEL(&wi->list);
511 free(wi);
512 }
513
Willy Tarreau350c1c62016-12-21 14:57:34 +0100514 list_for_each_entry_safe(wp, wp2, &global_wurfl.patch_file_list, list) {
scientiamobiled0027ed2016-11-04 10:55:08 +0100515 LIST_DEL(&wp->list);
516 free(wp);
517 }
518
519 send_log(NULL, LOG_NOTICE, "WURFL: Module unloaded\n");
520}
521
522static int ha_wurfl_get_all(const struct arg *args, struct sample *smp, const char *kw, void *private)
523{
524 wurfl_device_handle dHandle;
Willy Tarreau83061a82018-07-13 11:56:34 +0200525 struct buffer *temp;
scientiamobiled0027ed2016-11-04 10:55:08 +0100526 wurfl_information_t *wi;
527 ha_wurfl_header_t wh;
528
529 ha_wurfl_log("WURFL: starting ha_wurfl_get_all\n");
530 wh.wsmp = smp;
Willy Tarreau350c1c62016-12-21 14:57:34 +0100531 dHandle = wurfl_lookup(global_wurfl.handle, &ha_wurfl_retrieve_header, &wh);
scientiamobiled0027ed2016-11-04 10:55:08 +0100532
533 if (!dHandle) {
Willy Tarreau350c1c62016-12-21 14:57:34 +0100534 ha_wurfl_log("WURFL: unable to retrieve device from request %s\n", wurfl_get_error_message(global_wurfl.handle));
scientiamobiled0027ed2016-11-04 10:55:08 +0100535 return 1;
536 }
537
538 temp = get_trash_chunk();
539 chunk_reset(temp);
540
Willy Tarreau350c1c62016-12-21 14:57:34 +0100541 list_for_each_entry(wi, &global_wurfl.information_list, list) {
542 chunk_appendf(temp, "%c", global_wurfl.information_list_separator);
scientiamobiled0027ed2016-11-04 10:55:08 +0100543
544 switch(wi->data.type) {
545 case HA_WURFL_DATA_TYPE_UNKNOWN :
546 ha_wurfl_log("WURFL: %s is of an %s type\n", wi->data.name, HA_WURFL_DATA_TYPE_UNKNOWN_STRING);
547#ifdef WURFL_HEADER_WITH_DETAILS
548 // write WURFL property type and name before its value...
549 chunk_appendf(temp, "%s=%s", HA_WURFL_DATA_TYPE_UNKNOWN_STRING, wi->data.name);
550#endif
551 break;
552 case HA_WURFL_DATA_TYPE_CAP :
553 ha_wurfl_log("WURFL: %s is a %s\n", wi->data.name, HA_WURFL_DATA_TYPE_CAP_STRING);
554#ifdef WURFL_HEADER_WITH_DETAILS
555 // write WURFL property type and name before its value...
556 chunk_appendf(temp, "%s=%s=", HA_WURFL_DATA_TYPE_CAP_STRING, wi->data.name);
557#endif
558 chunk_appendf(temp, "%s", wurfl_device_get_capability(dHandle, wi->data.name));
559 break;
560 case HA_WURFL_DATA_TYPE_VCAP :
561 ha_wurfl_log("WURFL: %s is a %s\n", wi->data.name, HA_WURFL_DATA_TYPE_VCAP_STRING);
562#ifdef WURFL_HEADER_WITH_DETAILS
563 // write WURFL property type and name before its value...
564 chunk_appendf(temp, "%s=%s=", HA_WURFL_DATA_TYPE_VCAP_STRING, wi->data.name);
565#endif
566 chunk_appendf(temp, "%s", wurfl_device_get_virtual_capability(dHandle, wi->data.name));
567 break;
568 case HA_WURFL_DATA_TYPE_PROPERTY :
569 ha_wurfl_log("WURFL: %s is a %s\n", wi->data.name, HA_WURFL_DATA_TYPE_PROPERTY_STRING);
570#ifdef WURFL_HEADER_WITH_DETAILS
571 // write WURFL property type and name before its value...
572 chunk_appendf(temp, "%s=%s=", HA_WURFL_DATA_TYPE_PROPERTY_STRING, wi->data.name);
573#endif
Willy Tarreau350c1c62016-12-21 14:57:34 +0100574 chunk_appendf(temp, "%s", wi->data.func_callback(global_wurfl.handle, dHandle));
scientiamobiled0027ed2016-11-04 10:55:08 +0100575 break;
576 }
577
578 }
579
580 wurfl_device_destroy(dHandle);
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200581 smp->data.u.str.area = temp->area;
582 smp->data.u.str.data = temp->data;
scientiamobiled0027ed2016-11-04 10:55:08 +0100583 return 1;
584}
585
586static int ha_wurfl_get(const struct arg *args, struct sample *smp, const char *kw, void *private)
587{
588 wurfl_device_handle dHandle;
Willy Tarreau83061a82018-07-13 11:56:34 +0200589 struct buffer *temp;
scientiamobiled0027ed2016-11-04 10:55:08 +0100590 wurfl_data_t *wn = NULL;
591 struct ebmb_node *node;
592 ha_wurfl_header_t wh;
593 int i = 0;
594
595 ha_wurfl_log("WURFL: starting ha_wurfl_get\n");
596 wh.wsmp = smp;
Willy Tarreau350c1c62016-12-21 14:57:34 +0100597 dHandle = wurfl_lookup(global_wurfl.handle, &ha_wurfl_retrieve_header, &wh);
scientiamobiled0027ed2016-11-04 10:55:08 +0100598
599 if (!dHandle) {
Willy Tarreau350c1c62016-12-21 14:57:34 +0100600 ha_wurfl_log("WURFL: unable to retrieve device from request %s\n", wurfl_get_error_message(global_wurfl.handle));
scientiamobiled0027ed2016-11-04 10:55:08 +0100601 return 1;
602 }
603
604 temp = get_trash_chunk();
605 chunk_reset(temp);
606
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200607 while (args[i].data.str.area) {
Willy Tarreau350c1c62016-12-21 14:57:34 +0100608 chunk_appendf(temp, "%c", global_wurfl.information_list_separator);
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200609 node = ebst_lookup(&global_wurfl.btree, args[i].data.str.area);
scientiamobiled0027ed2016-11-04 10:55:08 +0100610 wn = container_of(node, wurfl_data_t, nd);
611
612 if (wn) {
613
614 switch(wn->type) {
615 case HA_WURFL_DATA_TYPE_UNKNOWN :
616 ha_wurfl_log("WURFL: %s is of an %s type\n", wn->name, HA_WURFL_DATA_TYPE_UNKNOWN_STRING);
617#ifdef WURFL_HEADER_WITH_DETAILS
618 // write WURFL property type and name before its value...
619 chunk_appendf(temp, "%s=%s", HA_WURFL_DATA_TYPE_UNKNOWN_STRING, wn->name);
620#endif
621 break;
622 case HA_WURFL_DATA_TYPE_CAP :
623 ha_wurfl_log("WURFL: %s is a %s\n", wn->name, HA_WURFL_DATA_TYPE_CAP_STRING);
624#ifdef WURFL_HEADER_WITH_DETAILS
625 // write WURFL property type and name before its value...
626 chunk_appendf(temp, "%s=%s=", HA_WURFL_DATA_TYPE_CAP_STRING, wn->name);
627#endif
628 chunk_appendf(temp, "%s", wurfl_device_get_capability(dHandle, wn->name));
629 break;
630 case HA_WURFL_DATA_TYPE_VCAP :
631 ha_wurfl_log("WURFL: %s is a %s\n", wn->name, HA_WURFL_DATA_TYPE_VCAP_STRING);
632#ifdef WURFL_HEADER_WITH_DETAILS
633 // write WURFL property type and name before its value...
634 chunk_appendf(temp, "%s=%s=", HA_WURFL_DATA_TYPE_VCAP_STRING, wn->name);
635#endif
636 chunk_appendf(temp, "%s", wurfl_device_get_virtual_capability(dHandle, wn->name));
637 break;
638 case HA_WURFL_DATA_TYPE_PROPERTY :
639 ha_wurfl_log("WURFL: %s is a %s\n", wn->name, HA_WURFL_DATA_TYPE_PROPERTY_STRING);
640#ifdef WURFL_HEADER_WITH_DETAILS
641 // write WURFL property type and name before its value...
642 chunk_appendf(temp, "%s=%s=", HA_WURFL_DATA_TYPE_PROPERTY_STRING, wn->name);
643#endif
Willy Tarreau350c1c62016-12-21 14:57:34 +0100644 chunk_appendf(temp, "%s", wn->func_callback(global_wurfl.handle, dHandle));
scientiamobiled0027ed2016-11-04 10:55:08 +0100645 break;
646 }
647
648 } else {
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200649 ha_wurfl_log("WURFL: %s not in wurfl-information-list \n",
650 args[i].data.str.area);
scientiamobiled0027ed2016-11-04 10:55:08 +0100651 }
652
653 i++;
654 }
655
656 wurfl_device_destroy(dHandle);
Willy Tarreau843b7cb2018-07-13 10:54:26 +0200657 smp->data.u.str.area = temp->area;
658 smp->data.u.str.data = temp->data;
scientiamobiled0027ed2016-11-04 10:55:08 +0100659 return 1;
660}
661
662static struct cfg_kw_list wurflcfg_kws = {{ }, {
663 { CFG_GLOBAL, "wurfl-data-file", ha_wurfl_cfg_data_file },
664 { CFG_GLOBAL, "wurfl-information-list-separator", ha_wurfl_cfg_information_list_separator },
665 { CFG_GLOBAL, "wurfl-information-list", ha_wurfl_cfg_information_list },
666 { CFG_GLOBAL, "wurfl-patch-file", ha_wurfl_cfg_patch_file_list },
667 { CFG_GLOBAL, "wurfl-cache-size", ha_wurfl_cfg_cache },
668 { CFG_GLOBAL, "wurfl-engine-mode", ha_wurfl_cfg_engine_mode },
669 { CFG_GLOBAL, "wurfl-useragent-priority", ha_wurfl_cfg_useragent_priority },
670 { 0, NULL, NULL },
671 }
672};
673
Willy Tarreau0108d902018-11-25 19:14:37 +0100674INITCALL1(STG_REGISTER, cfg_register_keywords, &wurflcfg_kws);
675
scientiamobiled0027ed2016-11-04 10:55:08 +0100676/* Note: must not be declared <const> as its list will be overwritten */
677static struct sample_fetch_kw_list fetch_kws = {ILH, {
678 { "wurfl-get-all", ha_wurfl_get_all, 0, NULL, SMP_T_STR, SMP_USE_HRQHV },
679 { "wurfl-get", ha_wurfl_get, ARG12(1,STR,STR,STR,STR,STR,STR,STR,STR,STR,STR,STR,STR), NULL, SMP_T_STR, SMP_USE_HRQHV },
680 { NULL, NULL, 0, 0, 0 },
681 }
682};
683
Willy Tarreau0108d902018-11-25 19:14:37 +0100684INITCALL1(STG_REGISTER, sample_register_fetches, &fetch_kws);
685
scientiamobiled0027ed2016-11-04 10:55:08 +0100686/* Note: must not be declared <const> as its list will be overwritten */
687static struct sample_conv_kw_list conv_kws = {ILH, {
688 { NULL, NULL, 0, 0, 0 },
689 }
690};
691
Willy Tarreau0108d902018-11-25 19:14:37 +0100692INITCALL1(STG_REGISTER, sample_register_convs, &conv_kws);
693
scientiamobiled0027ed2016-11-04 10:55:08 +0100694// WURFL properties wrapper functions
695static const char *ha_wurfl_get_wurfl_root_id (wurfl_handle wHandle, wurfl_device_handle dHandle)
696{
697 return wurfl_device_get_root_id(dHandle);
698}
699
700static const char *ha_wurfl_get_wurfl_id (wurfl_handle wHandle, wurfl_device_handle dHandle)
701{
702 return wurfl_device_get_id(dHandle);
703}
704
705static const char *ha_wurfl_get_wurfl_isdevroot (wurfl_handle wHandle, wurfl_device_handle dHandle)
706{
707 if (wurfl_device_is_actual_device_root(dHandle))
708 return HA_WURFL_ISDEVROOT_TRUE;
709 else
710 return HA_WURFL_ISDEVROOT_FALSE;
711}
712
713static const char *ha_wurfl_get_wurfl_useragent (wurfl_handle wHandle, wurfl_device_handle dHandle)
714{
715 return wurfl_device_get_original_useragent(dHandle);
716}
717
718static const char *ha_wurfl_get_wurfl_api_version (wurfl_handle wHandle, wurfl_device_handle dHandle)
719{
720 return wurfl_get_api_version();
721}
722
723static const char *ha_wurfl_get_wurfl_engine_target (wurfl_handle wHandle, wurfl_device_handle dHandle)
724{
725 return wurfl_get_engine_target_as_string(wHandle);
726}
727
728static const char *ha_wurfl_get_wurfl_info (wurfl_handle wHandle, wurfl_device_handle dHandle)
729{
730 return wurfl_get_wurfl_info(wHandle);
731}
732
733static const char *ha_wurfl_get_wurfl_last_load_time (wurfl_handle wHandle, wurfl_device_handle dHandle)
734{
735 return wurfl_get_last_load_time_as_string(wHandle);
736}
737
738static const char *ha_wurfl_get_wurfl_normalized_useragent (wurfl_handle wHandle, wurfl_device_handle dHandle)
739{
740 return wurfl_device_get_normalized_useragent(dHandle);
741}
742
743static const char *ha_wurfl_get_wurfl_useragent_priority (wurfl_handle wHandle, wurfl_device_handle dHandle)
744{
745 return wurfl_get_useragent_priority_as_string(wHandle);
746}
747
748// call function for WURFL properties
749static const char *(*ha_wurfl_get_property_callback(char *name)) (wurfl_handle wHandle, wurfl_device_handle dHandle)
750{
751 int position;
752 int begin = 0;
753 int end = HA_WURFL_PROPERTIES_NBR - 1;
754 int cond = 0;
755
756 while(begin <= end) {
757 position = (begin + end) / 2;
758
759 if((cond = strcmp(wurfl_properties_function_map[position].name, name)) == 0) {
760 ha_wurfl_log("WURFL: ha_wurfl_get_property_callback match %s\n", wurfl_properties_function_map[position].name );
761 return wurfl_properties_function_map[position].func;
762 } else if(cond < 0)
763 begin = position + 1;
764 else
765 end = position - 1;
766
767 }
768
769 return NULL;
770}
771
772static const char *ha_wurfl_retrieve_header(const char *header_name, const void *wh)
773{
774 struct sample *smp;
775 struct hdr_idx *idx;
776 struct hdr_ctx ctx;
777 const struct http_msg *msg;
778 int header_len = HA_WURFL_MAX_HEADER_LENGTH;
779
780 ha_wurfl_log("WURFL: retrieve header request [%s]\n", header_name);
781 smp = ((ha_wurfl_header_t *)wh)->wsmp;
782 idx = &smp->strm->txn->hdr_idx;
783 msg = &smp->strm->txn->req;
784 ctx.idx = 0;
785
786 if (http_find_full_header2(header_name, strlen(header_name), msg->chn->buf->p, idx, &ctx) == 0)
787 return 0;
788
789 if (header_len > ctx.vlen)
790 header_len = ctx.vlen;
791
792 strncpy(((ha_wurfl_header_t *)wh)->header_value, ctx.line + ctx.val, header_len);
793 ((ha_wurfl_header_t *)wh)->header_value[header_len] = '\0';
794 ha_wurfl_log("WURFL: retrieve header request returns [%s]\n", ((ha_wurfl_header_t *)wh)->header_value);
795 return ((ha_wurfl_header_t *)wh)->header_value;
796}
Willy Tarreau80713382018-11-26 10:19:54 +0100797
Willy Tarreau172f5ce2018-11-26 11:21:50 +0100798REGISTER_POST_CHECK(ha_wurfl_init);
799REGISTER_POST_DEINIT(ha_wurfl_deinit);
Willy Tarreau80713382018-11-26 10:19:54 +0100800REGISTER_BUILD_OPTS("Built with WURFL support.");