Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
M
mongoose
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
esp
mongoose
Commits
900bbe72
Commit
900bbe72
authored
8 years ago
by
Marko Mikulicic
Committed by
Cesanta Bot
8 years ago
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Mongoose forwarding
PUBLISHED_FROM=51652f0157bb951a43508f0fe948c62c351e96ba
parent
f5a28576
master
6.7.1
dev
6.11
6.10
6.9
6.8
6.7
No related merge requests found
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
327 additions
and
35 deletions
+327
-35
intro.md
docs/c-api/http_server.h/intro.md
+1
-0
mg_http_send_error.md
docs/c-api/http_server.h/mg_http_send_error.md
+11
-0
struct_mg_serve_http_opts.md
docs/c-api/http_server.h/struct_mg_serve_http_opts.md
+8
-4
Makefile
examples/reverse_proxy/Makefile
+4
-0
hello.html
examples/reverse_proxy/frontend/hello.html
+6
-0
reverse_proxy.c
examples/reverse_proxy/reverse_proxy.c
+127
-0
mongoose.c
mongoose.c
+150
-27
mongoose.h
mongoose.h
+20
-4
No files found.
docs/c-api/http_server.h/intro.md
View file @
900bbe72
...
...
@@ -8,6 +8,7 @@ items:
-
{
name
:
mg_get_http_var.md
}
-
{
name
:
mg_http_check_digest_auth.md
}
-
{
name
:
mg_http_parse_header.md
}
-
{
name
:
mg_http_send_error.md
}
-
{
name
:
mg_http_send_redirect.md
}
-
{
name
:
mg_http_serve_file.md
}
-
{
name
:
mg_parse_http.md
}
...
...
This diff is collapsed.
Click to expand it.
docs/c-api/http_server.h/mg_http_send_error.md
0 → 100644
View file @
900bbe72
---
title
:
"
mg_http_send_error()"
decl_name
:
"
mg_http_send_error"
symbol_kind
:
"
func"
signature
:
|
void mg_http_send_error(struct mg_connection *nc, int code, const char *reason);
---
Sends an error response. If reason is NULL, the message will be inferred
from the error code (if supported).
This diff is collapsed.
Click to expand it.
docs/c-api/http_server.h/struct_mg_serve_http_opts.md
View file @
900bbe72
...
...
@@ -100,16 +100,19 @@ signature: |
/* IP ACL. By default, NULL, meaning all IPs are allowed to connect */
const char *ip_acl;
#if MG_ENABLE_HTTP_URL_REWRITES
/* URL rewrites.
*
* Comma-separated list of `uri_pattern=file_or_directory_path` rewrites.
* Comma-separated list of `uri_pattern=
url_
file_or_directory_path` rewrites.
* When HTTP request is received, Mongoose constructs a file name from the
* requested URI by combining `document_root` and the URI. However, if the
* rewrite option is used and `uri_pattern` matches requested URI, then
* `document_root` is ignored. Instead, `file_or_directory_path` is used,
* `document_root` is ignored. Instead, `
url_
file_or_directory_path` is used,
* which should be a full path name or a path relative to the web server's
* current working directory. Note that `uri_pattern`, as all Mongoose
* patterns, is a prefix pattern.
* current working directory. It can also be an URI (http:// or https://)
* in which case mongoose will behave as a reverse proxy for that destination.
*
* Note that `uri_pattern`, as all Mongoose patterns, is a prefix pattern.
*
* If uri_pattern starts with `@` symbol, then Mongoose compares it with the
* HOST header of the request. If they are equal, Mongoose sets document root
...
...
@@ -123,6 +126,7 @@ signature: |
* automatically appended to the redirect location.
*/
const char *url_rewrites;
#endif
/* DAV document root. If NULL, DAV requests are going to fail. */
const char *dav_document_root;
...
...
This diff is collapsed.
Click to expand it.
examples/reverse_proxy/Makefile
0 → 100644
View file @
900bbe72
PROG
=
reverse_proxy
MODULE_CFLAGS
=
SSL_LIB
=
include
../examples.mk
This diff is collapsed.
Click to expand it.
examples/reverse_proxy/frontend/hello.html
0 → 100644
View file @
900bbe72
<!DOCTYPE html>
<html>
<body>
Hello
</body>
</html>
This diff is collapsed.
Click to expand it.
examples/reverse_proxy/reverse_proxy.c
0 → 100644
View file @
900bbe72
/*
* Copyright (c) 2014 Cesanta Software Limited
* All rights reserved
*/
/*
* This example shows how mongoose can be used as a reverse
* proxy for another http server.
*
* A common setup is to have a frontend web server that delegates
* some urls to a backend web server.
*
* In this example we create two webservers. The frontend listens on port
* 8000 and servers a static file and forwards any call matching the /api prefix
* to the backend.
*
* The backend listens on port 8001 and replies a simple JSON object which
* shows the request URI that the backend http server receives.
*
* Things to try out:
*
* curl http://localhost:8000/
* curl http://localhost:8000/api
* curl http://localhost:8000/api/foo
* curl http://localhost:8001/foo
*
* The reverse proxy functionality is enabled via the url rewrite functionality:
*
* ```
* s_frontend_server_opts.url_rewrites =
* "/api=http://localhost:8001,/=frontend/hello.html";
* ```
*
* This example maps the /api to a remote http server, and / to a
* specific file on the filesystem.
*
* Obviously you can use any http server as the backend, we spawn
* another web server from the same process in order to make the example easy
* to run.
*/
#include "../../mongoose.h"
static
const
char
*
s_frontend_port
=
"8000"
;
static
struct
mg_serve_http_opts
s_frontend_server_opts
;
static
const
char
*
s_backend_port
=
"8001"
;
static
struct
mg_serve_http_opts
s_backend_server_opts
;
static
void
frontend_handler
(
struct
mg_connection
*
nc
,
int
ev
,
void
*
ev_data
)
{
struct
http_message
*
hm
=
(
struct
http_message
*
)
ev_data
;
switch
(
ev
)
{
case
MG_EV_HTTP_REQUEST
:
mg_serve_http
(
nc
,
hm
,
s_frontend_server_opts
);
/* Serve static content */
break
;
default:
break
;
}
}
static
void
backend_handler
(
struct
mg_connection
*
nc
,
int
ev
,
void
*
ev_data
)
{
struct
http_message
*
hm
=
(
struct
http_message
*
)
ev_data
;
int
i
;
switch
(
ev
)
{
case
MG_EV_HTTP_REQUEST
:
mg_send_response_line
(
nc
,
200
,
"Content-Type: text/html
\r\n
"
"Connection: close
\r\n
"
);
mg_printf
(
nc
,
"{
\"
uri
\"
:
\"
%.*s
\"
,
\"
method
\"
:
\"
%.*s
\"
,
\"
body
\"
:
\"
%.*s
\"
, "
"
\"
headers
\"
: {"
,
(
int
)
hm
->
uri
.
len
,
hm
->
uri
.
p
,
(
int
)
hm
->
method
.
len
,
hm
->
method
.
p
,
(
int
)
hm
->
body
.
len
,
hm
->
body
.
p
);
for
(
i
=
0
;
i
<
MG_MAX_HTTP_HEADERS
&&
hm
->
header_names
[
i
].
len
>
0
;
i
++
)
{
struct
mg_str
hn
=
hm
->
header_names
[
i
];
struct
mg_str
hv
=
hm
->
header_values
[
i
];
mg_printf
(
nc
,
"%s
\"
%.*s
\"
:
\"
%.*s
\"
"
,
(
i
!=
0
?
","
:
""
),
(
int
)
hn
.
len
,
hn
.
p
,
(
int
)
hv
.
len
,
hv
.
p
);
}
mg_printf
(
nc
,
"}}"
);
nc
->
flags
|=
MG_F_SEND_AND_CLOSE
;
break
;
default:
break
;
}
}
int
main
(
int
argc
,
char
*
argv
[])
{
struct
mg_mgr
mgr
;
struct
mg_connection
*
nc
;
int
i
;
/* Open listening socket */
mg_mgr_init
(
&
mgr
,
NULL
);
/* configure frontend web server */
nc
=
mg_bind
(
&
mgr
,
s_frontend_port
,
frontend_handler
);
mg_set_protocol_http_websocket
(
nc
);
s_frontend_server_opts
.
document_root
=
"frontend"
;
s_frontend_server_opts
.
url_rewrites
=
"/api=http://localhost:8001,/=frontend/hello.html"
;
/* configure backend web server */
nc
=
mg_bind
(
&
mgr
,
s_backend_port
,
backend_handler
);
mg_set_protocol_http_websocket
(
nc
);
s_backend_server_opts
.
document_root
=
"backend"
;
/* Parse command line arguments */
for
(
i
=
1
;
i
<
argc
;
i
++
)
{
if
(
strcmp
(
argv
[
i
],
"-D"
)
==
0
)
{
mgr
.
hexdump_file
=
argv
[
++
i
];
}
else
if
(
strcmp
(
argv
[
i
],
"-r"
)
==
0
)
{
s_frontend_server_opts
.
document_root
=
argv
[
++
i
];
}
}
printf
(
"Starting web server on port %s
\n
"
,
s_frontend_port
);
for
(;;)
{
mg_mgr_poll
(
&
mgr
,
1000
);
}
}
This diff is collapsed.
Click to expand it.
mongoose.c
View file @
900bbe72
...
...
@@ -1181,6 +1181,19 @@ int mg_strcmp(const struct mg_str str1, const struct mg_str str2) {
if
(
i
<
str2
.
len
)
return
-
1
;
return
0
;
}
int
mg_strncmp
(
const
struct
mg_str
str1
,
const
struct
mg_str
str2
,
size_t
n
)
{
struct
mg_str
s1
=
str1
;
struct
mg_str
s2
=
str2
;
if
(
s1
.
len
>
n
)
{
s1
.
len
=
n
;
}
if
(
s2
.
len
>
n
)
{
s2
.
len
=
n
;
}
return
mg_strcmp
(
s1
,
s2
);
}
#ifdef MG_MODULE_LINES
#line 1 "common/sha1.c"
#endif
...
...
@@ -3970,6 +3983,10 @@ struct mg_http_proto_data {
};
static
void
mg_http_conn_destructor
(
void
*
proto_data
);
struct
mg_connection
*
mg_connect_http_base
(
struct
mg_mgr
*
mgr
,
mg_event_handler_t
ev_handler
,
struct
mg_connect_opts
opts
,
const
char
*
schema
,
const
char
*
schema_ssl
,
const
char
*
url
,
const
char
**
path
,
char
**
addr
);
static
struct
mg_http_proto_data
*
mg_http_get_proto_data
(
struct
mg_connection
*
c
)
{
...
...
@@ -5039,40 +5056,37 @@ void mg_set_protocol_http_websocket(struct mg_connection *nc) {
nc
->
proto_handler
=
mg_http_handler
;
}
void
mg_send_response_line_s
(
struct
mg_connection
*
nc
,
int
status_code
,
const
struct
mg_str
extra_headers
)
{
const
char
*
status_message
=
"OK"
;
const
char
*
mg_status_message
(
int
status_code
)
{
switch
(
status_code
)
{
case
206
:
status_message
=
"Partial Content"
;
break
;
return
"Partial Content"
;
case
301
:
status_message
=
"Moved"
;
break
;
return
"Moved"
;
case
302
:
status_message
=
"Found"
;
break
;
return
"Found"
;
case
401
:
status_message
=
"Unauthorized"
;
break
;
return
"Unauthorized"
;
case
403
:
status_message
=
"Forbidden"
;
break
;
return
"Forbidden"
;
case
404
:
status_message
=
"Not Found"
;
break
;
return
"Not Found"
;
case
416
:
status_message
=
"Requested range not satisfiable"
;
break
;
return
"Requested range not satisfiable"
;
case
418
:
status_message
=
"I'm a teapot"
;
break
;
return
"I'm a teapot"
;
case
500
:
status_message
=
"Internal Server Error"
;
break
;
return
"Internal Server Error"
;
case
502
:
return
"Bad Gateway"
;
default:
return
"OK"
;
}
mg_printf
(
nc
,
"HTTP/1.1 %d %s
\r\n
Server: %s
\r\n
"
,
status_code
,
status_message
,
mg_version_header
);
}
void
mg_send_response_line_s
(
struct
mg_connection
*
nc
,
int
status_code
,
const
struct
mg_str
extra_headers
)
{
mg_printf
(
nc
,
"HTTP/1.1 %d %s
\r\n
Server: %s
\r\n
"
,
status_code
,
mg_status_message
(
status_code
),
mg_version_header
);
if
(
extra_headers
.
len
>
0
)
{
mg_printf
(
nc
,
"%.*s
\r\n
"
,
(
int
)
extra_headers
.
len
,
extra_headers
.
p
);
}
...
...
@@ -5116,10 +5130,9 @@ void mg_send_head(struct mg_connection *c, int status_code,
mg_send
(
c
,
"
\r\n
"
,
2
);
}
#if MG_ENABLE_FILESYSTEM
static
void
mg_http_send_error
(
struct
mg_connection
*
nc
,
int
code
,
const
char
*
reason
)
{
if
(
!
reason
)
reason
=
""
;
void
mg_http_send_error
(
struct
mg_connection
*
nc
,
int
code
,
const
char
*
reason
)
{
if
(
!
reason
)
reason
=
mg_status_message
(
code
);
DBG
((
"%p %d %s"
,
nc
,
code
,
reason
));
mg_send_head
(
nc
,
code
,
strlen
(
reason
),
"Content-Type: text/plain
\r\n
Connection: close"
);
...
...
@@ -5127,6 +5140,7 @@ static void mg_http_send_error(struct mg_connection *nc, int code,
nc
->
flags
|=
MG_F_SEND_AND_CLOSE
;
}
#if MG_ENABLE_FILESYSTEM
static
void
mg_http_construct_etag
(
char
*
buf
,
size_t
buf_len
,
const
cs_stat_t
*
st
)
{
snprintf
(
buf
,
buf_len
,
"
\"
%lx.%"
INT64_FMT
"
\"
"
,
(
unsigned
long
)
st
->
st_mtime
,
...
...
@@ -5784,6 +5798,7 @@ MG_INTERNAL void mg_find_index_file(const char *path, const char *list,
DBG
((
"[%s] [%s]"
,
path
,
(
*
index_file
?
*
index_file
:
""
)));
}
#if MG_ENABLE_HTTP_URL_REWRITES
static
int
mg_http_send_port_based_redirect
(
struct
mg_connection
*
c
,
struct
http_message
*
hm
,
const
struct
mg_serve_http_opts
*
opts
)
{
...
...
@@ -5807,6 +5822,103 @@ static int mg_http_send_port_based_redirect(
return
0
;
}
static
void
mg_reverse_proxy_handler
(
struct
mg_connection
*
nc
,
int
ev
,
void
*
ev_data
)
{
struct
http_message
*
hm
=
(
struct
http_message
*
)
ev_data
;
struct
mg_connection
*
upstream
=
(
struct
mg_connection
*
)
nc
->
user_data
;
switch
(
ev
)
{
case
MG_EV_CONNECT
:
if
(
*
(
int
*
)
ev_data
!=
0
)
{
mg_http_send_error
(
upstream
,
502
,
NULL
);
}
break
;
/* TODO(mkm): handle streaming */
case
MG_EV_HTTP_REPLY
:
mg_send
(
upstream
,
hm
->
message
.
p
,
hm
->
message
.
len
);
upstream
->
flags
|=
MG_F_SEND_AND_CLOSE
;
nc
->
flags
|=
MG_F_CLOSE_IMMEDIATELY
;
break
;
case
MG_EV_CLOSE
:
upstream
->
flags
|=
MG_F_SEND_AND_CLOSE
;
break
;
}
}
void
mg_handle_reverse_proxy
(
struct
mg_connection
*
nc
,
struct
http_message
*
hm
,
struct
mg_str
mount
,
struct
mg_str
upstream
)
{
struct
mg_connection
*
be
;
char
burl
[
256
],
*
purl
=
burl
;
char
*
addr
=
NULL
;
const
char
*
path
=
NULL
;
int
i
;
struct
mg_connect_opts
opts
;
memset
(
&
opts
,
0
,
sizeof
(
opts
));
mg_asprintf
(
&
purl
,
sizeof
(
burl
),
"%.*s%.*s"
,
(
int
)
upstream
.
len
,
upstream
.
p
,
(
int
)
(
hm
->
uri
.
len
-
mount
.
len
),
hm
->
uri
.
p
+
mount
.
len
);
be
=
mg_connect_http_base
(
nc
->
mgr
,
mg_reverse_proxy_handler
,
opts
,
"http://"
,
"https://"
,
purl
,
&
path
,
&
addr
);
DBG
((
"Proxying %.*s to %s (rule: %.*s)"
,
(
int
)
hm
->
uri
.
len
,
hm
->
uri
.
p
,
purl
,
(
int
)
mount
.
len
,
mount
.
p
));
if
(
be
==
NULL
)
{
mg_http_send_error
(
nc
,
502
,
NULL
);
goto
cleanup
;
}
be
->
user_data
=
nc
;
/* send request upstream */
mg_printf
(
be
,
"%.*s %s HTTP/1.1
\r\n
"
,
(
int
)
hm
->
method
.
len
,
hm
->
method
.
p
,
path
);
mg_printf
(
be
,
"Host: %s
\r\n
"
,
addr
);
for
(
i
=
0
;
i
<
MG_MAX_HTTP_HEADERS
&&
hm
->
header_names
[
i
].
len
>
0
;
i
++
)
{
struct
mg_str
hn
=
hm
->
header_names
[
i
];
struct
mg_str
hv
=
hm
->
header_values
[
i
];
/* we rewrite the host header */
if
(
mg_vcasecmp
(
&
hn
,
"Host"
)
==
0
)
continue
;
/*
* Don't pass chunked transfer encoding to the client because hm->body is
* already dechunked when we arrive here.
*/
if
(
mg_vcasecmp
(
&
hn
,
"Transfer-encoding"
)
==
0
&&
mg_vcasecmp
(
&
hv
,
"chunked"
)
==
0
)
{
mg_printf
(
be
,
"Content-Length: %"
SIZE_T_FMT
"
\r\n
"
,
hm
->
body
.
len
);
continue
;
}
mg_printf
(
be
,
"%.*s: %.*s
\r\n
"
,
(
int
)
hn
.
len
,
hn
.
p
,
(
int
)
hv
.
len
,
hv
.
p
);
}
mg_send
(
be
,
"
\r\n
"
,
2
);
mg_send
(
be
,
hm
->
body
.
p
,
hm
->
body
.
len
);
cleanup:
if
(
purl
!=
burl
)
MG_FREE
(
purl
);
}
static
int
mg_http_handle_forwarding
(
struct
mg_connection
*
nc
,
struct
http_message
*
hm
,
const
struct
mg_serve_http_opts
*
opts
)
{
const
char
*
rewrites
=
opts
->
url_rewrites
;
struct
mg_str
a
,
b
;
struct
mg_str
p1
=
MG_MK_STR
(
"http://"
),
p2
=
MG_MK_STR
(
"https://"
);
while
((
rewrites
=
mg_next_comma_list_entry
(
rewrites
,
&
a
,
&
b
))
!=
NULL
)
{
if
(
mg_strncmp
(
a
,
hm
->
uri
,
a
.
len
)
==
0
)
{
if
(
mg_strncmp
(
b
,
p1
,
p1
.
len
)
==
0
||
mg_strncmp
(
b
,
p2
,
p2
.
len
)
==
0
)
{
mg_handle_reverse_proxy
(
nc
,
hm
,
a
,
b
);
return
1
;
}
}
}
return
0
;
}
#endif
MG_INTERNAL
int
mg_uri_to_local_path
(
struct
http_message
*
hm
,
const
struct
mg_serve_http_opts
*
opts
,
char
**
local_path
,
...
...
@@ -5820,7 +5932,12 @@ MG_INTERNAL int mg_uri_to_local_path(struct http_message *hm,
remainder
->
len
=
0
;
{
/* 1. Determine which root to use. */
#if MG_ENABLE_HTTP_URL_REWRITES
const
char
*
rewrites
=
opts
->
url_rewrites
;
#else
const
char
*
rewrites
=
""
;
#endif
struct
mg_str
*
hh
=
mg_get_http_header
(
hm
,
"Host"
);
struct
mg_str
a
,
b
;
/* Check rewrites first. */
...
...
@@ -6154,9 +6271,15 @@ void mg_serve_http(struct mg_connection *nc, struct http_message *hm,
return
;
}
#if MG_ENABLE_HTTP_URL_REWRITES
if
(
mg_http_handle_forwarding
(
nc
,
hm
,
&
opts
))
{
return
;
}
if
(
mg_http_send_port_based_redirect
(
nc
,
hm
,
&
opts
))
{
return
;
}
#endif
if
(
opts
.
document_root
==
NULL
)
{
opts
.
document_root
=
"."
;
...
...
This diff is collapsed.
Click to expand it.
mongoose.h
View file @
900bbe72
...
...
@@ -1354,6 +1354,7 @@ int mg_vcasecmp(const struct mg_str *str2, const char *str1);
struct
mg_str
mg_strdup
(
const
struct
mg_str
s
);
int
mg_strcmp
(
const
struct
mg_str
str1
,
const
struct
mg_str
str2
);
int
mg_strncmp
(
const
struct
mg_str
str1
,
const
struct
mg_str
str2
,
size_t
n
);
#ifdef __cplusplus
}
...
...
@@ -1779,6 +1780,11 @@ char *strdup(const char *src);
#define MG_ENABLE_MQTT 1
#endif
#ifndef MG_ENABLE_HTTP_URL_REWRITES
#define MG_ENABLE_HTTP_URL_REWRITES \
(CS_PLATFORM == CS_P_WINDOWS || CS_PLATFORM == CS_P_UNIX)
#endif
#endif
/* CS_MONGOOSE_SRC_FEATURES_H_ */
#ifdef MG_MODULE_LINES
#line 1 "mongoose/src/net.h"
...
...
@@ -3226,16 +3232,19 @@ struct mg_serve_http_opts {
/* IP ACL. By default, NULL, meaning all IPs are allowed to connect */
const
char
*
ip_acl
;
#if MG_ENABLE_HTTP_URL_REWRITES
/* URL rewrites.
*
* Comma-separated list of `uri_pattern=file_or_directory_path` rewrites.
* Comma-separated list of `uri_pattern=
url_
file_or_directory_path` rewrites.
* When HTTP request is received, Mongoose constructs a file name from the
* requested URI by combining `document_root` and the URI. However, if the
* rewrite option is used and `uri_pattern` matches requested URI, then
* `document_root` is ignored. Instead, `file_or_directory_path` is used,
* `document_root` is ignored. Instead, `
url_
file_or_directory_path` is used,
* which should be a full path name or a path relative to the web server's
* current working directory. Note that `uri_pattern`, as all Mongoose
* patterns, is a prefix pattern.
* current working directory. It can also be an URI (http:// or https://)
* in which case mongoose will behave as a reverse proxy for that destination.
*
* Note that `uri_pattern`, as all Mongoose patterns, is a prefix pattern.
*
* If uri_pattern starts with `@` symbol, then Mongoose compares it with the
* HOST header of the request. If they are equal, Mongoose sets document root
...
...
@@ -3249,6 +3258,7 @@ struct mg_serve_http_opts {
* automatically appended to the redirect location.
*/
const
char
*
url_rewrites
;
#endif
/* DAV document root. If NULL, DAV requests are going to fail. */
const
char
*
dav_document_root
;
...
...
@@ -3449,6 +3459,12 @@ void mg_printf_http_chunk(struct mg_connection *nc, const char *fmt, ...);
void
mg_send_response_line
(
struct
mg_connection
*
nc
,
int
status_code
,
const
char
*
extra_headers
);
/*
* Sends an error response. If reason is NULL, the message will be inferred
* from the error code (if supported).
*/
void
mg_http_send_error
(
struct
mg_connection
*
nc
,
int
code
,
const
char
*
reason
);
/*
* Sends a redirect response.
* `status_code` should be either 301 or 302 and `location` point to the
...
...
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment