Files
RTLPlayground/httpd/httpd.c
T
d00f 0503e7952d httpd: check the port index /counters.json is given
The handler took one raw character of the request line and passed it to
send_counters(), which uses it to index machine.phys_to_log_port. That
array has nine entries and the character is whatever the client sent, so
the read ran up to 246 entries past the end and the result went on to
STAT_GET as a port number. is_word() accepts any request whose name is
followed by a question mark, so nothing constrained the byte to a digit.

Bounding it where it is read keeps the check beside the assumption it
protects and needs nothing from the machine description. sdcc leaves
plain char unsigned and the subtraction wraps in eight bits, so a byte
below '0' comes out above 200 and one upper test covers both ends:
exactly '0' to '8' now reach send_counters. The compiled test is
add a,#0xf7 followed by jnc, which I read back out of the assembly rather
than assuming.

Out of range answers 400 by the path the other malformed requests already
take, rather than an empty array. An empty array would have been worse
than useless here, since the statistics page calls BigInt on the first
element before it looks at the length. The page asks only for index zero
to the port count minus one, so nothing that answered before stops
answering, and a non-200 reply makes its handler do nothing at all.

11 bytes of BANK1, nothing in the common segment, BANK2, xdata or
internal RAM. Built for SWTGW218AS and KP_9000_6XHML_X2 on sdcc 4.5.0.
2026-08-15 00:49:21 +02:00

775 lines
20 KiB
C

#include "httpd.h"
#include "page_impl.h"
#include "rtl837x_common.h"
#include "rtl837x_regs.h"
#include "cmd_parser.h"
#include "rtl837x_flash.h"
#include "uip.h"
#include "html_data.h"
// #define DEBUG
#include "debug.h"
#define SESSION_ID_LENGTH 12
#define SESSION_TIMEOUT 200
#define CMARK_S 6
#pragma codeseg BANK1
#pragma constseg BANK1
extern volatile __xdata uint8_t sfr_data[4];
extern __code uint8_t * __code hex;
extern __code struct f_data f_data[];
extern __code char * __code mime_strings[];
extern __xdata struct flash_region_t flash_region;
extern __xdata uint32_t flash_size;
// Flash buffer to optimize flash writing speed, write_len is the current filling position
extern __xdata uint8_t flash_buf[FLASH_BUF_SIZE];
__xdata uint32_t uptr; // Current flash write position
__xdata uint16_t write_len;
__xdata uint8_t outbuf[TCP_OUTBUF_SIZE];
__xdata uint8_t entry;
__xdata uint16_t slen;
__xdata uint16_t o_idx;
__xdata uint16_t len_left;
__xdata uint16_t cont_len;
__xdata uint32_t cont_addr;
// HTTP header properties
__xdata uint8_t boundary[72];
__xdata uint8_t *content_type = 0;
__xdata uint8_t *session = 0;
// Global variables holding POST state
__xdata uint16_t bindex; // Current index into the boundary
__xdata uint8_t verify_crc;
__xdata uint32_t max_upload;
__xdata uint16_t short_parsed;
__xdata char passwd[21];
__xdata char session_id[SESSION_ID_LENGTH + 1];
__xdata uint8_t authenticated;
__xdata uint32_t now;
__xdata uint8_t *timeptr;
__xdata uint32_t last_session_use;
#define TSTATE_NONE 0
#define TSTATE_TX 1
#define TSTATE_ACKED 2
#define TSTATE_CLOSED 3
#define TSTATE_POST 4
#define TSTATE_MULTIPART 5
extern __xdata uint16_t crc_value;
__xdata uint16_t crc_final;
void crc16(__xdata uint8_t *v) __naked;
inline uint8_t is_separator(uint8_t c)
{
return c == ' ' || c == '\t' || c == '?' || c == '=';
}
void httpd_init(void) __banked
{
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
// Start listening to port 80
uip_listen(HTONS(80));
s->tstate = TSTATE_CLOSED;
}
uint8_t find_entry(__xdata uint8_t *e)
{
uint8_t i, j;
for (i = 0; f_data[i].len; i++) {
j = 0;
while (f_data[i].file[j] && (f_data[i].file[j] == e[j])) {
j++;
}
if ((!f_data[i].file[j]) && (!e[j])) {
return i;
}
}
return 0xff;
}
bool is_word(__xdata uint8_t *xdata_str_p, __code uint8_t * __xdata code_str_p)
{
uint8_t u, c;
while (1) {
u = *xdata_str_p++;
c = *code_str_p++;
if (c == '\0') {
if (u != '\0' && u != ' ' && u != '\t' && u != ':' && u != '?' && u != '=' && u != '\n' && u != '\r')
return false;
return true;
}
if (c != u) {
return false;
}
}
}
bool is_url_word_x(__xdata uint8_t *uri_str_p, __xdata uint8_t *src_str_p)
{
uint8_t u, s;
while(1) {
u = *uri_str_p++;
s = *src_str_p++;
if (s == '\0') {
if (u != '\0' && u != ' ' && u != '\t' && u != ':' && u != '?' && u != '=' && u != '\n' && u != '\r')
return false;
return true;
}
if (u == '%') {
bool again = true;
u = 0;
while(1) {
// Swap instruction is fine for rotation
u = (u << 4) | (u >> 4);
uint8_t p = *uri_str_p++;
u |= p - '0' < 10 ? (p - '0') : (p - 'A' + 10);
// force `jbc`-instruction.
if (again) {
again = false;
} else {
break;
}
}
} else if (u == '+') {
u = ' ';
}
if (s != u) {
return false;
}
}
}
bool is_word_x(__xdata uint8_t *lhs_str_p, __xdata uint8_t *rhs_str_p)
{
uint8_t u, c;
while (1) {
u = *lhs_str_p++;
c = *rhs_str_p++;
if (c == '\0') {
/* ';' separates cookies in a Cookie header, so it ends a value too. */
if (u != '\0' && u != ' ' && u != '\t' && u != ':' && u != '?' && u != '=' && u != '\n' && u != '\r' && u != ';')
return false;
return true;
}
if (c != u) {
return false;
}
}
}
uint8_t parse_short(__xdata uint8_t *p)
{
uint8_t err = 1;
uint8_t c = 0;
short_parsed = 0;
while(1) {
c = *p++ - '0';
if (c > 9) { break; }
err = 0;
short_parsed = (short_parsed * 10) + c;
}
return err;
}
void send_not_found(void)
{
slen = strtox(outbuf, "HTTP/1.1 404 Not found\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n" \
"<!DOCTYPE HTML PUBLIC>\n<title>404 Not Found</title>\n<h1>Not Found</h1>\n");
}
void send_bad_request(void)
{
slen = strtox(outbuf, "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n" \
"<!DOCTYPE HTML PUBLIC>\n<title>400 Bad Request</title>\n<h1>Bad Request</h1>\n");
}
void send_to_login(void)
{
slen = strtox(outbuf, "HTTP/1.1 302 Found\r\nConnection: close\r\n" \
"Location: login.html\r\n\r\n");
}
void send_unauthorized(void)
{
slen = strtox(outbuf, "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
}
__xdata uint8_t *skip_boundary(__xdata uint8_t *p)
{
while (*p) {
if (is_word_x(p, boundary))
return p + strlen_x(boundary);
p++;
}
return p;
}
__xdata uint8_t *scan_header(__xdata uint8_t *p)
{
content_type = 0;
session = 0;
authenticated = 0;
while (*p != '\r' || *(p + 1) != '\n' || *(p + 2) != '\r' || *(p + 3) != '\n') {
dbg_char(*p);
if (!*p++)
break;
if (is_word(p, "\nContent-Type:"))
content_type = p + 15;
else if (is_word(p, "\nCookie:")) {
/* Scan for the "session" key: the header may hold several
* cookies in any order. Match "session" not "session=" -
* is_word() requires a separator after the match and '=' is
* one, so this also rejects a longer key like "sessionx". */
__xdata uint8_t *c = p + 8; /* past "\nCookie:" */
while (*c && *c != '\r' && *c != '\n') {
if (is_word(c, "session")) {
session = c + 8; /* past "session=" */
break;
}
c++;
}
}
}
if (content_type && is_word(content_type, "multipart/form-data; boundary")) {
dbg_string("\nFound multipart\n");
content_type += 30;
uint8_t i = 0;
while (i < (sizeof(boundary) - 5) &&
content_type[i] != '\r' && content_type[i] != '\n') {
boundary[i + 4] = content_type[i];
i++;
}
// The boundary between parts is "\r\n--" + the boundary given in the header
boundary[0] = '\r';
boundary[1] = '\n';
boundary[2] = '-';
boundary[3] = '-';
boundary[i + 4] = 0;
}
read_reg_timer(&now);
if (session) {
if (now - last_session_use > SESSION_TIMEOUT) {
dbg_string("Session expired\n");
} else {
if (is_word_x(session, session_id))
authenticated = 1;
else
dbg_string("Invalid session cookie!\n");
}
}
return p;
}
void gen_random_bytes(__xdata uint8_t *b, uint8_t bytes)
{
__xdata uint8_t i = 0;
while (bytes) {
if (!i)
get_random_32();
b[--bytes] = itohex(sfr_data[i]);
if (!bytes) { break; }
b[--bytes] = itohex(sfr_data[i] >> 4 | sfr_data[i] << 4);
i = (i + 1) & 0x3;
}
}
/*
* Reads post data from the http stream and writes it into flash memory
* Input: the current position in the TCP buffer (uip_appdata)
* Returns 1: More data to read, 0: Upload complete, all parts reads
*/
uint8_t stream_upload(uint16_t bptr)
{
__xdata uint8_t *p = uip_appdata;
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
dbg_string("Stream_upload called: ");
dbg_short(bptr); dbg_char('\n');
do {
if (bptr >= uip_len) {
s->tstate = TSTATE_POST;
return 1;
}
// Have we reached the end of the part?
if (!boundary[bindex]) {
s->tstate = TSTATE_NONE;
dbg_string("len 2: "); dbg_short(write_len); dbg_char(' ');
flash_region.addr = uptr;
flash_region.len = write_len;
flash_write_bytes(flash_buf);
uptr += write_len;
write_len = 0;
// TODO: This is a bit premature, what about a nice web-page saying the device will reset???
if (verify_crc) {
dbg_string("CRC16: "); dbg_short(crc_final); dbg_char('\n');
if (crc_final == 0xb001) {
print_string("Checksum OK.\nUpload to flash done, will reset!\n");
// close connection to avoid retries by browser
uip_close();
reset_chip();
} else {
print_string("Checksum incorrect! Aborting.\n");
uip_close();
}
}
// Make sure there is a 0 at the end of the uploaded data
flash_buf[0] = 0;
flash_region.addr = uptr;
flash_region.len = 1;
flash_write_bytes(flash_buf);
if (bptr >= uip_len)
return 0;
if(!verify_crc)
//ugly hack to signal connection finished after config upload.
uip_close();
return 1;
}
if (p[bptr] == boundary[bindex]) {
if (!bindex)
crc_final = crc_value;
crc16(p + bptr);
bptr++;
bindex++;
} else {
if (bindex) {
memcpy(flash_buf + write_len, boundary, bindex);
write_len += bindex;
bindex = 0;
}
crc16(p + bptr);
flash_buf[write_len++] = p[bptr++];
if (write_len >= FLASH_PAGE_SIZE) {
dbg_string("len: "); dbg_short(write_len); dbg_char(' ');
dbg_string("CRC16: "); dbg_short(crc_value); dbg_char('\n');
if (uptr % FLASH_SECTOR_SIZE == 0) {
flash_region.addr = uptr;
flash_sector_erase();
}
flash_region.addr = uptr;
flash_region.len = FLASH_PAGE_SIZE;
flash_write_bytes(flash_buf);
uptr += FLASH_PAGE_SIZE;
write_len -= FLASH_PAGE_SIZE;
// Copy the remaining byte for the next page to the beginning of the buffer.
if (write_len > 0) {
memcpy(flash_buf, flash_buf + FLASH_PAGE_SIZE, write_len);
}
}
bindex = 0;
}
} while(1);
}
void handle_post(void)
{
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
__xdata uint8_t *p = uip_appdata;
__xdata uint8_t *request_path = p + 6;
// Was the multipart header sent in multiple packets?
if (s->tstate != TSTATE_MULTIPART) {
dbg_string("Is POST\n");
p += 5; // Skip post
// Find end of request path
while (*p && !is_separator(*p))
p++;
*p++ = '\0';
// Find end of request header
boundary[0] ='\0';
p = scan_header(p);
dbg_string("Boundary: >"); dbg_string_x(boundary); dbg_string("<\n");
if (!*p || !content_type) {
dbg_string("Bad Request!\n");
send_not_found();
return;
}
if (is_word(request_path, "upload")) {
if (flash_size < FIRMWARE_UPLOAD_START*2)
{
print_string("Flash too small for firmware upload!\n");
send_bad_request();
return;
}
print_string("Firmware upload started.");
uptr = FIRMWARE_UPLOAD_START;
verify_crc = 1;
max_upload = 1024576;
} else if (is_word(request_path, "config")) {
if (!authenticated) {
send_unauthorized();
return;
}
dbg_string("Configuration upload, erasing config mem!\n");
uptr = CONFIG_START;
verify_crc = 0;
max_upload = 2048;
flash_region.addr = CONFIG_START;
flash_sector_erase();
}
// Check for other POST requests, which are not multipart, below
} else {
dbg_string("Multipart request\n");
}
if (is_word(request_path, "cmd")) {
p += 4;
if (!authenticated) {
send_unauthorized();
return;
}
execute_commands(p);
if (err_status != ERR_OK) {
send_bad_request();
return;
}
} else if (is_word(request_path, "login")) {
dbg_string("POST login\n");
if (!content_type || !is_word(content_type, "application/x-www-form-urlencoded")) {
dbg_string("Bad request!\n");
send_bad_request();
return;
}
p += 8; // Read also over "pwd="
if (is_url_word_x(p, passwd)) {
dbg_string("Password accepted!\n");
read_reg_timer(&last_session_use);
gen_random_bytes(session_id, SESSION_ID_LENGTH);
session_id[SESSION_ID_LENGTH] = '\0';
slen = strtox(outbuf, "HTTP/1.1 302 Found\r\nConnection: close\r\nLocation: index.html\r\n" \
"Set-Cookie: session=");
for (register uint8_t i = 0; i < SESSION_ID_LENGTH; i++)
outbuf[slen++] = session_id[i];
slen += strtox(outbuf + slen, "; SameSite=Strict\r\n\r\n");
} else {
dbg_string("Password invalid!\n");
slen = strtox(outbuf, "HTTP/1.1 302 Found\r\nConnection: close\r\nLocation: login.html\r\n\r\n");
}
return;
} else if (s->tstate == TSTATE_MULTIPART || is_word(request_path, "upload") || is_word(request_path, "config")) {
dbg_string("POST upload/config request\n");
if (!authenticated) {
send_unauthorized();
return;
}
if (!boundary[0]) {
dbg_string("Bad request, no boundary!\n");
send_bad_request();
return;
}
// We skip the intial parts as part of the header
do {
p = skip_boundary(p);
if (!*p) {
s->tstate = TSTATE_MULTIPART;
return;
}
p = scan_header(p);
if (!*p)
goto bad_request;
if (!content_type) // We are waiting for the part with the octet stream
continue;
} while (!is_word(content_type, "application/octet-stream"));
dbg_string("Have content octets\n");
p += 4; // Skip \r\n\r\n sequence at end of preamble of part
flash_init(0); // Re-initialize flash for non-DIO operation, otherwise flashing fails
set_sys_led_state(SYS_LED_FAST);
crc_value = 0;
bindex = 0;
write_len = 0;
stream_upload(p - uip_appdata);
dbg_string("Done reading first fragment\n");
return;
} else {
send_not_found();
return;
}
slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n");
return;
bad_request:
send_bad_request();
return;
}
void httpd_appcall(void)
{
__xdata struct httpd_state * __xdata s = &(uip_conn->appstate);
dbg_char('P');
#ifdef DEBUG
if (uip_newdata())
write_char('N');
print_byte(s->tstate);
write_char(' ');
#endif
if(uip_connected() && s->tstate == TSTATE_CLOSED) {
dbg_string("Connected...\n");
s->tstate = TSTATE_NONE;
} else if (uip_closed()) {
dbg_string("Connection closed\n");
s->tstate = TSTATE_CLOSED;
} else if (uip_aborted()) {
dbg_string("Connection aborted\n");
uip_close();
s->tstate = TSTATE_CLOSED;
} else if (uip_poll()) {
uip_len = 0;
if (s->tstate == TSTATE_ACKED) {
dbg_string("Closing because everything has been transmitted\n");
uip_close();
s->tstate = TSTATE_CLOSED;
}
} else if (uip_acked() && s->tstate == TSTATE_TX) {
dbg_string("ACK\n");
if (slen > uip_mss()) {
slen -= uip_mss();
o_idx += uip_mss();
} else {
slen = 0;
o_idx += slen;
}
s->tstate = TSTATE_ACKED;
if (slen > uip_mss()) {
dbg_string("Sending A: "); dbg_short(slen); dbg_char('\n');
uip_send(outbuf + o_idx, uip_mss());
s->tstate = TSTATE_TX;
} else if (slen > 0) {
dbg_string("Sending B: "); dbg_short(slen); dbg_char('\n');
uip_send(outbuf + o_idx, slen);
s->tstate = TSTATE_TX;
} else if (cont_len) {
dbg_string("CONT cont_len: "); dbg_short(cont_len);
slen = cont_len > uip_mss() ? uip_mss() : cont_len;
if (slen > TCP_OUTBUF_SIZE)
slen = TCP_OUTBUF_SIZE;
flash_region.addr = cont_addr;
flash_region.len = slen;
flash_read_bulk(outbuf);
uip_send(outbuf, slen);
cont_len -= slen;
cont_addr += slen;
s->tstate = TSTATE_TX;
}
} else if (uip_newdata() && s->tstate == TSTATE_POST) {
// Check here maxupload by subtracting uip_len and close socekt if fails!
if (max_upload - uip_len > 0) {
stream_upload(0);
write_char('.');
} else {
send_bad_request();
goto do_send;
}
} else if (uip_newdata() && s->tstate != TSTATE_TX) {
cont_len = 0;
dbg_char('<'); dbg_short(uip_len); dbg_char('\n');
__xdata uint8_t *p = uip_appdata;
// Mark end of request header with \0
p[uip_len] = 0;
#ifdef DEBUG
while (*p)
dbg_char(*p++);
dbg_char('\n');
#endif
p = uip_appdata;
if (is_word(p, "POST") || s->tstate == TSTATE_MULTIPART) {
handle_post();
// If this is an ongoing post stream, then wait for the next packet
if (s->tstate == TSTATE_POST || s->tstate == TSTATE_MULTIPART) {
uip_len = 0;
return;
}
goto do_send;
}
if (is_word(p, "GET"))
dbg_string("GET request ");
p += 4;
scan_header(p);
__xdata uint8_t *q = p;
while (*p && !is_separator(*p))
p++;
*p = '\0';
dbg_string_x(q);
dbg_char('\n');
s->tstate = TSTATE_NONE;
entry = find_entry(q);
dbg_string("Entry is: "); dbg_byte(entry); dbg_char('\n');
if (entry == 0xff) {
if (!authenticated) {
dbg_string("Not authorized!\n");
send_unauthorized();
goto do_send;
}
dbg_string("Not file entry\n");
if (!strcmp(q, "/status.json")) {
send_status();
} else if (!strcmp(q, "/information.json")) {
send_basic_info();
} else if (!strcmp(q, "/vlan.json")) {
parse_short(q + 15);
send_vlan(short_parsed);
} else if (is_word(q, "/counters.json")) {
/* The port is one raw character of the request line and
* indexes a nine entry table, so bound it here instead
* of trusting the client to have sent a digit. Anything
* below '0' wraps well past eight, so the one test
* covers both ends. */
uint8_t cport = q[20] - '0';
if (cport > 8)
send_bad_request();
else
send_counters(cport);
} else if (is_word(q, "/eee.json")) {
send_eee();
} else if (is_word(q, "/bandwidth.json")) {
send_bandwidth();
} else if (is_word(q, "/l2.json")) {
parse_short(q + 13); // e.g.: /l2.json?idx=10
send_l2(short_parsed);
} else if (is_word(q, "/l2_del.json")) {
parse_short(q + 17);
l2_delete(short_parsed);
} else if (is_word(q, "/mirror.json")) {
send_mirror();
} else if (is_word(q, "/mtu.json")) {
send_mtu();
} else if (is_word(q, "/lag.json")) {
send_lag();
} else if (is_word(q, "/vlanlist")) {
send_vlanlist();
} else if (is_word(q, "/config")) {
send_config();
} else if (is_word(q, "/cmd_log")) {
send_cmd_log();
} else if (is_word(q, "/cmd_log_clear")) {
clear_command_history();
send_mtu(); // dummy response
} else if (is_word(q, "/reset")) {
uip_close();
delay(1000); //wait for the close packet to be sent, otherwise the browser will retry
reset_chip();
} else {
send_not_found();
}
} else {
dbg_string("Have entry, authenticated: "); dbg_byte(authenticated); dbg_char('\n');
if (!authenticated && !(f_data[entry].start == FDATA_START_login_html
|| f_data[entry].start == FDATA_START_port_svg
|| f_data[entry].start == FDATA_START_sfp_svg
|| f_data[entry].start == FDATA_START_style_css)) {
send_to_login();
goto do_send;
}
// A web-page is actively accessed, we can reset session time-out
reg_read_m(RTL837X_REG_SEC_COUNTER);
timeptr = (uint8_t*)&last_session_use; // last_session_use is Little endian
timeptr[0] = sfr_data[3]; timeptr[1] = sfr_data[2]; timeptr[2] = sfr_data[1]; timeptr[3] = sfr_data[0];
slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nContent-Type: ");
slen += strtox(outbuf + slen, mime_strings[f_data[entry].mime]);
/* 'unsafe-inline' is needed for the inline onclick handlers
* and the inline <script> on login.html. Connection: close is
* required because this httpd closes the connection after every
* response; without advertising it a browser reuses the socket
* from its keep-alive pool and the next request hits the already
* closed connection (a POST is then dropped without a retry). */
slen += strtox(outbuf + slen, "; charset=UTF-8\r\nCache-Control: max-age=60, must-revalidate\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nContent-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; form-action 'self'\r\n\r\n");
len_left = f_data[entry].len;
if (len_left > (TCP_OUTBUF_SIZE - slen)) {
cont_len = len_left - (TCP_OUTBUF_SIZE - slen);
len_left = TCP_OUTBUF_SIZE - slen;
cont_addr = f_data[entry].start + len_left;
}
dbg_string("MIME: "); dbg_string(mime_strings[f_data[entry].mime]); dbg_char('\n');
flash_region.addr = f_data[entry].start;
flash_region.len = len_left;
flash_read_bulk(outbuf + slen);
slen += len_left;
}
do_send:
dbg_string("slen: "); dbg_short(slen); dbg_char('\n');
o_idx = 0;
if (slen > uip_mss()) {
dbg_string("Sending a: "); dbg_short(slen); dbg_char('\n');
uip_send(outbuf + o_idx, uip_mss());
dbg_string("Sending a done\n");
} else {
dbg_string("Sending b: "); dbg_short(slen); dbg_char('\n');
uip_send(outbuf + o_idx, slen);
dbg_string("Sending b done\n");
}
s->tstate = TSTATE_TX;
} else if (uip_rexmit()) { // Connection established, need to rexmit?
dbg_string("RETRANSMIT requested\n");
if (slen > uip_mss()) {
dbg_string("Sending C: "); dbg_short(slen); dbg_char('\n');
uip_send(outbuf + o_idx, uip_mss());
dbg_string("Sending C done\n");
} else if (slen > 0) {
dbg_string("Sending D: "); dbg_short(slen); dbg_char('\n');
uip_send(outbuf + o_idx, slen);
dbg_string("Sending D done\n");
}
s->tstate = TSTATE_TX;
uip_len = 0;
} else {
uip_len = 0;
}
}