tor/src/or/circuit.c

633 lines
19 KiB
C
Raw Normal View History

/* Copyright 2001,2002 Roger Dingledine, Matej Pfajfar. */
/* See LICENSE for licensing information */
/* $Id$ */
2002-06-27 00:45:49 +02:00
#include "or.h"
/********* START VARIABLES **********/
2002-07-05 08:27:23 +02:00
static circuit_t *global_circuitlist=NULL;
2002-06-27 00:45:49 +02:00
char *circuit_state_to_string[] = {
"receiving the onion", /* 0 */
"waiting to process create", /* 1 */
"connecting to firsthop", /* 2 */
"open" /* 3 */
};
2002-06-27 00:45:49 +02:00
/********* END VARIABLES ************/
void circuit_add(circuit_t *circ) {
if(!global_circuitlist) { /* first one */
global_circuitlist = circ;
circ->next = NULL;
} else {
circ->next = global_circuitlist;
global_circuitlist = circ;
}
}
void circuit_remove(circuit_t *circ) {
circuit_t *tmpcirc;
2002-07-05 08:27:23 +02:00
assert(circ && global_circuitlist);
2002-06-27 00:45:49 +02:00
if(global_circuitlist == circ) {
global_circuitlist = global_circuitlist->next;
return;
}
for(tmpcirc = global_circuitlist;tmpcirc->next;tmpcirc = tmpcirc->next) {
if(tmpcirc->next == circ) {
tmpcirc->next = circ->next;
return;
}
}
}
circuit_t *circuit_new(aci_t p_aci, connection_t *p_conn) {
circuit_t *circ;
circ = (circuit_t *)malloc(sizeof(circuit_t));
if(!circ)
return NULL;
memset(circ,0,sizeof(circuit_t)); /* zero it out */
circ->p_aci = p_aci;
circ->p_conn = p_conn;
circ->state = CIRCUIT_STATE_ONION_WAIT;
2002-06-27 00:45:49 +02:00
/* ACIs */
circ->p_aci = p_aci;
2002-07-05 08:27:23 +02:00
/* circ->n_aci remains 0 because we haven't identified the next hop yet */
2002-06-27 00:45:49 +02:00
circ->n_receive_circwindow = CIRCWINDOW_START;
circ->p_receive_circwindow = CIRCWINDOW_START;
2002-06-27 00:45:49 +02:00
circuit_add(circ);
return circ;
}
void circuit_free(circuit_t *circ) {
struct data_queue_t *tmpd;
if (circ->n_crypto)
crypto_free_cipher_env(circ->n_crypto);
if (circ->p_crypto)
crypto_free_cipher_env(circ->p_crypto);
2002-06-27 00:45:49 +02:00
if(circ->onion)
free(circ->onion);
if(circ->cpath)
circuit_free_cpath(circ->cpath, circ->cpathlen);
while(circ->data_queue) {
tmpd = circ->data_queue;
circ->data_queue = tmpd->next;
free(tmpd->cell);
free(tmpd);
}
2002-06-27 00:45:49 +02:00
free(circ);
}
void circuit_free_cpath(crypt_path_t **cpath, int cpathlen) {
int i;
for(i=0;i<cpathlen;i++)
free(cpath[i]);
2002-06-27 00:45:49 +02:00
free(cpath);
2002-06-27 00:45:49 +02:00
}
2002-12-30 09:51:41 +01:00
/* return 0 if can't get a unique aci. */
2002-06-27 00:45:49 +02:00
aci_t get_unique_aci_by_addr_port(uint32_t addr, uint16_t port, int aci_type) {
aci_t test_aci;
connection_t *conn;
2002-12-30 09:51:41 +01:00
try_again:
2002-06-27 00:45:49 +02:00
log(LOG_DEBUG,"get_unique_aci_by_addr_port() trying to get a unique aci");
crypto_pseudo_rand(2, (unsigned char *)&test_aci);
2002-06-27 00:45:49 +02:00
2002-12-30 09:51:41 +01:00
if(aci_type == ACI_TYPE_LOWER && test_aci >= (2<<15))
test_aci -= (2<<15);
if(aci_type == ACI_TYPE_HIGHER && test_aci < (2<<15))
test_aci += (2<<15);
/* if aci_type == ACI_BOTH, don't filter any of it */
2002-06-27 00:45:49 +02:00
if(test_aci == 0)
2002-12-30 09:51:41 +01:00
goto try_again;
2002-06-27 00:45:49 +02:00
conn = connection_exact_get_by_addr_port(addr,port);
2002-06-27 00:45:49 +02:00
if(!conn) /* there can't be a conflict -- no connection of that sort yet */
return test_aci;
if(circuit_get_by_aci_conn(test_aci, conn))
2002-12-30 09:51:41 +01:00
goto try_again;
2002-06-27 00:45:49 +02:00
return test_aci;
}
int circuit_init(circuit_t *circ, int aci_type) {
unsigned char iv[16];
2002-06-27 00:45:49 +02:00
unsigned char digest1[20];
unsigned char digest2[20];
struct timeval start, end;
int time_passed;
assert(circ && circ->onion);
2002-06-27 00:45:49 +02:00
log(LOG_DEBUG,"circuit_init(): starting");
circ->n_port = ntohs(*(uint16_t *)(circ->onion+2));
log(LOG_DEBUG,"circuit_init(): Set port to %u.",circ->n_port);
circ->n_addr = ntohl(*(uint32_t *)(circ->onion+4));
circ->p_f = *(circ->onion+1) >> 4; /* backf */
log(LOG_DEBUG,"circuit_init(): Set BACKF to %u.",circ->p_f);
circ->n_f = *(circ->onion+1) & 0x0f; /* forwf */
log(LOG_DEBUG,"circuit_init(): Set FORWF to %u.",circ->n_f);
2002-06-27 00:45:49 +02:00
circ->state = CIRCUIT_STATE_OPEN;
log(LOG_DEBUG,"circuit_init(): aci_type = %u.",aci_type);
gettimeofday(&start,NULL);
2002-06-27 00:45:49 +02:00
circ->n_aci = get_unique_aci_by_addr_port(circ->n_addr, circ->n_port, aci_type);
2002-12-30 09:51:41 +01:00
if(!circ->n_aci) {
log(LOG_ERR,"circuit_init(): failed to get unique aci.");
return -1;
}
2002-06-27 00:45:49 +02:00
gettimeofday(&end,NULL);
if(end.tv_usec < start.tv_usec) {
end.tv_sec--;
end.tv_usec += 1000000;
}
time_passed = ((end.tv_sec - start.tv_sec)*1000000) + (end.tv_usec - start.tv_usec);
if(time_passed > 1000) { /* more than 1ms */
log(LOG_NOTICE,"circuit_init(): get_unique_aci just took %d us!",time_passed);
}
2002-06-27 00:45:49 +02:00
log(LOG_DEBUG,"circuit_init(): Chosen ACI %u.",circ->n_aci);
/* keys */
memset(iv, 0, 16);
crypto_SHA_digest(circ->onion+12,16,digest1);
crypto_SHA_digest(digest1,20,digest2);
crypto_SHA_digest(digest2,20,digest1);
2002-06-27 00:45:49 +02:00
log(LOG_DEBUG,"circuit_init(): Computed keys.");
if (!(circ->p_crypto = create_onion_cipher(circ->p_f,digest2,iv,1))) {
2002-06-27 00:45:49 +02:00
log(LOG_ERR,"Cipher initialization failed (ACI %u).",circ->n_aci);
return -1;
}
if (!(circ->n_crypto = create_onion_cipher(circ->n_f, digest1, iv, 0))) {
2002-06-27 00:45:49 +02:00
log(LOG_ERR,"Cipher initialization failed (ACI %u).",circ->n_aci);
return -1;
}
2002-06-27 00:45:49 +02:00
log(LOG_DEBUG,"circuit_init(): Cipher initialization complete.");
circ->expire = ntohl(*(uint32_t *)(circ->onion+8));
2002-06-27 00:45:49 +02:00
return 0;
}
circuit_t *circuit_enumerate_by_naddr_nport(circuit_t *circ, uint32_t naddr, uint16_t nport) {
if(!circ) /* use circ if it's defined, else start from the beginning */
circ = global_circuitlist;
else
circ = circ->next;
for( ; circ; circ = circ->next) {
if(circ->n_addr == naddr && circ->n_port == nport)
return circ;
}
return NULL;
}
2002-06-27 00:45:49 +02:00
circuit_t *circuit_get_by_aci_conn(aci_t aci, connection_t *conn) {
circuit_t *circ;
connection_t *tmpconn;
2002-06-27 00:45:49 +02:00
for(circ=global_circuitlist;circ;circ = circ->next) {
if(circ->p_aci == aci) {
for(tmpconn = circ->p_conn; tmpconn; tmpconn = tmpconn->next_topic) {
if(tmpconn == conn)
return circ;
}
}
if(circ->n_aci == aci) {
for(tmpconn = circ->n_conn; tmpconn; tmpconn = tmpconn->next_topic) {
if(tmpconn == conn)
return circ;
}
}
2002-06-27 00:45:49 +02:00
}
return NULL;
}
circuit_t *circuit_get_by_conn(connection_t *conn) {
circuit_t *circ;
connection_t *tmpconn;
2002-06-27 00:45:49 +02:00
for(circ=global_circuitlist;circ;circ = circ->next) {
for(tmpconn = circ->p_conn; tmpconn; tmpconn=tmpconn->next_topic)
if(tmpconn == conn)
return circ;
for(tmpconn = circ->n_conn; tmpconn; tmpconn=tmpconn->next_topic)
if(tmpconn == conn)
return circ;
2002-06-27 00:45:49 +02:00
}
return NULL;
}
circuit_t *circuit_get_by_edge_type(char edge_type) {
circuit_t *circ;
for(circ=global_circuitlist;circ;circ = circ->next) {
if(edge_type == EDGE_AP && circ->n_conn && circ->n_conn->type == CONN_TYPE_OR) {
log(LOG_DEBUG,"circuit_get_by_edge_type(): Choosing n_aci %d.", circ->n_aci);
return circ;
}
if(edge_type == EDGE_EXIT && circ->p_conn && circ->p_conn->type == CONN_TYPE_OR) {
return circ;
}
log(LOG_DEBUG,"circuit_get_by_edge_type(): Skipping p_aci %d / n_aci %d.", circ->p_aci, circ->n_aci);
}
return NULL;
}
int circuit_deliver_data_cell_from_edge(cell_t *cell, circuit_t *circ, char edge_type) {
int cell_direction;
static int numsent_ap=0, numsent_exit=0;
log(LOG_DEBUG,"circuit_deliver_data_cell_from_edge(): called, edge_type %d.", edge_type);
2002-06-27 00:45:49 +02:00
if(edge_type == EDGE_AP) { /* i'm the AP */
cell_direction = CELL_DIRECTION_OUT;
numsent_ap++;
log(LOG_DEBUG,"circuit_deliver_data_cell_from_edge(): now sent %d data cells from ap", numsent_ap);
if(circ->p_receive_circwindow <= 0) {
log(LOG_DEBUG,"circuit_deliver_data_cell_from_edge(): pwindow 0, queueing for later.");
circ->data_queue = data_queue_add(circ->data_queue, cell);
return 0;
}
circ->p_receive_circwindow--;
// log(LOG_INFO,"circuit_deliver_data_cell_from_edge(): p_receive_circwindow now %d.",circ->p_receive_circwindow);
} else { /* i'm the exit */
cell_direction = CELL_DIRECTION_IN;
numsent_exit++;
log(LOG_DEBUG,"circuit_deliver_data_cell_from_edge(): now sent %d data cells from exit", numsent_exit);
if(circ->n_receive_circwindow <= 0) {
log(LOG_DEBUG,"circuit_deliver_data_cell_from_edge(): nwindow 0, queueing for later.");
circ->data_queue = data_queue_add(circ->data_queue, cell);
return 0;
}
circ->n_receive_circwindow--;
}
if(circuit_deliver_data_cell(cell, circ, cell_direction) < 0) {
return -1;
}
circuit_consider_stop_edge_reading(circ, edge_type); /* has window reached 0? */
return 0;
}
int circuit_deliver_data_cell(cell_t *cell, circuit_t *circ, int cell_direction) {
connection_t *conn;
assert(cell && circ);
assert(cell_direction == CELL_DIRECTION_OUT || cell_direction == CELL_DIRECTION_IN);
if(cell_direction == CELL_DIRECTION_OUT)
conn = circ->n_conn;
else
conn = circ->p_conn;
/* first crypt cell->length */
if(circuit_crypt(circ, &(cell->length), 1, cell_direction) < 0) {
log(LOG_DEBUG,"circuit_deliver_data_cell(): length crypt failed. Dropping connection.");
2002-06-27 00:45:49 +02:00
return -1;
}
/* then crypt the payload */
if(circuit_crypt(circ, (char *)&(cell->payload), CELL_PAYLOAD_SIZE, cell_direction) < 0) {
log(LOG_DEBUG,"circuit_deliver_data_cell(): payload crypt failed. Dropping connection.");
2002-06-27 00:45:49 +02:00
return -1;
}
2003-03-05 21:03:05 +01:00
if(cell_direction == CELL_DIRECTION_OUT && (!conn || conn->type == CONN_TYPE_EXIT)) {
log(LOG_DEBUG,"circuit_deliver_data_cell(): Sending to exit.");
return connection_exit_process_data_cell(cell, circ);
2002-06-27 00:45:49 +02:00
}
2003-03-05 21:03:05 +01:00
if(cell_direction == CELL_DIRECTION_IN && (!conn || conn->type == CONN_TYPE_AP)) {
log(LOG_DEBUG,"circuit_deliver_data_cell(): Sending to AP.");
return connection_ap_process_data_cell(cell, circ);
}
/* else send it as a cell */
assert(conn);
//log(LOG_DEBUG,"circuit_deliver_data_cell(): Sending to connection.");
return connection_write_cell_to_buf(cell, conn);
2002-06-27 00:45:49 +02:00
}
int circuit_crypt(circuit_t *circ, char *in, int inlen, char cell_direction) {
2002-06-27 00:45:49 +02:00
char *out;
int i;
crypt_path_t *thishop;
2002-06-27 00:45:49 +02:00
assert(circ && in);
2002-06-27 00:45:49 +02:00
out = (char *)malloc(inlen);
2002-06-27 00:45:49 +02:00
if(!out)
return -1;
2003-03-05 21:03:05 +01:00
if(cell_direction == CELL_DIRECTION_IN) {
if(circ->cpath) { /* we're at the beginning of the circuit. We'll want to do layered crypts. */
for (i=circ->cpathlen-1; i >= 0; i--) /* moving from first to last hop
* Remember : cpath is in reverse order, i.e. last hop first
*/
{
thishop = circ->cpath[i];
/* decrypt */
if(crypto_cipher_decrypt(thishop->b_crypto, in, inlen, out)) {
log(LOG_ERR,"Error performing decryption:%s",crypto_perror());
free(out);
return -1;
}
/* copy ciphertext back to buf */
memcpy(in,out,inlen);
}
} else { /* we're in the middle. Just one crypt. */
if(crypto_cipher_encrypt(circ->p_crypto,in, inlen, out)) {
log(LOG_ERR,"circuit_encrypt(): Encryption failed for ACI : %u (%s).",
circ->p_aci, crypto_perror());
free(out);
return -1;
}
memcpy(in,out,inlen);
2002-06-27 00:45:49 +02:00
}
2003-03-05 21:03:05 +01:00
} else if(cell_direction == CELL_DIRECTION_OUT) {
if(circ->cpath) { /* we're at the beginning of the circuit. We'll want to do layered crypts. */
for (i=0; i < circ->cpathlen; i++) /* moving from last to first hop
* Remember : cpath is in reverse order, i.e. last hop first
*/
{
thishop = circ->cpath[i];
/* encrypt */
if(crypto_cipher_encrypt(thishop->f_crypto, in, inlen, (unsigned char *)out)) {
log(LOG_ERR,"Error performing encryption:%s",crypto_perror());
free(out);
return -1;
}
/* copy ciphertext back to buf */
memcpy(in,out,inlen);
}
} else { /* we're in the middle. Just one crypt. */
if(crypto_cipher_decrypt(circ->n_crypto,in, inlen, out)) {
log(LOG_ERR,"circuit_crypt(): Decryption failed for ACI : %u (%s).",
circ->n_aci, crypto_perror());
free(out);
return -1;
}
memcpy(in,out,inlen);
2002-06-27 00:45:49 +02:00
}
} else {
log(LOG_ERR,"circuit_crypt(): unknown cell direction %d.", cell_direction);
assert(0);
2002-06-27 00:45:49 +02:00
}
free(out);
return 0;
}
void circuit_resume_edge_reading(circuit_t *circ, int edge_type) {
connection_t *conn;
struct data_queue_t *tmpd;
assert(edge_type == EDGE_EXIT || edge_type == EDGE_AP);
/* first, send the queue waiting at circ onto the circuit */
while(circ->data_queue) {
assert(circ->data_queue->cell);
if(edge_type == EDGE_EXIT) {
circ->n_receive_circwindow--;
assert(circ->n_receive_circwindow >= 0);
if(circuit_deliver_data_cell(circ->data_queue->cell, circ, CELL_DIRECTION_IN) < 0) {
circuit_close(circ);
return;
}
} else { /* ap */
circ->p_receive_circwindow--;
assert(circ->p_receive_circwindow >= 0);
if(circuit_deliver_data_cell(circ->data_queue->cell, circ, CELL_DIRECTION_OUT) < 0) {
circuit_close(circ);
return;
}
}
tmpd = circ->data_queue;
circ->data_queue = tmpd->next;
free(tmpd->cell);
free(tmpd);
if(circuit_consider_stop_edge_reading(circ, edge_type))
return;
}
if(edge_type == EDGE_EXIT)
conn = circ->n_conn;
else
conn = circ->p_conn;
for( ; conn; conn=conn->next_topic) {
if((edge_type == EDGE_EXIT && conn->n_receive_topicwindow > 0) ||
(edge_type == EDGE_AP && conn->p_receive_topicwindow > 0)) {
connection_start_reading(conn);
connection_package_raw_inbuf(conn); /* handle whatever might still be on the inbuf */
}
}
circuit_consider_stop_edge_reading(circ, edge_type);
}
/* returns 1 if the window is empty, else 0. If it's empty, tell edge conns to stop reading. */
int circuit_consider_stop_edge_reading(circuit_t *circ, int edge_type) {
connection_t *conn = NULL;
assert(edge_type == EDGE_EXIT || edge_type == EDGE_AP);
if(edge_type == EDGE_EXIT && circ->n_receive_circwindow <= 0)
conn = circ->n_conn;
else if(edge_type == EDGE_AP && circ->p_receive_circwindow <= 0)
conn = circ->p_conn;
else
return 0;
for( ; conn; conn=conn->next_topic)
connection_stop_reading(conn);
return 1;
}
int circuit_consider_sending_sendme(circuit_t *circ, int edge_type) {
cell_t sendme;
2002-06-27 00:45:49 +02:00
assert(circ);
sendme.command = CELL_SENDME;
sendme.length = CIRCWINDOW_INCREMENT;
if(edge_type == EDGE_AP) { /* i'm the AP */
while(circ->n_receive_circwindow < CIRCWINDOW_START-CIRCWINDOW_INCREMENT) {
log(LOG_INFO,"circuit_consider_sending_sendme(): n_receive_circwindow %d, Queueing sendme forward.", circ->n_receive_circwindow);
circ->n_receive_circwindow += CIRCWINDOW_INCREMENT;
sendme.aci = circ->n_aci;
if(connection_write_cell_to_buf(&sendme, circ->n_conn) < 0) {
return -1;
}
}
} else if(edge_type == EDGE_EXIT) { /* i'm the exit */
while(circ->p_receive_circwindow < CIRCWINDOW_START-CIRCWINDOW_INCREMENT) {
log(LOG_INFO,"circuit_consider_sending_sendme(): p_receive_circwindow %d, Queueing sendme back.", circ->p_receive_circwindow);
circ->p_receive_circwindow += CIRCWINDOW_INCREMENT;
sendme.aci = circ->p_aci;
if(connection_write_cell_to_buf(&sendme, circ->p_conn) < 0) {
return -1;
}
}
}
2002-06-27 00:45:49 +02:00
return 0;
}
void circuit_close(circuit_t *circ) {
connection_t *conn;
assert(circ);
2002-06-27 00:45:49 +02:00
circuit_remove(circ);
for(conn=circ->n_conn; conn; conn=conn->next_topic) {
connection_send_destroy(circ->n_aci, circ->n_conn);
}
for(conn=circ->p_conn; conn; conn=conn->next_topic) {
connection_send_destroy(circ->p_aci, circ->p_conn);
}
2002-06-27 00:45:49 +02:00
circuit_free(circ);
}
void circuit_about_to_close_connection(connection_t *conn) {
/* send destroys for all circuits using conn */
/* currently, we assume it's too late to flush conn's buf here.
* down the road, maybe we'll consider that eof doesn't mean can't-write
*/
circuit_t *circ;
connection_t *prevconn, *tmpconn;
cell_t cell;
int edge_type;
if(!connection_speaks_cells(conn)) {
/* it's an edge conn. need to remove it from the linked list of
* conn's for this circuit. Send an 'end' data topic.
* But don't kill the circuit.
*/
circ = circuit_get_by_conn(conn);
if(!circ)
return;
memset(&cell, 0, sizeof(cell_t));
cell.command = CELL_DATA;
cell.length = TOPIC_HEADER_SIZE;
*(uint16_t *)(cell.payload+2) = htons(conn->topic_id);
*cell.payload = TOPIC_COMMAND_END;
if(conn == circ->p_conn) {
circ->p_conn = conn->next_topic;
edge_type = EDGE_AP;
goto send_end;
}
if(conn == circ->n_conn) {
circ->n_conn = conn->next_topic;
edge_type = EDGE_EXIT;
goto send_end;
}
for(prevconn = circ->p_conn; prevconn->next_topic && prevconn->next_topic != conn; prevconn = prevconn->next_topic) ;
if(prevconn->next_topic) {
prevconn->next_topic = conn->next_topic;
edge_type = EDGE_AP;
goto send_end;
}
for(prevconn = circ->n_conn; prevconn->next_topic && prevconn->next_topic != conn; prevconn = prevconn->next_topic) ;
if(prevconn->next_topic) {
prevconn->next_topic = conn->next_topic;
edge_type = EDGE_EXIT;
goto send_end;
}
log(LOG_ERR,"circuit_about_to_close_connection(): edge conn not in circuit's list?");
assert(0); /* should never get here */
send_end:
if(edge_type == EDGE_AP) { /* send to circ->n_conn */
log(LOG_INFO,"circuit_about_to_close_connection(): send data end forward (aci %d).",circ->n_aci);
cell.aci = circ->n_aci;
} else { /* send to circ->p_conn */
assert(edge_type == EDGE_EXIT);
log(LOG_INFO,"circuit_about_to_close_connection(): send data end backward (aci %d).",circ->p_aci);
cell.aci = circ->p_aci;
}
if(circuit_deliver_data_cell_from_edge(&cell, circ, edge_type) < 0) {
log(LOG_DEBUG,"circuit_about_to_close_connection(): circuit_deliver_data_cell_from_edge (%d) failed. Closing.", edge_type);
circuit_close(circ);
}
return;
}
2002-06-27 00:45:49 +02:00
while((circ = circuit_get_by_conn(conn))) {
circuit_remove(circ);
if(circ->n_conn == conn) /* it's closing in front of us */
for(tmpconn=circ->p_conn; tmpconn; tmpconn=tmpconn->next_topic) {
connection_send_destroy(circ->p_aci, tmpconn);
}
2002-06-27 00:45:49 +02:00
if(circ->p_conn == conn) /* it's closing behind us */
for(tmpconn=circ->n_conn; tmpconn; tmpconn=tmpconn->next_topic) {
connection_send_destroy(circ->n_aci, tmpconn);
}
2002-06-27 00:45:49 +02:00
circuit_free(circ);
}
}
/* FIXME this now leaves some out */
void circuit_dump_by_conn(connection_t *conn) {
circuit_t *circ;
connection_t *tmpconn;
for(circ=global_circuitlist;circ;circ = circ->next) {
for(tmpconn=circ->p_conn; tmpconn; tmpconn=tmpconn->next_topic) {
if(tmpconn == conn) {
printf("Conn %d has App-ward circuit: aci %d (other side %d), state %d (%s)\n",
conn->poll_index, circ->p_aci, circ->n_aci, circ->state, circuit_state_to_string[circ->state]);
}
}
for(tmpconn=circ->n_conn; tmpconn; tmpconn=tmpconn->next_topic) {
if(tmpconn == conn) {
printf("Conn %d has Exit-ward circuit: aci %d (other side %d), state %d (%s)\n",
conn->poll_index, circ->n_aci, circ->p_aci, circ->state, circuit_state_to_string[circ->state]);
}
}
}
}