Coverage Report

Created: 2026-01-03 06:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/haproxy/src/http_rules.c
Line
Count
Source
1
/*
2
 * HTTP rules parsing and registration
3
 *
4
 * Copyright 2000-2018 Willy Tarreau <w@1wt.eu>
5
 *
6
 * This program is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU General Public License
8
 * as published by the Free Software Foundation; either version
9
 * 2 of the License, or (at your option) any later version.
10
 *
11
 */
12
13
#include <sys/types.h>
14
15
#include <ctype.h>
16
#include <string.h>
17
#include <time.h>
18
19
#include <haproxy/acl.h>
20
#include <haproxy/action.h>
21
#include <haproxy/api.h>
22
#include <haproxy/arg.h>
23
#include <haproxy/capture-t.h>
24
#include <haproxy/cfgparse.h>
25
#include <haproxy/chunk.h>
26
#include <haproxy/global.h>
27
#include <haproxy/http.h>
28
#include <haproxy/http_ana-t.h>
29
#include <haproxy/http_rules.h>
30
#include <haproxy/log.h>
31
#include <haproxy/pool.h>
32
#include <haproxy/proxy.h>
33
#include <haproxy/sample.h>
34
#include <haproxy/tools.h>
35
#include <haproxy/version.h>
36
37
38
/* List head of all known action keywords for "http-request" */
39
struct action_kw_list http_req_keywords = {
40
       .list = LIST_HEAD_INIT(http_req_keywords.list)
41
};
42
43
/* List head of all known action keywords for "http-response" */
44
struct action_kw_list http_res_keywords = {
45
       .list = LIST_HEAD_INIT(http_res_keywords.list)
46
};
47
48
/* List head of all known action keywords for "http-after-response" */
49
struct action_kw_list http_after_res_keywords = {
50
       .list = LIST_HEAD_INIT(http_after_res_keywords.list)
51
};
52
53
void http_req_keywords_register(struct action_kw_list *kw_list)
54
0
{
55
0
  LIST_APPEND(&http_req_keywords.list, &kw_list->list);
56
0
}
57
58
void http_res_keywords_register(struct action_kw_list *kw_list)
59
0
{
60
0
  LIST_APPEND(&http_res_keywords.list, &kw_list->list);
61
0
}
62
63
void http_after_res_keywords_register(struct action_kw_list *kw_list)
64
0
{
65
0
  LIST_APPEND(&http_after_res_keywords.list, &kw_list->list);
66
0
}
67
68
/*
69
 * Return the struct http_req_action_kw associated to a keyword.
70
 */
71
struct action_kw *action_http_req_custom(const char *kw)
72
0
{
73
0
  return action_lookup(&http_req_keywords.list, kw);
74
0
}
75
76
/*
77
 * Return the struct http_res_action_kw associated to a keyword.
78
 */
79
struct action_kw *action_http_res_custom(const char *kw)
80
0
{
81
0
  return action_lookup(&http_res_keywords.list, kw);
82
0
}
83
84
/*
85
 * Return the struct http_after_res_action_kw associated to a keyword.
86
 */
87
struct action_kw *action_http_after_res_custom(const char *kw)
88
0
{
89
0
  return action_lookup(&http_after_res_keywords.list, kw);
90
0
}
91
92
/* parse an "http-request" rule */
93
struct act_rule *parse_http_req_cond(const char **args, const char *file, int linenum, struct proxy *proxy)
94
0
{
95
0
  struct act_rule *rule;
96
0
  const struct action_kw *custom = NULL;
97
0
  int cur_arg;
98
99
0
  rule = new_act_rule(ACT_F_HTTP_REQ, file, linenum);
100
0
  if (!rule) {
101
0
    ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
102
0
    goto out;
103
0
  }
104
105
0
  if (((custom = action_http_req_custom(args[0])) != NULL)) {
106
0
    char *errmsg = NULL;
107
108
0
    cur_arg = 1;
109
    /* try in the module list */
110
0
    rule->kw = custom;
111
112
0
    if (custom->flags & KWF_EXPERIMENTAL) {
113
0
      if (!experimental_directives_allowed) {
114
0
        ha_alert("parsing [%s:%d] : '%s' action is experimental, must be allowed via a global 'expose-experimental-directives'\n",
115
0
                 file, linenum, custom->kw);
116
0
        goto out_err;
117
0
      }
118
0
      mark_tainted(TAINTED_CONFIG_EXP_KW_DECLARED);
119
0
    }
120
121
0
    if (custom->parse(args, &cur_arg, proxy, rule, &errmsg) == ACT_RET_PRS_ERR) {
122
0
      ha_alert("parsing [%s:%d] : error detected in %s '%s' while parsing 'http-request %s' rule : %s.\n",
123
0
         file, linenum, proxy_type_str(proxy), proxy->id, args[0], errmsg);
124
0
      free(errmsg);
125
0
      goto out_err;
126
0
    }
127
0
    else if (errmsg) {
128
0
      ha_warning("parsing [%s:%d] : %s.\n", file, linenum, errmsg);
129
0
      free(errmsg);
130
0
    }
131
0
  }
132
0
  else {
133
0
    const char *best = action_suggest(args[0], &http_req_keywords.list, NULL);
134
135
0
    action_build_list(&http_req_keywords.list, &trash);
136
0
    ha_alert("parsing [%s:%d]: 'http-request' expects %s, but got '%s'%s.%s%s%s\n",
137
0
             file, linenum, trash.area,
138
0
             args[0], *args[0] ? "" : " (missing argument)",
139
0
             best ? " Did you mean '" : "",
140
0
             best ? best : "",
141
0
             best ? "' maybe ?" : "");
142
0
    goto out_err;
143
0
  }
144
145
0
  if (strcmp(args[cur_arg], "if") == 0 || strcmp(args[cur_arg], "unless") == 0) {
146
0
    struct acl_cond *cond;
147
0
    char *errmsg = NULL;
148
149
0
    if ((cond = build_acl_cond(file, linenum, &proxy->acl, proxy, args+cur_arg, &errmsg)) == NULL) {
150
0
      ha_alert("parsing [%s:%d] : error detected while parsing an 'http-request %s' condition : %s.\n",
151
0
         file, linenum, args[0], errmsg);
152
0
      free(errmsg);
153
0
      goto out_err;
154
0
    }
155
0
    rule->cond = cond;
156
0
  }
157
0
  else if (*args[cur_arg]) {
158
0
    ha_alert("parsing [%s:%d]: 'http-request %s' expects"
159
0
       " either 'if' or 'unless' followed by a condition but found '%s'.\n",
160
0
       file, linenum, args[0], args[cur_arg]);
161
0
    goto out_err;
162
0
  }
163
164
0
  return rule;
165
0
 out_err:
166
0
  free_act_rule(rule);
167
0
 out:
168
0
  return NULL;
169
0
}
170
171
/* parse an "http-respose" rule */
172
struct act_rule *parse_http_res_cond(const char **args, const char *file, int linenum, struct proxy *proxy)
173
0
{
174
0
  struct act_rule *rule;
175
0
  const struct action_kw *custom = NULL;
176
0
  int cur_arg;
177
178
0
  rule = new_act_rule(ACT_F_HTTP_RES, file, linenum);
179
0
  if (!rule) {
180
0
    ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
181
0
    goto out;
182
0
  }
183
184
0
  if (((custom = action_http_res_custom(args[0])) != NULL)) {
185
0
    char *errmsg = NULL;
186
187
0
    cur_arg = 1;
188
    /* try in the module list */
189
0
    rule->kw = custom;
190
191
0
    if (custom->flags & KWF_EXPERIMENTAL) {
192
0
      if (!experimental_directives_allowed) {
193
0
        ha_alert("parsing [%s:%d] : '%s' action is experimental, must be allowed via a global 'expose-experimental-directives'\n",
194
0
                 file, linenum, custom->kw);
195
0
        goto out_err;
196
0
      }
197
0
      mark_tainted(TAINTED_CONFIG_EXP_KW_DECLARED);
198
0
    }
199
200
0
    if (custom->parse(args, &cur_arg, proxy, rule, &errmsg) == ACT_RET_PRS_ERR) {
201
0
      ha_alert("parsing [%s:%d] : error detected in %s '%s' while parsing 'http-response %s' rule : %s.\n",
202
0
         file, linenum, proxy_type_str(proxy), proxy->id, args[0], errmsg);
203
0
      free(errmsg);
204
0
      goto out_err;
205
0
    }
206
0
    else if (errmsg) {
207
0
      ha_warning("parsing [%s:%d] : %s.\n", file, linenum, errmsg);
208
0
      free(errmsg);
209
0
    }
210
0
  }
211
0
  else {
212
0
    const char *best = action_suggest(args[0], &http_res_keywords.list, NULL);
213
214
0
    action_build_list(&http_res_keywords.list, &trash);
215
0
    ha_alert("parsing [%s:%d]: 'http-response' expects %s, but got '%s'%s.%s%s%s\n",
216
0
             file, linenum, trash.area,
217
0
             args[0], *args[0] ? "" : " (missing argument)",
218
0
             best ? " Did you mean '" : "",
219
0
             best ? best : "",
220
0
             best ? "' maybe ?" : "");
221
0
    goto out_err;
222
0
  }
223
224
0
  if (strcmp(args[cur_arg], "if") == 0 || strcmp(args[cur_arg], "unless") == 0) {
225
0
    struct acl_cond *cond;
226
0
    char *errmsg = NULL;
227
228
0
    if ((cond = build_acl_cond(file, linenum, &proxy->acl, proxy, args+cur_arg, &errmsg)) == NULL) {
229
0
      ha_alert("parsing [%s:%d] : error detected while parsing an 'http-response %s' condition : %s.\n",
230
0
         file, linenum, args[0], errmsg);
231
0
      free(errmsg);
232
0
      goto out_err;
233
0
    }
234
0
    rule->cond = cond;
235
0
  }
236
0
  else if (*args[cur_arg]) {
237
0
    ha_alert("parsing [%s:%d]: 'http-response %s' expects"
238
0
       " either 'if' or 'unless' followed by a condition but found '%s'.\n",
239
0
       file, linenum, args[0], args[cur_arg]);
240
0
    goto out_err;
241
0
  }
242
243
0
  return rule;
244
0
 out_err:
245
0
  free_act_rule(rule);
246
0
 out:
247
0
  return NULL;
248
0
}
249
250
251
/* parse an "http-after-response" rule */
252
struct act_rule *parse_http_after_res_cond(const char **args, const char *file, int linenum, struct proxy *proxy)
253
0
{
254
0
  struct act_rule *rule;
255
0
  const struct action_kw *custom = NULL;
256
0
  int cur_arg;
257
258
0
  rule = new_act_rule(ACT_F_HTTP_RES, file, linenum);
259
0
  if (!rule) {
260
0
    ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum);
261
0
    goto out;
262
0
  }
263
264
0
  if (((custom = action_http_after_res_custom(args[0])) != NULL)) {
265
0
    char *errmsg = NULL;
266
267
0
    cur_arg = 1;
268
    /* try in the module list */
269
0
    rule->kw = custom;
270
0
    if (custom->parse(args, &cur_arg, proxy, rule, &errmsg) == ACT_RET_PRS_ERR) {
271
0
      ha_alert("parsing [%s:%d] : error detected in %s '%s' while parsing 'http-after-response %s' rule : %s.\n",
272
0
         file, linenum, proxy_type_str(proxy), proxy->id, args[0], errmsg);
273
0
      free(errmsg);
274
0
      goto out_err;
275
0
    }
276
0
    else if (errmsg) {
277
0
      ha_warning("parsing [%s:%d] : %s.\n", file, linenum, errmsg);
278
0
      free(errmsg);
279
0
    }
280
0
  }
281
0
  else {
282
0
    const char *best = action_suggest(args[0], &http_after_res_keywords.list, NULL);
283
284
0
    action_build_list(&http_after_res_keywords.list, &trash);
285
0
    ha_alert("parsing [%s:%d]: 'http-after-response' expects %s, but got '%s'%s.%s%s%s\n",
286
0
             file, linenum, trash.area,
287
0
             args[0], *args[0] ? "" : " (missing argument)",
288
0
             best ? " Did you mean '" : "",
289
0
             best ? best : "",
290
0
             best ? "' maybe ?" : "");
291
0
    goto out_err;
292
0
  }
293
294
0
  if (strcmp(args[cur_arg], "if") == 0 || strcmp(args[cur_arg], "unless") == 0) {
295
0
    struct acl_cond *cond;
296
0
    char *errmsg = NULL;
297
298
0
    if ((cond = build_acl_cond(file, linenum, &proxy->acl, proxy, args+cur_arg, &errmsg)) == NULL) {
299
0
      ha_alert("parsing [%s:%d] : error detected while parsing an 'http-after-response %s' condition : %s.\n",
300
0
         file, linenum, args[0], errmsg);
301
0
      free(errmsg);
302
0
      goto out_err;
303
0
    }
304
0
    rule->cond = cond;
305
0
  }
306
0
  else if (*args[cur_arg]) {
307
0
    ha_alert("parsing [%s:%d]: 'http-after-response %s' expects"
308
0
       " either 'if' or 'unless' followed by a condition but found '%s'.\n",
309
0
       file, linenum, args[0], args[cur_arg]);
310
0
    goto out_err;
311
0
  }
312
313
0
  return rule;
314
0
 out_err:
315
0
  free_act_rule(rule);
316
0
 out:
317
0
  return NULL;
318
0
}
319
320
/* completely free redirect rule */
321
void http_free_redirect_rule(struct redirect_rule *rdr)
322
0
{
323
0
  free_acl_cond(rdr->cond);
324
0
  free(rdr->rdr_str);
325
0
  if ((rdr->flags & REDIRECT_FLAG_COOKIE_FMT))
326
0
    lf_expr_deinit(&rdr->cookie.fmt);
327
0
  else
328
0
    istfree(&rdr->cookie.str);
329
0
  lf_expr_deinit(&rdr->rdr_fmt);
330
0
  free(rdr);
331
0
}
332
333
/* Parses a redirect rule. Returns the redirect rule on success or NULL on error,
334
 * with <err> filled with the error message. If <use_fmt> is not null, builds a
335
 * dynamic log-format rule instead of a static string. Parameter <dir> indicates
336
 * the direction of the rule, and equals 0 for request, non-zero for responses.
337
 */
338
struct redirect_rule *http_parse_redirect_rule(const char *file, int linenum, struct proxy *curproxy,
339
                                               const char **args, char **errmsg, int use_fmt, int dir)
340
0
{
341
0
  struct redirect_rule *rule = NULL;
342
0
  int cur_arg;
343
0
  int type = REDIRECT_TYPE_NONE;
344
0
  int code = 302;
345
0
  const char *destination = NULL;
346
0
  const char *cookie = NULL;
347
0
  int cookie_set = 0;
348
0
  size_t cookie_len = 0;
349
0
  unsigned int flags = (!dir ? REDIRECT_FLAG_FROM_REQ : REDIRECT_FLAG_NONE);
350
0
  struct acl_cond *cond = NULL;
351
352
0
  cur_arg = 0;
353
0
  while (*(args[cur_arg])) {
354
0
    if (strcmp(args[cur_arg], "location") == 0) {
355
0
      if (!*args[cur_arg + 1])
356
0
        goto missing_arg;
357
358
0
      type = REDIRECT_TYPE_LOCATION;
359
0
      cur_arg++;
360
0
      destination = args[cur_arg];
361
0
    }
362
0
    else if (strcmp(args[cur_arg], "prefix") == 0) {
363
0
      if (!*args[cur_arg + 1])
364
0
        goto missing_arg;
365
0
      type = REDIRECT_TYPE_PREFIX;
366
0
      cur_arg++;
367
0
      destination = args[cur_arg];
368
0
    }
369
0
    else if (strcmp(args[cur_arg], "scheme") == 0) {
370
0
      if (!*args[cur_arg + 1])
371
0
        goto missing_arg;
372
373
0
      type = REDIRECT_TYPE_SCHEME;
374
0
      cur_arg++;
375
0
      destination = args[cur_arg];
376
0
    }
377
0
    else if (strcmp(args[cur_arg], "set-cookie") == 0) {
378
0
      if (!*args[cur_arg + 1])
379
0
        goto missing_arg;
380
381
0
      cur_arg++;
382
0
      cookie = args[cur_arg];
383
0
      cookie_set = 1;
384
0
    }
385
0
    else if (strcmp(args[cur_arg], "set-cookie-fmt") == 0) {
386
0
      if (!*args[cur_arg + 1])
387
0
        goto missing_arg;
388
389
0
      cur_arg++;
390
0
      cookie = args[cur_arg];
391
0
      cookie_set = 2;
392
0
    }
393
0
    else if (strcmp(args[cur_arg], "clear-cookie") == 0) {
394
0
      if (!*args[cur_arg + 1])
395
0
        goto missing_arg;
396
397
0
      cur_arg++;
398
0
      cookie = args[cur_arg];
399
0
      cookie_set = 0;
400
0
    }
401
0
    else if (strcmp(args[cur_arg], "code") == 0) {
402
0
      if (!*args[cur_arg + 1])
403
0
        goto missing_arg;
404
405
0
      cur_arg++;
406
0
      code = atol(args[cur_arg]);
407
0
      if (code < 301 || code > 308 || (code > 303 && code < 307)) {
408
0
        memprintf(errmsg,
409
0
                  "'%s': unsupported HTTP code '%s' (must be one of 301, 302, 303, 307 or 308)",
410
0
                  args[cur_arg - 1], args[cur_arg]);
411
0
        goto err;
412
0
      }
413
0
    }
414
0
    else if (strcmp(args[cur_arg], "drop-query") == 0) {
415
0
      flags |= REDIRECT_FLAG_DROP_QS;
416
0
    }
417
0
    else if (strcmp(args[cur_arg], "keep-query") == 0) {
418
0
      flags |= REDIRECT_FLAG_KEEP_QS;
419
0
    }
420
0
    else if (strcmp(args[cur_arg], "append-slash") == 0) {
421
0
      flags |= REDIRECT_FLAG_APPEND_SLASH;
422
0
    }
423
0
    else if (strcmp(args[cur_arg], "ignore-empty") == 0) {
424
0
      flags |= REDIRECT_FLAG_IGNORE_EMPTY;
425
0
    }
426
0
    else if (strcmp(args[cur_arg], "if") == 0 ||
427
0
       strcmp(args[cur_arg], "unless") == 0) {
428
0
      cond = build_acl_cond(file, linenum, &curproxy->acl, curproxy, (const char **)args + cur_arg, errmsg);
429
0
      if (!cond) {
430
0
        memprintf(errmsg, "error in condition: %s", *errmsg);
431
0
        goto err;
432
0
      }
433
0
      break;
434
0
    }
435
0
    else {
436
0
      memprintf(errmsg,
437
0
                "expects 'code', 'prefix', 'location', 'scheme', 'set-cookie', 'set-cookie-fmt',"
438
0
          " 'clear-cookie', 'drop-query', 'keep-query', 'ignore-empty' or 'append-slash' (was '%s')",
439
0
                args[cur_arg]);
440
0
      goto err;
441
0
    }
442
0
    cur_arg++;
443
0
  }
444
445
0
  if (type == REDIRECT_TYPE_NONE) {
446
0
    memprintf(errmsg, "redirection type expected ('prefix', 'location', or 'scheme')");
447
0
    goto err;
448
0
  }
449
450
0
  if (dir && type != REDIRECT_TYPE_LOCATION) {
451
0
    memprintf(errmsg, "response only supports redirect type 'location'");
452
0
    goto err;
453
0
  }
454
455
0
  rule = calloc(1, sizeof(*rule));
456
0
  if (!rule)
457
0
    goto out_of_memory;
458
0
  rule->cond = cond;
459
0
  lf_expr_init(&rule->rdr_fmt);
460
461
0
  if (!use_fmt) {
462
    /* old-style static redirect rule */
463
0
    rule->rdr_str = strdup(destination);
464
0
    if (!rule->rdr_str)
465
0
      goto out_of_memory;
466
0
    rule->rdr_len = strlen(destination);
467
0
  }
468
0
  else {
469
    /* log-format based redirect rule */
470
0
    int cap = 0;
471
472
    /* Parse destination. Note that in the REDIRECT_TYPE_PREFIX case,
473
     * if prefix == "/", we don't want to add anything, otherwise it
474
     * makes it hard for the user to configure a self-redirection.
475
     */
476
0
    curproxy->conf.args.ctx = ARGC_RDR;
477
0
    if (curproxy->cap & PR_CAP_FE)
478
0
      cap |= (dir ? SMP_VAL_FE_HRS_HDR : SMP_VAL_FE_HRQ_HDR);
479
0
    if (curproxy->cap & PR_CAP_BE)
480
0
      cap |= (dir ? SMP_VAL_BE_HRS_HDR : SMP_VAL_BE_HRQ_HDR);
481
0
    if (!(type == REDIRECT_TYPE_PREFIX && destination[0] == '/' && destination[1] == '\0')) {
482
0
      if (!parse_logformat_string(destination, curproxy, &rule->rdr_fmt, LOG_OPT_HTTP, cap, errmsg)) {
483
0
        goto err;
484
0
      }
485
0
    }
486
0
  }
487
488
0
  if (cookie) {
489
    /* depending on cookie_set, either we want to set the cookie, or to clear it.
490
     * a clear consists in appending "; path=/; Max-Age=0;" at the end.
491
     */
492
0
    cookie_len = strlen(cookie);
493
0
    if (cookie_set == 1) { // set-cookie
494
0
      rule->cookie.str = istalloc(cookie_len+9);
495
0
      if (!isttest(rule->cookie.str))
496
0
        goto out_of_memory;
497
0
      istcpy(&rule->cookie.str, ist2(cookie, cookie_len), cookie_len);
498
0
      istcat(&rule->cookie.str, ist2("; path=/;", 9), cookie_len+10);
499
0
    }
500
0
    else if (cookie_set == 2) { // set-cookie-fmt
501
0
      int cap = 0;
502
503
0
      lf_expr_init(&rule->cookie.fmt);
504
0
      curproxy->conf.args.ctx = ARGC_RDR;
505
0
      if (curproxy->cap & PR_CAP_FE)
506
0
        cap |= (dir ? SMP_VAL_FE_HRS_HDR : SMP_VAL_FE_HRQ_HDR);
507
0
      if (curproxy->cap & PR_CAP_BE)
508
0
        cap |= (dir ? SMP_VAL_BE_HRS_HDR : SMP_VAL_BE_HRQ_HDR);
509
510
0
      chunk_memcpy(&trash, cookie, cookie_len);
511
0
      chunk_strcat(&trash, "; path=/;");
512
0
      if (!parse_logformat_string(trash.area, curproxy, &rule->cookie.fmt, LOG_OPT_HTTP, cap, errmsg)) {
513
0
        goto err;
514
0
      }
515
516
0
      flags |= REDIRECT_FLAG_COOKIE_FMT;
517
0
    }
518
0
    else { // clear-cookie
519
0
      rule->cookie.str = istalloc(cookie_len+20);
520
0
      if (!isttest(rule->cookie.str))
521
0
        goto out_of_memory;
522
0
      istcpy(&rule->cookie.str, ist2(cookie, cookie_len), cookie_len);
523
0
      istcat(&rule->cookie.str, ist2("; path=/; Max-Age=0;", 20), cookie_len+21);
524
0
    }
525
0
  }
526
0
  rule->type = type;
527
0
  rule->code = code;
528
0
  rule->flags = flags;
529
0
  LIST_INIT(&rule->list);
530
0
  return rule;
531
532
0
 missing_arg:
533
0
  memprintf(errmsg, "missing argument for '%s'", args[cur_arg]);
534
0
  goto err;
535
0
 out_of_memory:
536
0
  memprintf(errmsg, "parsing [%s:%d]: out of memory.", file, linenum);
537
0
 err:
538
0
  if (rule)
539
0
    http_free_redirect_rule(rule);
540
0
  else if (cond) {
541
    /* rule not yet allocated, but cond already is */
542
0
    free_acl_cond(cond);
543
0
  }
544
545
  return NULL;
546
0
}
547
548
/*
549
 * Local variables:
550
 *  c-indent-level: 8
551
 *  c-basic-offset: 8
552
 * End:
553
 */