Commit 8bf0382b authored by Nick Mathewson's avatar Nick Mathewson 🎨
Browse files

Skeleton ExtORPort implementation. Needs testing, documentation.

Does not implement TransportControlPort yet.
parent f45e1fbd
......@@ -1702,6 +1702,54 @@ fetch_from_evbuffer_socks(struct evbuffer *buf, socks_request_t *req,
}
#endif
/*DOCDOC*/
#define EXT_OR_CMD_HEADER_SIZE 4
/*DOCDOC*/
int
fetch_ext_or_command_from_buf(buf_t *buf, ext_or_cmd_t **out)
{
char hdr[EXT_OR_CMD_HEADER_SIZE];
uint16_t len;
check();
if (buf->datalen < EXT_OR_CMD_HEADER_SIZE)
return 0;
peek_from_buf(hdr, sizeof(hdr), buf);
len = ntohs(get_uint16(hdr+2));
if (buf->datalen < (unsigned)len + EXT_OR_CMD_HEADER_SIZE)
return 0;
*out = ext_or_cmd_new(len);
(*out)->cmd = ntohs(get_uint16(hdr));
(*out)->len = len;
buf_remove_from_front(buf, EXT_OR_CMD_HEADER_SIZE);
fetch_from_buf((*out)->body, len, buf);
return 1;
}
#ifdef USE_BUFFEREVENTS
/*DOCDOC*/
int
fetch_ext_or_command_from_evbuffer(struct evbuffer *buf, ext_or_cmd_t **out)
{
char hdr[EXT_OR_CMD_HEADER_SIZE];
uint16_t len;
size_t buf_len = evbuffer_get_length(buf);
if (buf_len < EXT_OR_CMD_HEADER_SIZE)
return 0;
evbuffer_copyout(buf, hdr, EXT_OR_CMD_HEADER_SIZE);
len = ntohs(get_uint16(hdr+2));
if (buf_len < (unsigned)len + EXT_OR_CMD_HEADER_SIZE)
return 0;
*out = ext_or_cmd_new(len);
(*out)->cmd = ntohs(get_uint16(hdr));
(*out)->len = len;
evbuffer_drain(buf, EXT_OR_CMD_HEADER_SIZE);
evbuffer_remove(buf, (*out)->body, len);
return 1;
}
#endif
/** Implementation helper to implement fetch_from_*_socks. Instead of looking
* at a buffer's contents, we look at the <b>datalen</b> bytes of data in
* <b>data</b>. Instead of removing data from the buffer, we set
......
......@@ -53,6 +53,8 @@ int fetch_from_buf_line(buf_t *buf, char *data_out, size_t *data_len);
int peek_buf_has_control0_command(buf_t *buf);
int fetch_ext_or_command_from_buf(buf_t *buf, ext_or_cmd_t **out);
#ifdef USE_BUFFEREVENTS
int fetch_var_cell_from_evbuffer(struct evbuffer *buf, var_cell_t **out,
int linkproto);
......@@ -68,6 +70,8 @@ int peek_evbuffer_has_control0_command(struct evbuffer *buf);
int write_to_evbuffer_zlib(struct evbuffer *buf, tor_zlib_state_t *state,
const char *data, size_t data_len,
int done);
int fetch_ext_or_command_from_evbuffer(struct evbuffer *buf,
ext_or_cmd_t **out);
#endif
#ifdef USE_BUFFEREVENTS
......
......@@ -230,6 +230,7 @@ static config_var_t option_vars_[] = {
V(ExitPolicyRejectPrivate, BOOL, "1"),
V(ExitPortStatistics, BOOL, "0"),
V(ExtendAllowPrivateAddresses, BOOL, "0"),
VPORT(ExtORPort, LINELIST, NULL),
V(ExtraInfoStatistics, BOOL, "1"),
V(FallbackDir, LINELIST, NULL),
......@@ -5687,6 +5688,14 @@ parse_ports(or_options_t *options, int validate_only,
*msg = tor_strdup("Invalid ORPort/ORListenAddress configuration");
goto err;
}
if (parse_port_config(ports,
options->ExtORPort_lines, NULL,
"ExtOR", CONN_TYPE_EXT_OR_LISTENER,
"127.0.0.1", 0,
CL_PORT_SERVER_OPTIONS) < 0) {
*msg = tor_strdup("Invalid ExtORPort configuration");
goto err;
}
if (parse_port_config(ports,
options->DirPort_lines, options->DirListenAddress,
"Dir", CONN_TYPE_DIR_LISTENER,
......
......@@ -98,6 +98,7 @@ static smartlist_t *outgoing_addrs = NULL;
#define CASE_ANY_LISTENER_TYPE \
case CONN_TYPE_OR_LISTENER: \
case CONN_TYPE_EXT_OR_LISTENER: \
case CONN_TYPE_AP_LISTENER: \
case CONN_TYPE_DIR_LISTENER: \
case CONN_TYPE_CONTROL_LISTENER: \
......@@ -129,6 +130,8 @@ conn_type_to_string(int type)
case CONN_TYPE_CPUWORKER: return "CPU worker";
case CONN_TYPE_CONTROL_LISTENER: return "Control listener";
case CONN_TYPE_CONTROL: return "Control";
case CONN_TYPE_EXT_OR: return "Extended OR";
case CONN_TYPE_EXT_OR_LISTENER: return "Extended OR listener";
default:
log_warn(LD_BUG, "unknown connection type %d", type);
tor_snprintf(buf, sizeof(buf), "unknown [%d]", type);
......@@ -165,6 +168,12 @@ conn_state_to_string(int type, int state)
case OR_CONN_STATE_OPEN: return "open";
}
break;
case CONN_TYPE_EXT_OR:
switch (state) {
case EXT_OR_CONN_STATE_OPEN: return "open";
case EXT_OR_CONN_STATE_FLUSHING: return "flushing final OKAY";
}
break;
case CONN_TYPE_EXIT:
switch (state) {
case EXIT_CONN_STATE_RESOLVING: return "waiting for dest info";
......@@ -229,6 +238,7 @@ connection_type_uses_bufferevent(connection_t *conn)
case CONN_TYPE_DIR:
case CONN_TYPE_CONTROL:
case CONN_TYPE_OR:
case CONN_TYPE_EXT_OR:
case CONN_TYPE_CPUWORKER:
return 1;
default:
......@@ -259,14 +269,18 @@ dir_connection_new(int socket_family)
* Set active_circuit_pqueue_last_recalibrated to current cell_ewma tick.
*/
or_connection_t *
or_connection_new(int socket_family)
or_connection_new(int type, int socket_family)
{
or_connection_t *or_conn = tor_malloc_zero(sizeof(or_connection_t));
time_t now = time(NULL);
connection_init(now, TO_CONN(or_conn), CONN_TYPE_OR, socket_family);
tor_assert(type == CONN_TYPE_OR || type == CONN_TYPE_EXT_OR);
connection_init(now, TO_CONN(or_conn), type, socket_family);
or_conn->timestamp_last_added_nonpadding = time(NULL);
if (type == CONN_TYPE_EXT_OR)
connection_or_set_ext_or_identifier(or_conn);
return or_conn;
}
......@@ -335,7 +349,8 @@ connection_new(int type, int socket_family)
{
switch (type) {
case CONN_TYPE_OR:
return TO_CONN(or_connection_new(socket_family));
case CONN_TYPE_EXT_OR:
return TO_CONN(or_connection_new(type, socket_family));
case CONN_TYPE_EXIT:
return TO_CONN(edge_connection_new(type, socket_family));
......@@ -377,6 +392,7 @@ connection_init(time_t now, connection_t *conn, int type, int socket_family)
switch (type) {
case CONN_TYPE_OR:
case CONN_TYPE_EXT_OR:
conn->magic = OR_CONNECTION_MAGIC;
break;
case CONN_TYPE_EXIT:
......@@ -445,6 +461,7 @@ connection_free_(connection_t *conn)
switch (conn->type) {
case CONN_TYPE_OR:
case CONN_TYPE_EXT_OR:
tor_assert(conn->magic == OR_CONNECTION_MAGIC);
mem = TO_OR_CONN(conn);
memlen = sizeof(or_connection_t);
......@@ -575,6 +592,9 @@ connection_free_(connection_t *conn)
log_warn(LD_BUG, "called on OR conn with non-zeroed identity_digest");
connection_or_remove_from_identity_map(TO_OR_CONN(conn));
}
if (conn->type == CONN_TYPE_OR || conn->type == CONN_TYPE_EXT_OR)
connection_or_remove_from_ext_or_id_map(TO_OR_CONN(conn));
#ifdef USE_BUFFEREVENTS
if (conn->type == CONN_TYPE_OR && TO_OR_CONN(conn)->bucket_cfg) {
ev_token_bucket_cfg_free(TO_OR_CONN(conn)->bucket_cfg);
......@@ -638,6 +658,7 @@ connection_about_to_close_connection(connection_t *conn)
connection_dir_about_to_close(TO_DIR_CONN(conn));
break;
case CONN_TYPE_OR:
case CONN_TYPE_EXT_OR:
connection_or_about_to_close(TO_OR_CONN(conn));
break;
case CONN_TYPE_AP:
......@@ -1374,6 +1395,9 @@ connection_init_accepted_conn(connection_t *conn,
connection_start_reading(conn);
switch (conn->type) {
case CONN_TYPE_EXT_OR:
conn->state = EXT_OR_CONN_STATE_OPEN;
break;
case CONN_TYPE_OR:
control_event_or_conn_status(TO_OR_CONN(conn), OR_CONN_EVENT_NEW, 0);
rv = connection_tls_start_handshake(TO_OR_CONN(conn), 1);
......@@ -2886,6 +2910,8 @@ connection_handle_read_impl(connection_t *conn)
switch (conn->type) {
case CONN_TYPE_OR_LISTENER:
return connection_handle_listener_read(conn, CONN_TYPE_OR);
case CONN_TYPE_EXT_OR_LISTENER:
return connection_handle_listener_read(conn, CONN_TYPE_EXT_OR);
case CONN_TYPE_AP_LISTENER:
case CONN_TYPE_AP_TRANS_LISTENER:
case CONN_TYPE_AP_NATD_LISTENER:
......@@ -3918,6 +3944,7 @@ int
connection_is_listener(connection_t *conn)
{
if (conn->type == CONN_TYPE_OR_LISTENER ||
conn->type == CONN_TYPE_EXT_OR_LISTENER ||
conn->type == CONN_TYPE_AP_LISTENER ||
conn->type == CONN_TYPE_AP_TRANS_LISTENER ||
conn->type == CONN_TYPE_AP_DNS_LISTENER ||
......@@ -3940,6 +3967,7 @@ connection_state_is_open(connection_t *conn)
return 0;
if ((conn->type == CONN_TYPE_OR && conn->state == OR_CONN_STATE_OPEN) ||
(conn->type == CONN_TYPE_EXT_OR) ||
(conn->type == CONN_TYPE_AP && conn->state == AP_CONN_STATE_OPEN) ||
(conn->type == CONN_TYPE_EXIT && conn->state == EXIT_CONN_STATE_OPEN) ||
(conn->type == CONN_TYPE_CONTROL &&
......@@ -4109,6 +4137,8 @@ connection_process_inbuf(connection_t *conn, int package_partial)
switch (conn->type) {
case CONN_TYPE_OR:
return connection_or_process_inbuf(TO_OR_CONN(conn));
case CONN_TYPE_EXT_OR:
return connection_ext_or_process_inbuf(TO_OR_CONN(conn));
case CONN_TYPE_EXIT:
case CONN_TYPE_AP:
return connection_edge_process_inbuf(TO_EDGE_CONN(conn),
......@@ -4169,6 +4199,8 @@ connection_finished_flushing(connection_t *conn)
switch (conn->type) {
case CONN_TYPE_OR:
return connection_or_finished_flushing(TO_OR_CONN(conn));
case CONN_TYPE_EXT_OR:
return connection_ext_or_finished_flushing(TO_OR_CONN(conn));
case CONN_TYPE_AP:
case CONN_TYPE_EXIT:
return connection_edge_finished_flushing(TO_EDGE_CONN(conn));
......@@ -4224,6 +4256,7 @@ connection_reached_eof(connection_t *conn)
{
switch (conn->type) {
case CONN_TYPE_OR:
case CONN_TYPE_EXT_OR:
return connection_or_reached_eof(TO_OR_CONN(conn));
case CONN_TYPE_AP:
case CONN_TYPE_EXIT:
......
......@@ -19,7 +19,7 @@ const char *conn_type_to_string(int type);
const char *conn_state_to_string(int type, int state);
dir_connection_t *dir_connection_new(int socket_family);
or_connection_t *or_connection_new(int socket_family);
or_connection_t *or_connection_new(int type, int socket_family);
edge_connection_t *edge_connection_new(int type, int socket_family);
entry_connection_t *entry_connection_new(int type, int socket_family);
control_connection_t *control_connection_new(int socket_family);
......
......@@ -75,6 +75,9 @@ static void connection_or_handle_event_cb(struct bufferevent *bufev,
* they form a linked list, with next_with_same_id as the next pointer. */
static digestmap_t *orconn_identity_map = NULL;
/**DOCDOC */
static digestmap_t *orconn_ext_or_id_map = NULL;
/** If conn is listed in orconn_identity_map, remove it, and clear
* conn->identity_digest. Otherwise do nothing. */
void
......@@ -174,6 +177,52 @@ connection_or_set_identity_digest(or_connection_t *conn, const char *digest)
#endif
}
void
connection_or_remove_from_ext_or_id_map(or_connection_t *conn)
{
or_connection_t *tmp;
if (!orconn_identity_map)
orconn_identity_map = digestmap_new();
tmp = digestmap_remove(orconn_ext_or_id_map, conn->ext_or_conn_id);
if (!tor_digest_is_zero(conn->ext_or_conn_id))
tor_assert(tmp == conn);
memset(conn->ext_or_conn_id, 0, EXT_OR_CONN_ID_LEN);
}
/*DOCDOC*/
void
connection_or_clear_ext_or_id_map(void)
{
digestmap_free(orconn_ext_or_id_map, NULL);
}
/*DOCDOC
sets it to a random value */
void
connection_or_set_ext_or_identifier(or_connection_t *conn)
{
char random_id[EXT_OR_CONN_ID_LEN];
or_connection_t *tmp;
if (!orconn_ext_or_id_map)
orconn_ext_or_id_map = digestmap_new();
if (!tor_digest_is_zero(conn->ext_or_conn_id))
connection_or_remove_from_ext_or_id_map(conn);
do {
crypto_rand(random_id, sizeof(random_id));
} while (digestmap_get(orconn_ext_or_id_map, random_id));
memcpy(conn->ext_or_conn_id, random_id, EXT_OR_CONN_ID_LEN);
tmp = digestmap_set(orconn_ext_or_id_map, random_id, conn);
tor_assert(!tmp);
}
/**************************************************************/
/** Map from a string describing what a non-open OR connection was doing when
......@@ -228,7 +277,7 @@ connection_or_get_state_description(or_connection_t *orconn,
const char *conn_state;
char tls_state[256];
tor_assert(conn->type == CONN_TYPE_OR);
tor_assert(conn->type == CONN_TYPE_OR || conn->type == CONN_TYPE_EXT_OR);
conn_state = conn_state_to_string(conn->type, conn->state);
tor_tls_get_state_description(orconn->tls, tls_state, sizeof(tls_state));
......@@ -423,6 +472,23 @@ var_cell_free(var_cell_t *cell)
tor_free(cell);
}
/*DOCDOC*/
ext_or_cmd_t *
ext_or_cmd_new(uint16_t len)
{
size_t size = STRUCT_OFFSET(ext_or_cmd_t, body) + len;
ext_or_cmd_t *cmd = tor_malloc(size);
cmd->len = len;
return cmd;
}
/*DOCDOC*/
void
ext_or_cmd_free(ext_or_cmd_t *cmd)
{
tor_free(cmd);
}
/** We've received an EOF from <b>conn</b>. Mark it for close and return. */
int
connection_or_reached_eof(or_connection_t *conn)
......@@ -1077,7 +1143,7 @@ connection_or_connect(const tor_addr_t *_addr, uint16_t port,
return NULL;
}
conn = or_connection_new(tor_addr_family(&addr));
conn = or_connection_new(CONN_TYPE_OR, tor_addr_family(&addr));
/*
* Set up conn so it's got all the data we need to remember for channels
......@@ -1470,7 +1536,8 @@ connection_or_handle_event_cb(struct bufferevent *bufev, short event,
int
connection_or_nonopen_was_started_here(or_connection_t *conn)
{
tor_assert(conn->base_.type == CONN_TYPE_OR);
tor_assert(conn->base_.type == CONN_TYPE_OR ||
conn->base_.type == CONN_TYPE_EXT_OR);
if (!conn->tls)
return 1; /* it's still in proxy states or something */
if (conn->handshake_state)
......@@ -2365,3 +2432,132 @@ connection_or_send_authenticate_cell(or_connection_t *conn, int authtype)
return 0;
}
/*DOCDOC*/
static int
connection_fetch_ext_or_cmd_from_buf(connection_t *conn, ext_or_cmd_t **out)
{
IF_HAS_BUFFEREVENT(conn, {
struct evbuffer *input = bufferevent_get_input(conn->bufev);
return fetch_ext_or_command_from_evbuffer(input, out);
}) ELSE_IF_NO_BUFFEREVENT {
return fetch_ext_or_command_from_buf(conn->inbuf, out);
}
}
/*DOCDOC*/
static int
connection_write_ext_or_command(connection_t *conn,
uint16_t command,
const char *body,
size_t bodylen)
{
char header[4];
if (bodylen > UINT16_MAX)
return -1;
set_uint16(header, htons(command));
set_uint16(header+2, htons(bodylen));
connection_write_to_buf(header, 4, conn);
if (bodylen) {
tor_assert(body);
connection_write_to_buf(body, bodylen, conn);
}
return 0;
}
/*DOCDOC*/
static void
connection_ext_or_transition(or_connection_t *conn)
{
tor_assert(conn->base_.type == CONN_TYPE_EXT_OR);
conn->base_.type = CONN_TYPE_OR;
control_event_or_conn_status(conn, OR_CONN_EVENT_NEW, 0);
connection_tls_start_handshake(conn, 1);
}
/*XXXX make these match the spec .*/
#define EXT_OR_CMD_DONE 0x0001
#define EXT_OR_CMD_USERADDR 0x0002
#define EXT_OR_CMD_WANT_CONTROL 0x0003
#define EXT_OR_CMD_OKAY 0x1001
/*DOCDOC*/
int
connection_ext_or_process_inbuf(or_connection_t *or_conn)
{
connection_t *conn = TO_CONN(or_conn);
ext_or_cmd_t *command;
int r;
while (1) {
command = NULL;
r = connection_fetch_ext_or_cmd_from_buf(conn, &command);
if (r < 0)
return -1;
else if (r == 0)
return 0; /* need to wait for more data */
/* Got a command! */
tor_assert(command);
if (command->cmd == EXT_OR_CMD_DONE) {
if (connection_get_inbuf_len(conn)) {
/* The inbuf isn't empty; the client is misbehaving. */
goto err;
}
connection_write_ext_or_command(conn, EXT_OR_CMD_OKAY, NULL, 0);
/* can't transition immediately; need to flush first. */
conn->state = EXT_OR_CONN_STATE_FLUSHING;
connection_stop_reading(conn);
} else if (command->cmd == EXT_OR_CMD_USERADDR) {
/* Copy address string. */
tor_addr_t addr;
uint16_t port;
char *addr_str;
char *address_part=NULL;
int res;
addr_str = tor_malloc(command->len + 1);
memcpy(addr_str, command->body, command->len);
addr_str[command->len] = 0;
res = tor_addr_port_split(LOG_INFO, addr_str, &address_part, &port);
tor_free(addr_str);
if (res<0)
goto err;
res = tor_addr_parse(&addr, address_part);
tor_free(address_part);
if (res<0)
goto err;
/* record the address */
tor_addr_copy(&conn->addr, &addr);
conn->port = port;
} else if (command->cmd == EXT_OR_CMD_WANT_CONTROL) {
char response[128];
char *cp;
memcpy(response, or_conn->ext_or_conn_id, EXT_OR_CONN_ID_LEN);
cp = response+EXT_OR_CONN_ID_LEN;
/* XXXX write the TransportControlPort; advance cp. */
connection_write_ext_or_command(conn, EXT_OR_CMD_OKAY, response,
cp-response);
}
ext_or_cmd_free(command);
}
err:
ext_or_cmd_free(command);
return -1;
}
int
connection_ext_or_finished_flushing(or_connection_t *conn)
{
if (conn->base_.state == EXT_OR_CONN_STATE_FLUSHING) {
connection_start_reading(TO_CONN(conn));
connection_ext_or_transition(conn);
}
return 0;
}
......@@ -95,5 +95,14 @@ void var_cell_free(var_cell_t *cell);
/** DOCDOC */
#define MIN_LINK_PROTO_FOR_WIDE_CIRC_IDS 4
ext_or_cmd_t *ext_or_cmd_new(uint16_t len);
void ext_or_cmd_free(ext_or_cmd_t *cmd);
void connection_or_set_ext_or_identifier(or_connection_t *conn);
void connection_or_remove_from_ext_or_id_map(or_connection_t *conn);
void connection_or_clear_ext_or_id_map(void);
int connection_ext_or_finished_flushing(or_connection_t *conn);
int connection_ext_or_process_inbuf(or_connection_t *or_conn);
#endif
......@@ -228,8 +228,13 @@ typedef enum {
#define CONN_TYPE_AP_NATD_LISTENER 14
/** Type for sockets listening for DNS requests. */
#define CONN_TYPE_AP_DNS_LISTENER 15
#define CONN_TYPE_MAX_ 15
/* !!!! If CONN_TYPE_MAX_ is ever over 15, we must grow the type field in
/** DOCDOC */
#define CONN_TYPE_EXT_OR 16
#define CONN_TYPE_EXT_OR_LISTENER 17
#define CONN_TYPE_MAX_ 17
/* !!!! If _CONN_TYPE_MAX is ever over 31, we must grow the type field in
* connection_t. */
/* Proxy client types */
......@@ -309,6 +314,12 @@ typedef enum {
#define OR_CONN_STATE_OPEN 8
#define OR_CONN_STATE_MAX_ 8
/*DOCDOC*/
#define _EXT_OR_CONN_STATE_MIN 1
#define EXT_OR_CONN_STATE_OPEN 1
#define EXT_OR_CONN_STATE_FLUSHING 2
#define _EXT_OR_CONN_STATE_MAX 2
#define EXIT_CONN_STATE_MIN_ 1
/** State for an exit connection: waiting for response from DNS farm. */
#define EXIT_CONN_STATE_RESOLVING 1
......@@ -1082,6 +1093,13 @@ typedef struct var_cell_t {
uint8_t payload[FLEXIBLE_ARRAY_MEMBER];
} var_cell_t;
/* DOCDOC */
typedef struct ext_or_cmd_t {
uint16_t cmd;
uint16_t len;
char body[FLEXIBLE_ARRAY_MEMBER];
} ext_or_cmd_t;
/** A cell as packed for writing to the network. */
typedef struct packed_cell_t {
/** Next cell queued on this circuit. */
......@@ -1163,7 +1181,7 @@ typedef struct connection_t {
* *_CONNECTION_MAGIC. */
uint8_t state; /**< Current state of this connection. */
unsigned int type:4; /**< What kind of connection is this? */
unsigned int type:5; /**< What kind of connection is this? */
unsigned int purpose:5; /**< Only used for DIR and EXIT types currently. */
/* The next fields are all one-bit booleans. Some are only applicable to
......@@ -1405,6 +1423,9 @@ typedef struct or_handshake_state_t {
/**@}*/
} or_handshake_state_t;
/* DOCDOC */
#define EXT_OR_CONN_ID_LEN 20
/** Subtype of connection_t for an "OR connection" -- that is, one that speaks
* cells over TLS. */
typedef struct or_connection_t {
......@@ -1413,6 +1434,8 @@ typedef struct or_connection_t {
/** Hash of the public RSA key for the other side's identity key, or zeroes
* if the other side hasn't shown us a valid identity key. */
char identity_digest[DIGEST_LEN];
/*DOCDOC*/
char ext_or_conn_id[EXT_OR_CONN_ID_LEN];
char *nickname; /**< Nickname of OR on other side (if any). */
tor_tls_t *tls; /**< TLS connection state. */
......@@ -3428,6 +3451,8 @@ typedef struct {
char *User; /**< Name of user to run Tor as. */
char *Group; /**< Name of group to run Tor as. */
config_line_t *ORPort_lines; /**< Ports to listen on for OR connections. */
/** Ports to listen on for extended OR connections. */
config_line_t *ExtORPort_lines;
/** Ports to listen on for SOCKS connections. */
config_line_t *SocksPort_lines;
/** Ports to listen on for transparent pf/netfilter connections. */
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment