diff --git a/doc/configuration.txt b/doc/configuration.txt index e434a4750..cd75de566 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -4991,18 +4991,22 @@ http-request del-map() [ { if | unless } ] It takes one argument: "file name" It is the equivalent of the "del map" command from the stats socket, but can be triggered by an HTTP request. -http-request deny [deny_status ] [ { errorfile | errorfiles } ] - [ { if | unless } ] +http-request deny [deny_status ] [ { if | unless } ] +http-request deny [ { status | deny_status } ] [content-type ] + [ { default-errorfiles | errorfile | errorfiles | + file | lf-file | string | lf-string } ] + [ hdr ]* + [ { if | unless } ] - This stops the evaluation of the rules and immediately rejects the request - and emits an HTTP 403 error, or optionally the status code specified as an - argument to "deny_status". The list of permitted status codes is limited to - those that can be overridden by the "errorfile" directive. A specific error - message may be specified. It may be an error file, using the "errorfile" - keyword followed by the file containing the full HTTP response. It may also - be an error from an http-errors section, using the "errorfiles" keyword - followed by the section name. + This stops the evaluation of the rules and immediately rejects the request. + By default an HTTP 403 error is returned. But the response may be customized + using same syntax than "http-request return" rules. Thus, see "http-request + return" for details. For compatiblity purpose, when no argument is defined, + or only "deny_status", the argument "default-errorfiles" is implied. It means + "http-request deny [deny_status ]" is an alias of + "http-request deny [status ] default-errorfiles". No further "http-request" rules are evaluated. + See also "http-request return". http-request disable-l7-retry [ { if | unless } ] This disables any attempt to retry the request if it fails for any other @@ -5576,26 +5580,32 @@ http-request strict-mode { on | off } the frontend, the default mode is restored when HAProxy starts the backend rules evaluation. -http-request tarpit [deny_status ] [ { errorfile | errorfiles } ] - [ { if | unless } ] +http-request tarpit [deny_status ] [ { if | unless } ] +http-request tarpit [ { status | deny_status } ] [content-type ] + [ { default-errorfiles | errorfile | errorfiles | + file | lf-file | string | lf-string } ] + [ hdr ]* + [ { if | unless } ] This stops the evaluation of the rules and immediately blocks the request without responding for a delay specified by "timeout tarpit" or "timeout connect" if the former is not set. After that delay, if the client - is still connected, an HTTP error 500 (or optionally the status code - specified as an argument to "deny_status") is returned so that the client - does not suspect it has been tarpitted. Logs will report the flags "PT". - The goal of the tarpit rule is to slow down robots during an attack when - they're limited on the number of concurrent requests. It can be very - efficient against very dumb robots, and will significantly reduce the load - on firewalls compared to a "deny" rule. But when facing "correctly" - developed robots, it can make things worse by forcing haproxy and the front - firewall to support insane number of concurrent connections. A specific error - message may be specified. It may be an error file, using the "errorfile" - keyword followed by the file containing the full HTTP response. It may also - be an error from an http-errors section, using the "errorfiles" keyword - followed by the section name. - See also the "silent-drop" action. + is still connected, a response is returned so that the client does not + suspect it has been tarpitted. Logs will report the flags "PT". The goal of + the tarpit rule is to slow down robots during an attack when they're limited + on the number of concurrent requests. It can be very efficient against very + dumb robots, and will significantly reduce the load on firewalls compared to + a "deny" rule. But when facing "correctly" developed robots, it can make + things worse by forcing haproxy and the front firewall to support insane + number of concurrent connections. By default an HTTP error 500 is returned. + But the response may be customized using same syntax than + "http-request return" rules. Thus, see "http-request return" for details. + For compatiblity purpose, when no argument is defined, or only "deny_status", + the argument "default-errorfiles" is implied. It means + "http-request tarpit [deny_status ]" is an alias of + "http-request tarpit [status ] default-errorfiles". + No further "http-request" rules are evaluated. + See also "http-request return" and "http-request silent-drop". http-request track-sc0 [table ] [ { if | unless } ] http-request track-sc1 [table
] [ { if | unless } ] @@ -5779,18 +5789,22 @@ http-response del-map() [ { if | unless } ] It takes one argument: "file name" It is the equivalent of the "del map" command from the stats socket, but can be triggered by an HTTP response. -http-response deny [deny_status ] [ { errorfile | errorfiles } ] - [ { if | unless } ] +http-response deny [deny_status ] [ { if | unless } ] +http-response deny [ { status | deny_status } ] [content-type ] + [ { default-errorfiles | errorfile | errorfiles | + file | lf-file | string | lf-string } ] + [ hdr ]* + [ { if | unless } ] - This stops the evaluation of the rules and immediately rejects the response - and emits an HTTP 502 error, or optionally the status code specified as an - argument to "deny_status". The list of permitted status codes is limited to - those that can be overridden by the "errorfile" directive. A specific error - message may be specified. It may be an error file, using the "errorfile" - keyword followed by the file containing the full HTTP response. It may also - be an error from an http-errors section, using the "errorfiles" keyword - followed by the section name. + This stops the evaluation of the rules and immediately rejects the response. + By default an HTTP 502 error is returned. But the response may be customized + using same syntax than "http-response return" rules. Thus, see + "http-response return" for details. For compatiblity purpose, when no + argument is defined, or only "deny_status", the argument "default-errorfiles" + is implied. It means "http-response deny [deny_status ]" is an alias + of "http-response deny [status ] default-errorfiles". No further "http-response" rules are evaluated. + See also "http-response return". http-response redirect [ { if | unless } ] diff --git a/include/types/action.h b/include/types/action.h index 8d68f6d5a..b01380c3a 100644 --- a/include/types/action.h +++ b/include/types/action.h @@ -125,11 +125,7 @@ struct act_rule { struct list fmt; /* log-format compatible expression */ struct my_regex *re; /* used by replace-header/value/uri/path */ } http; /* args used by some HTTP rules */ - struct { - int status; /* status code */ - struct buffer *errmsg; /* HTTP error message, may be NULL */ - } http_deny; /* args used by HTTP deny rules */ - struct http_reply *http_reply; /* HTTP response to be used by return rules */ + struct http_reply *http_reply; /* HTTP response to be used by return/deny/tarpit rules */ struct redirect_rule *redir; /* redirect rule or "http-request redirect" */ struct { char *ref; /* MAP or ACL file name to update */ diff --git a/include/types/http_ana.h b/include/types/http_ana.h index 764ebc25b..316a2716d 100644 --- a/include/types/http_ana.h +++ b/include/types/http_ana.h @@ -26,6 +26,7 @@ #include #include +#include /* These are the flags that are found in txn->flags */ @@ -174,6 +175,7 @@ struct http_txn { /* 1 unused byte here */ short status; /* HTTP status from the server, negative if from proxy */ struct buffer *errmsg; /* custom HTTP error message to use as reply */ + struct http_reply *http_reply; /* The HTTP reply to use as reply */ char cache_hash[20]; /* Store the cache hash */ char *uri; /* first line if log needed, NULL otherwise */ diff --git a/reg-tests/http-errorfiles/errors/lf-403.txt b/reg-tests/http-errorfiles/errors/lf-403.txt new file mode 100644 index 000000000..3a3c3aa7e --- /dev/null +++ b/reg-tests/http-errorfiles/errors/lf-403.txt @@ -0,0 +1 @@ +The path "%[path]" is forbidden diff --git a/reg-tests/http-errorfiles/http_deny_errors.vtc b/reg-tests/http-errorfiles/http_deny_errors.vtc index de456d7b2..3a02af066 100644 --- a/reg-tests/http-errorfiles/http_deny_errors.vtc +++ b/reg-tests/http-errorfiles/http_deny_errors.vtc @@ -27,6 +27,9 @@ haproxy h1 -conf { http-request deny deny_status 500 errorfile /dev/null if { path /500-1 } http-request deny deny_status 500 errorfiles errors-1 if { path /500-2 } + http-request deny status 500 hdr x-err-info "path=%[path]" content-type "text/plain" string "Internal Error" if { path /int-err } + http-request deny status 403 hdr x-err-info "path=%[path]" content-type "text/plain" lf-file ${testdir}/errors/lf-403.txt if { path /forbidden } + } -start client c1r1 -connect ${h1_fe1_sock} { @@ -55,3 +58,20 @@ client c1r5 -connect ${h1_fe1_sock} { txreq -req GET -url /500-2 expect_close } -run +client c1r6 -connect ${h1_fe1_sock} { + txreq -req GET -url /int-err + rxresp + expect resp.status == 500 + expect resp.http.x-err-info == "path=/int-err" + expect resp.http.content-type == "text/plain" + expect resp.http.content-length == 14 + expect resp.body == "Internal Error" +} -run +client c1r7 -connect ${h1_fe1_sock} { + txreq -req GET -url /forbidden + rxresp + expect resp.status == 403 + expect resp.http.x-err-info == "path=/forbidden" + expect resp.http.content-type == "text/plain" + expect resp.body == "The path \"/forbidden\" is forbidden\n" +} -run diff --git a/src/http_act.c b/src/http_act.c index 61ffd5261..ab4a8bb94 100644 --- a/src/http_act.c +++ b/src/http_act.c @@ -58,6 +58,30 @@ static void release_http_action(struct act_rule *rule) } } +/* Release memory allocated by HTTP actions relying on an http reply. Concretly, + * it releases <.arg.http_reply> + */ +static void release_act_http_reply(struct act_rule *rule) +{ + release_http_reply(rule->arg.http_reply); + rule->arg.http_reply = NULL; +} + + +/* Check function for HTTP actions relying on an http reply. The function + * returns 1 in success case, otherwise, it returns 0 and err is filled. + */ +static int check_act_http_reply(struct act_rule *rule, struct proxy *px, char **err) +{ + struct http_reply *reply = rule->arg.http_reply; + + if (!http_check_http_reply(reply, px, err)) { + release_act_http_reply(rule); + return 0; + } + return 1; +} + /* This function executes one of the set-{method,path,query,uri} actions. It * builds a string in the trash from the specified format string. It finds @@ -797,119 +821,66 @@ static enum act_parse_ret parse_http_allow(const char **args, int *orig_arg, str return ACT_RET_PRS_OK; } -/* Check an "http-request deny" action when an http-errors section is referenced. - * - * The function returns 1 in success case, otherwise, it returns 0 and err is - * filled. - */ -static int check_http_deny_action(struct act_rule *rule, struct proxy *px, char **err) -{ - struct http_errors *http_errs; - int status = (intptr_t)(rule->arg.act.p[0]); - int ret = 1; - - list_for_each_entry(http_errs, &http_errors_list, list) { - if (strcmp(http_errs->id, (char *)rule->arg.act.p[1]) == 0) { - free(rule->arg.act.p[1]); - rule->arg.http_deny.status = status; - rule->arg.http_deny.errmsg = http_errs->errmsg[http_get_status_idx(status)]; - if (!rule->arg.http_deny.errmsg) - ha_warning("Proxy '%s': status '%d' referenced by http deny rule " - "not declared in http-errors section '%s'.\n", - px->id, status, http_errs->id); - break; - } - } - - if (&http_errs->list == &http_errors_list) { - memprintf(err, "unknown http-errors section '%s' referenced by http deny rule", - (char *)rule->arg.act.p[1]); - free(rule->arg.act.p[1]); - ret = 0; - } - - return ret; -} - /* Parse "deny" or "tarpit" actions for a request rule or "deny" action for a - * response rule. It may take optional arguments to define the status code, the - * error file or the http-errors section to use. It returns ACT_RET_PRS_OK on - * success, ACT_RET_PRS_ERR on error. + * response rule. It returns ACT_RET_PRS_OK on success, ACT_RET_PRS_ERR on + * error. It relies on http_parse_http_reply() to set + * <.arg.http_reply>. */ static enum act_parse_ret parse_http_deny(const char **args, int *orig_arg, struct proxy *px, struct act_rule *rule, char **err) { - int default_status, status, hc, cur_arg; - + int default_status; + int cur_arg, arg = 0; cur_arg = *orig_arg; if (rule->from == ACT_F_HTTP_REQ) { if (!strcmp(args[cur_arg-1], "tarpit")) { rule->action = ACT_HTTP_REQ_TARPIT; - default_status = status = 500; + default_status = 500; } else { rule->action = ACT_ACTION_DENY; - default_status = status = 403; + default_status = 403; } } else { rule->action = ACT_ACTION_DENY; - default_status = status = 502; + default_status = 502; + } + + /* If no args or only a deny_status specified, fallback on the legacy + * mode and use default error files despite the fact that + * default-errorfiles is not used. Otherwise, parse an http reply. + */ + + /* Prepare parsing of log-format strings */ + px->conf.args.ctx = ((rule->from == ACT_F_HTTP_REQ) ? ARGC_HRQ : ARGC_HRS); + + if (!*(args[cur_arg])) { + rule->arg.http_reply = http_parse_http_reply((const char *[]){"default-errorfiles", ""}, &arg, px, default_status, err); + goto end; } - rule->flags |= ACT_FLAG_FINAL; if (strcmp(args[cur_arg], "deny_status") == 0) { - cur_arg++; - if (!*args[cur_arg]) { - memprintf(err, "'%s' expects as argument", args[cur_arg-1]); - return ACT_RET_PRS_ERR; - } - - status = atol(args[cur_arg]); - cur_arg++; - for (hc = 0; hc < HTTP_ERR_SIZE; hc++) { - if (http_err_codes[hc] == status) - break; - } - if (hc >= HTTP_ERR_SIZE) { - memprintf(err, "status code '%d' not handled, using default code '%d'", - status, default_status); - status = default_status; - hc = http_get_status_idx(status); + if (!*(args[cur_arg+2]) || + (strcmp(args[cur_arg+2], "errorfile") != 0 && strcmp(args[cur_arg+2], "errorfiles") != 0)) { + rule->arg.http_reply = http_parse_http_reply((const char *[]){"status", args[cur_arg+1], "default-errorfiles", ""}, + &arg, px, default_status, err); + *orig_arg += 2; + goto end; } + args[cur_arg] += 5; /* skip "deny_" for the parsing */ } - if (strcmp(args[cur_arg], "errorfile") == 0) { - cur_arg++; - if (!*args[cur_arg]) { - memprintf(err, "'%s' expects as argument", args[cur_arg-1]); - return ACT_RET_PRS_ERR; - } + rule->arg.http_reply = http_parse_http_reply(args, orig_arg, px, default_status, err); - rule->arg.http_deny.errmsg = http_load_errorfile(args[cur_arg], err); - if (!rule->arg.http_deny.errmsg) - return ACT_RET_PRS_ERR; - cur_arg++; - } - else if (strcmp(args[cur_arg], "errorfiles") == 0) { - cur_arg++; - if (!*args[cur_arg]) { - memprintf(err, "'%s' expects as argument", args[cur_arg-1]); - return ACT_RET_PRS_ERR; - } - /* Must be resolved during the config validity check */ - rule->arg.act.p[0] = (void *)((intptr_t)status); - rule->arg.act.p[1] = strdup(args[cur_arg]); - rule->check_ptr = check_http_deny_action; - cur_arg++; - goto out; - } + end: + if (!rule->arg.http_reply) + return ACT_RET_PRS_ERR; - rule->arg.http_deny.status = status; - - out: - *orig_arg = cur_arg; + rule->flags |= ACT_FLAG_FINAL; + rule->check_ptr = check_act_http_reply; + rule->release_ptr = release_act_http_reply; return ACT_RET_PRS_OK; } @@ -1802,13 +1773,6 @@ static enum act_parse_ret parse_http_strict_mode(const char **args, int *orig_ar return ACT_RET_PRS_OK; } -/* Release <.arg.http_reply> */ -static void release_http_return(struct act_rule *rule) -{ - release_http_reply(rule->arg.http_reply); - rule->arg.http_reply = NULL; -} - /* This function executes a return action. It builds an HTX message from an * errorfile, an raw file or a log-format string, depending on <.action> * value. On success, it returns ACT_RET_ABRT. If an error occurs ACT_RET_ERR is @@ -1839,20 +1803,6 @@ static enum act_return http_action_return(struct act_rule *rule, struct proxy *p return ACT_RET_ABRT; } -/* Check an "http-request return" action. The function returns 1 in success - * case, otherwise, it returns 0 and err is filled. - */ -static int check_http_return_action(struct act_rule *rule, struct proxy *px, char **err) -{ - struct http_reply *reply = rule->arg.http_reply; - - if (!http_check_http_reply(reply, px, err)) { - release_http_return(rule); - return 0; - } - return 1; -} - /* Parse a "return" action. It returns ACT_RET_PRS_OK on success, * ACT_RET_PRS_ERR on error. It relies on http_parse_http_reply() to set * <.arg.http_reply>. @@ -1868,9 +1818,9 @@ static enum act_parse_ret parse_http_return(const char **args, int *orig_arg, st rule->flags |= ACT_FLAG_FINAL; rule->action = ACT_CUSTOM; - rule->check_ptr = check_http_return_action; + rule->check_ptr = check_act_http_reply; rule->action_ptr = http_action_return; - rule->release_ptr = release_http_return; + rule->release_ptr = release_act_http_reply; return ACT_RET_PRS_OK; } diff --git a/src/http_ana.c b/src/http_ana.c index 043937760..adadc186f 100644 --- a/src/http_ana.c +++ b/src/http_ana.c @@ -672,6 +672,12 @@ int http_process_req_common(struct stream *s, struct channel *req, int an_bit, s _HA_ATOMIC_ADD(&s->be->be_counters.denied_req, 1); if (sess->listener->counters) _HA_ATOMIC_ADD(&sess->listener->counters->denied_req, 1); + + if (txn->http_reply) { + if (http_reply_message(s, txn->http_reply) == -1) + goto return_int_err; + goto return_prx_cond; + } goto return_prx_err; return_int_err: @@ -1002,8 +1008,28 @@ int http_process_tarpit(struct stream *s, struct channel *req, int an_bit) */ s->logs.t_queue = tv_ms_elapsed(&s->logs.tv_accept, &now); - http_reply_and_close(s, txn->status, (!(req->flags & CF_READ_ERROR) ? http_error_message(s) : NULL)); + if (req->flags & CF_READ_ERROR) { + http_reply_and_close(s, txn->status, NULL); + goto end; + } + if (txn->http_reply) { + if (!http_reply_message(s, txn->http_reply)) + goto end; + + txn->status = 500; + if (!(s->flags & SF_ERR_MASK)) + s->flags |= SF_ERR_INTERNAL; + _HA_ATOMIC_ADD(&s->sess->fe->fe_counters.internal_errors, 1); + if (s->flags & SF_BE_ASSIGNED) + _HA_ATOMIC_ADD(&s->be->be_counters.internal_errors, 1); + if (s->sess->listener->counters) + _HA_ATOMIC_ADD(&s->sess->listener->counters->internal_errors, 1); + } + + http_reply_and_close(s, txn->status, http_error_message(s)); + + end: req->analysers &= AN_REQ_FLT_END; req->analyse_exp = TICK_ETERNITY; @@ -2159,6 +2185,12 @@ int http_process_res_common(struct stream *s, struct channel *rep, int an_bit, s _HA_ATOMIC_ADD(&sess->listener->counters->denied_resp, 1); if (objt_server(s->target)) _HA_ATOMIC_ADD(&__objt_server(s->target)->counters.denied_resp, 1); + + if (txn->http_reply) { + if (http_reply_message(s, txn->http_reply) == -1) + goto return_int_err; + goto return_prx_cond; + } goto return_prx_err; return_int_err: @@ -2907,17 +2939,15 @@ static enum rule_result http_req_get_intercept_rule(struct proxy *px, struct lis goto end; case ACT_ACTION_DENY: - txn->status = rule->arg.http_deny.status; - if (rule->arg.http_deny.errmsg) - txn->errmsg = rule->arg.http_deny.errmsg; + txn->status = rule->arg.http_reply->status; + txn->http_reply = rule->arg.http_reply; rule_ret = HTTP_RULE_RES_DENY; goto end; case ACT_HTTP_REQ_TARPIT: txn->flags |= TX_CLTARPIT; - txn->status = rule->arg.http_deny.status; - if (rule->arg.http_deny.errmsg) - txn->errmsg = rule->arg.http_deny.errmsg; + txn->status = rule->arg.http_reply->status; + txn->http_reply = rule->arg.http_reply; rule_ret = HTTP_RULE_RES_DENY; goto end; @@ -3085,9 +3115,8 @@ static enum rule_result http_res_get_intercept_rule(struct proxy *px, struct lis goto end; case ACT_ACTION_DENY: - txn->status = rule->arg.http_deny.status; - if (rule->arg.http_deny.errmsg) - txn->errmsg = rule->arg.http_deny.errmsg; + txn->status = rule->arg.http_reply->status; + txn->http_reply = rule->arg.http_reply; rule_ret = HTTP_RULE_RES_DENY; goto end; @@ -4688,9 +4717,7 @@ int http_reply_message(struct stream *s, struct http_reply *reply) /* get default error message */ errmsg = http_error_message(s); } - if (b_is_null(errmsg)) - goto leave; - if (!channel_htx_copy_msg(res, htx, errmsg)) + if (!b_is_null(errmsg) && !channel_htx_copy_msg(res, htx, errmsg)) goto fail; } else { @@ -5178,6 +5205,7 @@ void http_init_txn(struct stream *s) : 0); txn->status = -1; txn->errmsg = NULL; + txn->http_reply = NULL; write_u32(txn->cache_hash, 0); txn->cookie_first_date = 0;