| /* |
| * AF_INET/AF_INET6 SOCK_STREAM protocol layer (tcp) |
| * |
| * Copyright 2000-2013 Willy Tarreau <w@1wt.eu> |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License |
| * as published by the Free Software Foundation; either version |
| * 2 of the License, or (at your option) any later version. |
| * |
| */ |
| |
| #include <ctype.h> |
| #include <errno.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <time.h> |
| |
| #include <sys/param.h> |
| #include <sys/socket.h> |
| #include <sys/types.h> |
| |
| #include <netinet/tcp.h> |
| #include <netinet/in.h> |
| |
| #include <haproxy/action-t.h> |
| #include <haproxy/api.h> |
| #include <haproxy/arg.h> |
| #include <haproxy/channel.h> |
| #include <haproxy/connection.h> |
| #include <haproxy/global.h> |
| #include <haproxy/http_rules.h> |
| #include <haproxy/proto_tcp.h> |
| #include <haproxy/proxy-t.h> |
| #include <haproxy/sample.h> |
| #include <haproxy/sc_strm.h> |
| #include <haproxy/session.h> |
| #include <haproxy/tcp_rules.h> |
| #include <haproxy/tools.h> |
| |
| /* |
| * Execute the "set-src" action. May be called from {tcp,http}request. |
| * It only changes the address and tries to preserve the original port. If the |
| * previous family was neither AF_INET nor AF_INET6, the port is set to zero. |
| */ |
| static enum act_return tcp_action_req_set_src(struct act_rule *rule, struct proxy *px, |
| struct session *sess, struct stream *s, int flags) |
| { |
| struct connection *cli_conn; |
| struct sockaddr_storage *src; |
| struct sample *smp; |
| |
| switch (rule->from) { |
| case ACT_F_TCP_REQ_CON: |
| cli_conn = objt_conn(sess->origin); |
| if (!cli_conn || !conn_get_src(cli_conn)) |
| goto end; |
| src = cli_conn->src; |
| break; |
| |
| case ACT_F_TCP_REQ_SES: |
| if (!sess_get_src(sess)) |
| goto end; |
| src = sess->src; |
| break; |
| |
| case ACT_F_TCP_REQ_CNT: |
| case ACT_F_HTTP_REQ: |
| if (!sc_get_src(s->scf)) |
| goto end; |
| src = s->scf->src; |
| break; |
| |
| default: |
| goto end; |
| } |
| |
| smp = sample_fetch_as_type(px, sess, s, SMP_OPT_DIR_REQ|SMP_OPT_FINAL, rule->arg.expr, SMP_T_ADDR); |
| if (smp) { |
| int port = get_net_port(src); |
| |
| if (smp->data.type == SMP_T_IPV4) { |
| ((struct sockaddr_in *)src)->sin_family = AF_INET; |
| ((struct sockaddr_in *)src)->sin_addr.s_addr = smp->data.u.ipv4.s_addr; |
| ((struct sockaddr_in *)src)->sin_port = port; |
| } else if (smp->data.type == SMP_T_IPV6) { |
| ((struct sockaddr_in6 *)src)->sin6_family = AF_INET6; |
| memcpy(&((struct sockaddr_in6 *)src)->sin6_addr, &smp->data.u.ipv6, sizeof(struct in6_addr)); |
| ((struct sockaddr_in6 *)src)->sin6_port = port; |
| } |
| } |
| |
| end: |
| return ACT_RET_CONT; |
| } |
| |
| /* |
| * Execute the "set-dst" action. May be called from {tcp,http}request. |
| * It only changes the address and tries to preserve the original port. If the |
| * previous family was neither AF_INET nor AF_INET6, the port is set to zero. |
| */ |
| static enum act_return tcp_action_req_set_dst(struct act_rule *rule, struct proxy *px, |
| struct session *sess, struct stream *s, int flags) |
| { |
| struct connection *cli_conn; |
| struct sockaddr_storage *dst; |
| struct sample *smp; |
| |
| switch (rule->from) { |
| case ACT_F_TCP_REQ_CON: |
| cli_conn = objt_conn(sess->origin); |
| if (!cli_conn || !conn_get_dst(cli_conn)) |
| goto end; |
| dst = cli_conn->dst; |
| break; |
| |
| case ACT_F_TCP_REQ_SES: |
| if (!sess_get_dst(sess)) |
| goto end; |
| dst = sess->dst; |
| break; |
| |
| case ACT_F_TCP_REQ_CNT: |
| case ACT_F_HTTP_REQ: |
| if (!sc_get_dst(s->scf)) |
| goto end; |
| dst = s->scf->dst; |
| break; |
| |
| default: |
| goto end; |
| } |
| |
| smp = sample_fetch_as_type(px, sess, s, SMP_OPT_DIR_REQ|SMP_OPT_FINAL, rule->arg.expr, SMP_T_ADDR); |
| if (smp) { |
| int port = get_net_port(dst); |
| |
| if (smp->data.type == SMP_T_IPV4) { |
| ((struct sockaddr_in *)dst)->sin_family = AF_INET; |
| ((struct sockaddr_in *)dst)->sin_addr.s_addr = smp->data.u.ipv4.s_addr; |
| ((struct sockaddr_in *)dst)->sin_port = port; |
| } else if (smp->data.type == SMP_T_IPV6) { |
| ((struct sockaddr_in6 *)dst)->sin6_family = AF_INET6; |
| memcpy(&((struct sockaddr_in6 *)dst)->sin6_addr, &smp->data.u.ipv6, sizeof(struct in6_addr)); |
| ((struct sockaddr_in6 *)dst)->sin6_port = port; |
| } |
| } |
| |
| end: |
| return ACT_RET_CONT; |
| } |
| |
| /* |
| * Execute the "set-src-port" action. May be called from {tcp,http}request. |
| * We must test the sin_family before setting the port. If the address family |
| * is neither AF_INET nor AF_INET6, the address is forced to AF_INET "0.0.0.0" |
| * and the port is assigned. |
| */ |
| static enum act_return tcp_action_req_set_src_port(struct act_rule *rule, struct proxy *px, |
| struct session *sess, struct stream *s, int flags) |
| { |
| struct connection *cli_conn; |
| struct sockaddr_storage *src; |
| struct sample *smp; |
| |
| switch (rule->from) { |
| case ACT_F_TCP_REQ_CON: |
| cli_conn = objt_conn(sess->origin); |
| if (!cli_conn || !conn_get_src(cli_conn)) |
| goto end; |
| src = cli_conn->src; |
| break; |
| |
| case ACT_F_TCP_REQ_SES: |
| if (!sess_get_src(sess)) |
| goto end; |
| src = sess->src; |
| break; |
| |
| case ACT_F_TCP_REQ_CNT: |
| case ACT_F_HTTP_REQ: |
| if (!sc_get_src(s->scf)) |
| goto end; |
| src = s->scf->src; |
| break; |
| |
| default: |
| goto end; |
| } |
| |
| smp = sample_fetch_as_type(px, sess, s, SMP_OPT_DIR_REQ|SMP_OPT_FINAL, rule->arg.expr, SMP_T_SINT); |
| if (smp) { |
| if (src->ss_family == AF_INET6) { |
| ((struct sockaddr_in6 *)src)->sin6_port = htons(smp->data.u.sint); |
| } else { |
| if (src->ss_family != AF_INET) { |
| src->ss_family = AF_INET; |
| ((struct sockaddr_in *)src)->sin_addr.s_addr = 0; |
| } |
| ((struct sockaddr_in *)src)->sin_port = htons(smp->data.u.sint); |
| } |
| } |
| |
| end: |
| return ACT_RET_CONT; |
| } |
| |
| /* |
| * Execute the "set-dst-port" action. May be called from {tcp,http}request. |
| * We must test the sin_family before setting the port. If the address family |
| * is neither AF_INET nor AF_INET6, the address is forced to AF_INET "0.0.0.0" |
| * and the port is assigned. |
| */ |
| static enum act_return tcp_action_req_set_dst_port(struct act_rule *rule, struct proxy *px, |
| struct session *sess, struct stream *s, int flags) |
| { |
| struct connection *cli_conn; |
| struct sockaddr_storage *dst; |
| struct sample *smp; |
| |
| switch (rule->from) { |
| case ACT_F_TCP_REQ_CON: |
| cli_conn = objt_conn(sess->origin); |
| if (!cli_conn || !conn_get_dst(cli_conn)) |
| goto end; |
| dst = cli_conn->dst; |
| break; |
| |
| case ACT_F_TCP_REQ_SES: |
| if (!sess_get_dst(sess)) |
| goto end; |
| dst = sess->dst; |
| break; |
| |
| case ACT_F_TCP_REQ_CNT: |
| case ACT_F_HTTP_REQ: |
| if (!sc_get_dst(s->scf)) |
| goto end; |
| dst = s->scf->dst; |
| break; |
| |
| default: |
| goto end; |
| } |
| |
| smp = sample_fetch_as_type(px, sess, s, SMP_OPT_DIR_REQ|SMP_OPT_FINAL, rule->arg.expr, SMP_T_SINT); |
| if (smp) { |
| if (dst->ss_family == AF_INET6) { |
| ((struct sockaddr_in6 *)dst)->sin6_port = htons(smp->data.u.sint); |
| } else { |
| if (dst->ss_family != AF_INET) { |
| dst->ss_family = AF_INET; |
| ((struct sockaddr_in *)dst)->sin_addr.s_addr = 0; |
| } |
| ((struct sockaddr_in *)dst)->sin_port = htons(smp->data.u.sint); |
| } |
| } |
| |
| end: |
| return ACT_RET_CONT; |
| } |
| |
| /* Executes the "silent-drop" action. May be called from {tcp,http}{request,response}. |
| * If rule->arg.act.p[0] is 0, TCP_REPAIR is tried first, with a fallback to |
| * sending a RST with TTL 1 towards the client. If it is [1-255], we will skip |
| * TCP_REPAIR and prepare the socket to send a RST with the requested TTL when |
| * the connection is killed by channel_abort(). |
| */ |
| static enum act_return tcp_exec_action_silent_drop(struct act_rule *rule, struct proxy *px, |
| struct session *sess, struct stream *strm, int flags) |
| { |
| struct connection *conn = objt_conn(sess->origin); |
| unsigned int ttl __maybe_unused = (uintptr_t)rule->arg.act.p[0]; |
| char tcp_repair_enabled __maybe_unused; |
| |
| if (ttl == 0) { |
| tcp_repair_enabled = 1; |
| ttl = 1; |
| } else { |
| tcp_repair_enabled = 0; |
| } |
| |
| if (!conn) |
| goto out; |
| |
| if (!conn_ctrl_ready(conn)) |
| goto out; |
| |
| #ifdef TCP_QUICKACK |
| /* drain is needed only to send the quick ACK */ |
| conn_ctrl_drain(conn); |
| |
| /* re-enable quickack if it was disabled to ack all data and avoid |
| * retransmits from the client that might trigger a real reset. |
| */ |
| setsockopt(conn->handle.fd, IPPROTO_TCP, TCP_QUICKACK, &one, sizeof(one)); |
| #endif |
| /* lingering must absolutely be disabled so that we don't send a |
| * shutdown(), this is critical to the TCP_REPAIR trick. When no stream |
| * is present, returning with ERR will cause lingering to be disabled. |
| */ |
| if (strm) |
| strm->scf->flags |= SC_FL_NOLINGER; |
| |
| if (conn->flags & CO_FL_FDLESS) |
| goto out; |
| |
| /* We're on the client-facing side, we must force to disable lingering to |
| * ensure we will use an RST exclusively and kill any pending data. |
| */ |
| HA_ATOMIC_OR(&fdtab[conn->handle.fd].state, FD_LINGER_RISK); |
| |
| #ifdef TCP_REPAIR |
| /* try to put socket in repair mode if sending a RST was not requested by |
| * config. this often fails due to missing permissions (CAP_NET_ADMIN capability) |
| */ |
| if (tcp_repair_enabled && (setsockopt(conn->handle.fd, IPPROTO_TCP, TCP_REPAIR, &one, sizeof(one)) == 0)) { |
| /* socket will be quiet now */ |
| goto out; |
| } |
| #endif |
| |
| /* Either TCP_REPAIR is not defined, it failed (eg: permissions), or was |
| * not executed because a RST with a specific TTL was requested to be sent. |
| * Set the TTL of the client connection before the connection is killed |
| * by channel_abort and a RST packet will be emitted by the TCP/IP stack. |
| */ |
| #ifdef IP_TTL |
| if (conn->src && conn->src->ss_family == AF_INET) |
| setsockopt(conn->handle.fd, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)); |
| #endif |
| #ifdef IPV6_UNICAST_HOPS |
| if (conn->src && conn->src->ss_family == AF_INET6) |
| setsockopt(conn->handle.fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &ttl, sizeof(ttl)); |
| #endif |
| out: |
| /* kill the stream if any */ |
| if (strm) { |
| channel_abort(&strm->req); |
| channel_abort(&strm->res); |
| strm->req.analysers &= AN_REQ_FLT_END; |
| strm->res.analysers &= AN_RES_FLT_END; |
| if (strm->flags & SF_BE_ASSIGNED) |
| _HA_ATOMIC_INC(&strm->be->be_counters.denied_req); |
| if (!(strm->flags & SF_ERR_MASK)) |
| strm->flags |= SF_ERR_PRXCOND; |
| if (!(strm->flags & SF_FINST_MASK)) |
| strm->flags |= SF_FINST_R; |
| } |
| |
| _HA_ATOMIC_INC(&sess->fe->fe_counters.denied_req); |
| if (sess->listener && sess->listener->counters) |
| _HA_ATOMIC_INC(&sess->listener->counters->denied_req); |
| |
| return ACT_RET_ABRT; |
| } |
| |
| |
| #if defined(SO_MARK) || defined(SO_USER_COOKIE) || defined(SO_RTABLE) |
| static enum act_return tcp_action_set_mark(struct act_rule *rule, struct proxy *px, |
| struct session *sess, struct stream *s, int flags) |
| { |
| conn_set_mark(objt_conn(sess->origin), (uintptr_t)rule->arg.act.p[0]); |
| return ACT_RET_CONT; |
| } |
| #endif |
| |
| #ifdef IP_TOS |
| static enum act_return tcp_action_set_tos(struct act_rule *rule, struct proxy *px, |
| struct session *sess, struct stream *s, int flags) |
| { |
| conn_set_tos(objt_conn(sess->origin), (uintptr_t)rule->arg.act.p[0]); |
| return ACT_RET_CONT; |
| } |
| #endif |
| |
| /* |
| * Release the sample expr when releasing a set src/dst action |
| */ |
| static void release_set_src_dst_action(struct act_rule *rule) |
| { |
| release_sample_expr(rule->arg.expr); |
| } |
| |
| /* parse "set-{src,dst}[-port]" action */ |
| static enum act_parse_ret tcp_parse_set_src_dst(const char **args, int *orig_arg, struct proxy *px, |
| struct act_rule *rule, char **err) |
| { |
| int cur_arg; |
| struct sample_expr *expr; |
| unsigned int where; |
| |
| cur_arg = *orig_arg; |
| expr = sample_parse_expr((char **)args, &cur_arg, px->conf.args.file, px->conf.args.line, err, &px->conf.args, NULL); |
| if (!expr) |
| return ACT_RET_PRS_ERR; |
| |
| where = 0; |
| if (px->cap & PR_CAP_FE) |
| where |= SMP_VAL_FE_HRQ_HDR; |
| if (px->cap & PR_CAP_BE) |
| where |= SMP_VAL_BE_HRQ_HDR; |
| |
| if (!(expr->fetch->val & where)) { |
| memprintf(err, |
| "fetch method '%s' extracts information from '%s', none of which is available here", |
| args[cur_arg-1], sample_src_names(expr->fetch->use)); |
| free(expr); |
| return ACT_RET_PRS_ERR; |
| } |
| rule->arg.expr = expr; |
| rule->action = ACT_CUSTOM; |
| |
| if (strcmp(args[*orig_arg - 1], "set-src") == 0) { |
| rule->action_ptr = tcp_action_req_set_src; |
| } else if (strcmp(args[*orig_arg - 1], "set-src-port") == 0) { |
| rule->action_ptr = tcp_action_req_set_src_port; |
| } else if (strcmp(args[*orig_arg - 1], "set-dst") == 0) { |
| rule->action_ptr = tcp_action_req_set_dst; |
| } else if (strcmp(args[*orig_arg - 1], "set-dst-port") == 0) { |
| rule->action_ptr = tcp_action_req_set_dst_port; |
| } else { |
| return ACT_RET_PRS_ERR; |
| } |
| |
| rule->release_ptr = release_set_src_dst_action; |
| (*orig_arg)++; |
| |
| return ACT_RET_PRS_OK; |
| } |
| |
| |
| /* Parse a "set-mark" action. It takes the MARK value as argument. It returns |
| * ACT_RET_PRS_OK on success, ACT_RET_PRS_ERR on error. |
| */ |
| static enum act_parse_ret tcp_parse_set_mark(const char **args, int *cur_arg, struct proxy *px, |
| struct act_rule *rule, char **err) |
| { |
| #if defined(SO_MARK) || defined(SO_USER_COOKIE) || defined(SO_RTABLE) |
| char *endp; |
| unsigned int mark; |
| |
| if (!*args[*cur_arg]) { |
| memprintf(err, "expects exactly 1 argument (integer/hex value)"); |
| return ACT_RET_PRS_ERR; |
| } |
| mark = strtoul(args[*cur_arg], &endp, 0); |
| if (endp && *endp != '\0') { |
| memprintf(err, "invalid character starting at '%s' (integer/hex value expected)", endp); |
| return ACT_RET_PRS_ERR; |
| } |
| |
| (*cur_arg)++; |
| |
| /* Register processing function. */ |
| rule->action_ptr = tcp_action_set_mark; |
| rule->action = ACT_CUSTOM; |
| rule->arg.act.p[0] = (void *)(uintptr_t)mark; |
| global.last_checks |= LSTCHK_NETADM; |
| return ACT_RET_PRS_OK; |
| #else |
| memprintf(err, "not supported on this platform (SO_MARK|SO_USER_COOKIE|SO_RTABLE undefined)"); |
| return ACT_RET_PRS_ERR; |
| #endif |
| } |
| |
| |
| /* Parse a "set-tos" action. It takes the TOS value as argument. It returns |
| * ACT_RET_PRS_OK on success, ACT_RET_PRS_ERR on error. |
| */ |
| static enum act_parse_ret tcp_parse_set_tos(const char **args, int *cur_arg, struct proxy *px, |
| struct act_rule *rule, char **err) |
| { |
| #ifdef IP_TOS |
| char *endp; |
| int tos; |
| |
| if (!*args[*cur_arg]) { |
| memprintf(err, "expects exactly 1 argument (integer/hex value)"); |
| return ACT_RET_PRS_ERR; |
| } |
| tos = strtol(args[*cur_arg], &endp, 0); |
| if (endp && *endp != '\0') { |
| memprintf(err, "invalid character starting at '%s' (integer/hex value expected)", endp); |
| return ACT_RET_PRS_ERR; |
| } |
| |
| (*cur_arg)++; |
| |
| /* Register processing function. */ |
| rule->action_ptr = tcp_action_set_tos; |
| rule->action = ACT_CUSTOM; |
| rule->arg.act.p[0] = (void *)(uintptr_t)tos; |
| return ACT_RET_PRS_OK; |
| #else |
| memprintf(err, "not supported on this platform (IP_TOS undefined)"); |
| return ACT_RET_PRS_ERR; |
| #endif |
| } |
| |
| /* Parse a "silent-drop" action. It may take 2 optional arguments to define a |
| * "rst-ttl" parameter. It returns ACT_RET_PRS_OK on success, ACT_RET_PRS_ERR |
| * on error. |
| */ |
| static enum act_parse_ret tcp_parse_silent_drop(const char **args, int *cur_arg, struct proxy *px, |
| struct act_rule *rule, char **err) |
| { |
| unsigned int rst_ttl = 0; |
| char *endp; |
| |
| rule->action = ACT_CUSTOM; |
| rule->action_ptr = tcp_exec_action_silent_drop; |
| |
| if (strcmp(args[*cur_arg], "rst-ttl") == 0) { |
| if (!*args[*cur_arg + 1]) { |
| memprintf(err, "missing rst-ttl value\n"); |
| return ACT_RET_PRS_ERR; |
| } |
| |
| rst_ttl = (unsigned int)strtoul(args[*cur_arg + 1], &endp, 0); |
| |
| if (endp && *endp != '\0') { |
| memprintf(err, "invalid character starting at '%s' (value 1-255 expected)\n", |
| endp); |
| return ACT_RET_PRS_ERR; |
| } |
| if ((rst_ttl == 0) || (rst_ttl > 255) ) { |
| memprintf(err, "valid rst-ttl values are [1-255]\n"); |
| return ACT_RET_PRS_ERR; |
| } |
| |
| *cur_arg += 2; |
| } |
| |
| rule->arg.act.p[0] = (void *)(uintptr_t)rst_ttl; |
| return ACT_RET_PRS_OK; |
| } |
| |
| |
| static struct action_kw_list tcp_req_conn_actions = {ILH, { |
| { "set-dst" , tcp_parse_set_src_dst }, |
| { "set-dst-port", tcp_parse_set_src_dst }, |
| { "set-mark", tcp_parse_set_mark }, |
| { "set-src", tcp_parse_set_src_dst }, |
| { "set-src-port", tcp_parse_set_src_dst }, |
| { "set-tos", tcp_parse_set_tos }, |
| { "silent-drop", tcp_parse_silent_drop }, |
| { /* END */ } |
| }}; |
| |
| INITCALL1(STG_REGISTER, tcp_req_conn_keywords_register, &tcp_req_conn_actions); |
| |
| static struct action_kw_list tcp_req_sess_actions = {ILH, { |
| { "set-dst" , tcp_parse_set_src_dst }, |
| { "set-dst-port", tcp_parse_set_src_dst }, |
| { "set-mark", tcp_parse_set_mark }, |
| { "set-src", tcp_parse_set_src_dst }, |
| { "set-src-port", tcp_parse_set_src_dst }, |
| { "set-tos", tcp_parse_set_tos }, |
| { "silent-drop", tcp_parse_silent_drop }, |
| { /* END */ } |
| }}; |
| |
| INITCALL1(STG_REGISTER, tcp_req_sess_keywords_register, &tcp_req_sess_actions); |
| |
| static struct action_kw_list tcp_req_cont_actions = {ILH, { |
| { "set-src", tcp_parse_set_src_dst }, |
| { "set-src-port", tcp_parse_set_src_dst }, |
| { "set-dst" , tcp_parse_set_src_dst }, |
| { "set-dst-port", tcp_parse_set_src_dst }, |
| { "set-mark", tcp_parse_set_mark }, |
| { "set-tos", tcp_parse_set_tos }, |
| { "silent-drop", tcp_parse_silent_drop }, |
| { /* END */ } |
| }}; |
| |
| INITCALL1(STG_REGISTER, tcp_req_cont_keywords_register, &tcp_req_cont_actions); |
| |
| static struct action_kw_list tcp_res_cont_actions = {ILH, { |
| { "set-mark", tcp_parse_set_mark }, |
| { "set-tos", tcp_parse_set_tos }, |
| { "silent-drop", tcp_parse_silent_drop }, |
| { /* END */ } |
| }}; |
| |
| INITCALL1(STG_REGISTER, tcp_res_cont_keywords_register, &tcp_res_cont_actions); |
| |
| static struct action_kw_list http_req_actions = {ILH, { |
| { "set-dst", tcp_parse_set_src_dst }, |
| { "set-dst-port", tcp_parse_set_src_dst }, |
| { "set-mark", tcp_parse_set_mark }, |
| { "set-src", tcp_parse_set_src_dst }, |
| { "set-src-port", tcp_parse_set_src_dst }, |
| { "set-tos", tcp_parse_set_tos }, |
| { "silent-drop", tcp_parse_silent_drop }, |
| { /* END */ } |
| }}; |
| |
| INITCALL1(STG_REGISTER, http_req_keywords_register, &http_req_actions); |
| |
| static struct action_kw_list http_res_actions = {ILH, { |
| { "set-mark", tcp_parse_set_mark }, |
| { "set-tos", tcp_parse_set_tos }, |
| { "silent-drop", tcp_parse_silent_drop }, |
| { /* END */ } |
| }}; |
| |
| INITCALL1(STG_REGISTER, http_res_keywords_register, &http_res_actions); |
| |
| |
| /* |
| * Local variables: |
| * c-indent-level: 8 |
| * c-basic-offset: 8 |
| * End: |
| */ |