Commit dcef431e authored by Poul-Henning Kamp's avatar Poul-Henning Kamp

HTTP header munging part (N of M)

NB: Only pass mode (lightly) tested right now.

Give up on the three element array per header and use a two element struct
instead, it reduces obfuscation and removes risk of pointer fandango.

Introduce #defined filtercontrol in http_headers.h, use them in a new
field.  Only Pass is there for now.

Use the http-workspace for building headers instead of sbuf.

Move uiovec handling to cache_pool.c where it more naturally belongs
and so we can use it on both backends and sessions.

Add http header munging functiosn for copying, printf'ing, filtering and
writing headers.




git-svn-id: http://www.varnish-cache.org/svn/trunk/varnish-cache@533 d4fa192b-c00b-0410-8231-f00ffab90ce4
parent eb3d70c7
......@@ -15,10 +15,10 @@
#include "common.h"
#include "miniobj.h"
#define MAX_IOVS 10
#define MAX_HTTP_HDRS 32
#define MAX_IOVS (MAX_HTTP_HDRS * 2)
#define HTTP_HDR_REQ 0
#define HTTP_HDR_URL 1
#define HTTP_HDR_PROTO 2
......@@ -26,10 +26,6 @@
#define HTTP_HDR_RESPONSE 4
#define HTTP_HDR_FIRST 5
#define HTTP_START 0
#define HTTP_DATA 1
#define HTTP_END 2
struct event_base;
struct cli;
struct sbuf;
......@@ -52,6 +48,11 @@ enum step {
typedef void http_callback_f(void *, int bad);
struct http_hdr {
char *b;
char *e;
};
struct http {
unsigned magic;
#define HTTP_MAGIC 0x6428b5c9
......@@ -67,8 +68,9 @@ struct http {
unsigned conds; /* If-* headers present */
char *hd[MAX_HTTP_HDRS][HTTP_END + 1];
struct http_hdr hd[MAX_HTTP_HDRS];
unsigned nhd;
unsigned char hdf[MAX_HTTP_HDRS];
};
/*--------------------------------------------------------------------*/
......@@ -85,6 +87,8 @@ struct worker {
pthread_cond_t cv;
TAILQ_ENTRY(worker) list;
int *wfd;
unsigned werr; /* valid after WRK_Flush() */
struct iovec iov[MAX_IOVS];
unsigned niov;
size_t liov;
......@@ -296,6 +300,13 @@ void HSH_Init(void);
/* cache_http.c */
void HTTP_Init(void);
void http_Write(struct worker *w, struct http *hp, int resp);
void http_CopyReq(int fd, struct http *to, struct http *fm);
void http_CopyResp(int fd, struct http *to, struct http *fm);
void http_FilterHeader(int fd, struct http *to, struct http *fm, unsigned how);
void http_CopyHeader(int fd, struct http *to, struct http *fm, unsigned n);
void http_PrintfHeader(int fd, struct http *to, const char *fmt, ...);
int http_IsHdr(struct http_hdr *hh, char *hdr);
void http_Setup(struct http *ht, void *space, unsigned len);
int http_GetHdr(struct http *hp, const char *hdr, char **ptr);
int http_GetHdrField(struct http *hp, const char *hdr, const char *field, char **ptr);
......@@ -318,7 +329,7 @@ void http_BuildSbuf(int fd, enum http_build mode, struct sbuf *sb, struct http *
#undef HTTPH
/* cache_pass.c */
void PassSession(struct worker *w, struct sess *sp);
void PassSession(struct sess *sp);
void PassBody(struct worker *w, struct sess *sp);
/* cache_pipe.c */
......@@ -327,6 +338,10 @@ void PipeSession(struct worker *w, struct sess *sp);
/* cache_pool.c */
void WRK_Init(void);
void WRK_QueueSession(struct sess *sp);
void WRK_Reset(struct worker *w, int *fd);
int WRK_Flush(struct worker *w);
void WRK_Write(struct worker *w, const void *ptr, size_t len);
void WRK_WriteH(struct worker *w, struct http_hdr *hh, const char *suf);
/* cache_session.c [SES] */
void SES_Init(void);
......@@ -352,8 +367,6 @@ void VSL(enum shmlogtag tag, unsigned id, const char *fmt, ...);
/* cache_response.c */
void RES_Error(struct sess *sp, int error, const char *msg);
void RES_Flush(struct sess *sp);
void RES_Write(struct sess *sp, const void *ptr, size_t len);
void RES_WriteObj(struct sess *sp);
/* cache_vcl.c */
......
......@@ -90,8 +90,7 @@ cnt_done(struct sess *sp)
} else if (http_GetHdr(sp->http, H_Connection, &b) &&
!strcmp(b, "close")) {
vca_close_session(sp, "Connection header");
} else if (strcmp(sp->http->hd[HTTP_HDR_PROTO][HTTP_START],
"HTTP/1.1")) {
} else if (strcmp(sp->http->hd[HTTP_HDR_PROTO].b, "HTTP/1.1")) {
vca_close_session(sp, "not HTTP/1.1");
}
VCL_Rel(sp->vcl);
......@@ -263,7 +262,7 @@ cnt_hit(struct sess *sp)
if (sp->handling == VCL_RET_PASS) {
HSH_Deref(sp->obj);
sp->obj = NULL;
PassSession(sp->wrk, sp);
PassSession(sp);
sp->step = STP_PASSBODY;
return (0);
}
......@@ -398,7 +397,7 @@ cnt_miss(struct sess *sp)
HSH_Unbusy(sp->obj);
HSH_Deref(sp->obj);
sp->obj = 0;
PassSession(sp->wrk, sp);
PassSession(sp);
sp->step = STP_PASSBODY;
return (0);
}
......@@ -431,7 +430,7 @@ static int
cnt_pass(struct sess *sp)
{
PassSession(sp->wrk, sp);
PassSession(sp);
sp->step = STP_PASSBODY;
return (0);
}
......
......@@ -77,7 +77,7 @@ HSH_Lookup(struct sess *sp)
} else
CHECK_OBJ_NOTNULL(w->nobj, OBJECT_MAGIC);
url = h->hd[HTTP_HDR_URL][HTTP_START];
url = h->hd[HTTP_HDR_URL].b;
if (!http_GetHdr(h, H_Host, &host))
host = url;
if (sp->obj != NULL) {
......
This diff is collapsed.
......@@ -46,8 +46,9 @@ pass_straight(struct sess *sp, int fd, struct http *hp, char *bi)
if (i == 0 && bi == NULL)
return (1);
assert(i > 0);
RES_Write(sp, buf, i);
RES_Flush(sp);
WRK_Write(sp->wrk, buf, i);
if (WRK_Flush(sp->wrk))
vca_close_session(sp, "remote closed");
cl -= i;
}
return (0);
......@@ -92,7 +93,7 @@ pass_chunked(struct sess *sp, int fd, struct http *hp)
if (u == 0)
break;
RES_Write(sp, p, q - p);
WRK_Write(sp->wrk, p, q - p);
p = q;
......@@ -104,28 +105,30 @@ pass_chunked(struct sess *sp, int fd, struct http *hp)
}
if (bp - p < j)
j = bp - p;
RES_Write(sp, p, j);
WRK_Write(sp->wrk, p, j);
p += j;
u -= j;
}
while (u > 0) {
if (http_GetTail(hp, u, &b, &e)) {
j = e - b;
RES_Write(sp, q, j);
WRK_Write(sp->wrk, q, j);
u -= j;
} else
break;
}
RES_Flush(sp);
if (WRK_Flush(sp->wrk))
vca_close_session(sp, "remote closed");
while (u > 0) {
j = u;
if (j > sizeof buf)
j = sizeof buf;
i = read(fd, buf, j);
assert(i > 0);
RES_Write(sp, buf, i);
WRK_Write(sp->wrk, buf, i);
u -= i;
RES_Flush(sp);
if (WRK_Flush(sp->wrk))
vca_close_session(sp, "remote closed");
}
}
return (0);
......@@ -138,32 +141,34 @@ void
PassBody(struct worker *w, struct sess *sp)
{
struct vbe_conn *vc;
struct http *hp;
char *b;
int cls;
hp = sp->bkd_http;
assert(hp != NULL);
vc = sp->vbc;
assert(vc != NULL);
http_BuildSbuf(sp->fd, Build_Reply, w->sb, hp);
sbuf_cat(w->sb, "\r\n");
sbuf_finish(w->sb);
RES_Write(sp, sbuf_data(w->sb), sbuf_len(w->sb));
if (http_GetHdr(hp, H_Content_Length, &b))
cls = pass_straight(sp, vc->fd, hp, b);
else if (http_HdrIs(hp, H_Connection, "close"))
cls = pass_straight(sp, vc->fd, hp, NULL);
else if (http_HdrIs(hp, H_Transfer_Encoding, "chunked"))
cls = pass_chunked(sp, vc->fd, hp);
sp->http->f = sp->http->v;
sp->http->nhd = HTTP_HDR_FIRST;
http_CopyResp(sp->fd, sp->http, vc->http);
http_FilterHeader(sp->fd, sp->http, vc->http, HTTPH_A_PASS);
http_PrintfHeader(sp->fd, sp->http, "X-Varnish: %u", sp->xid);
WRK_Reset(w, &sp->fd);
http_Write(w, sp->http, 1);
if (http_GetHdr(vc->http, H_Content_Length, &b))
cls = pass_straight(sp, vc->fd, vc->http, b);
else if (http_HdrIs(vc->http, H_Connection, "close"))
cls = pass_straight(sp, vc->fd, vc->http, NULL);
else if (http_HdrIs(vc->http, H_Transfer_Encoding, "chunked"))
cls = pass_chunked(sp, vc->fd, vc->http);
else {
cls = pass_straight(sp, vc->fd, hp, NULL);
cls = pass_straight(sp, vc->fd, vc->http, NULL);
}
RES_Flush(sp);
if (http_GetHdr(hp, H_Connection, &b) && !strcasecmp(b, "close"))
if (WRK_Flush(w))
vca_close_session(sp, "remote closed");
if (http_GetHdr(vc->http, H_Connection, &b) && !strcasecmp(b, "close"))
cls = 1;
if (cls)
......@@ -172,21 +177,31 @@ PassBody(struct worker *w, struct sess *sp)
VBE_RecycleFd(vc);
}
/*--------------------------------------------------------------------*/
void
PassSession(struct worker *w, struct sess *sp)
PassSession(struct sess *sp)
{
int i;
struct vbe_conn *vc;
struct http *hp;
struct worker *w;
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
w = sp->wrk;
vc = VBE_GetFd(sp->backend, sp->xid);
assert(vc != NULL);
VSL(SLT_Backend, sp->fd, "%d %s", vc->fd, sp->backend->vcl_name);
http_BuildSbuf(vc->fd, Build_Pass, w->sb, sp->http);
i = write(vc->fd, sbuf_data(w->sb), sbuf_len(w->sb));
assert(i == sbuf_len(w->sb));
http_CopyReq(vc->fd, vc->http, sp->http);
http_FilterHeader(vc->fd, vc->http, sp->http, HTTPH_R_PASS);
http_PrintfHeader(vc->fd, vc->http, "X-Varnish: %u", sp->xid);
WRK_Reset(w, &vc->fd);
http_Write(w, vc->http, 0);
i = WRK_Flush(w);
assert(i == 0);
/* XXX: copy any contents */
......
......@@ -24,6 +24,73 @@ static unsigned wrk_overflow;
static TAILQ_HEAD(, worker) wrk_head = TAILQ_HEAD_INITIALIZER(wrk_head);
static TAILQ_HEAD(, workreq) wrk_reqhead = TAILQ_HEAD_INITIALIZER(wrk_reqhead);
/*--------------------------------------------------------------------
* Write data to fd
* We try to use writev() if possible in order to minimize number of
* syscalls made and packets sent. It also just might allow the worker
* thread to complete the request without holding stuff locked.
*/
void
WRK_Reset(struct worker *w, int *fd)
{
CHECK_OBJ_NOTNULL(w, WORKER_MAGIC);
w->werr = 0;
w->liov = 0;
w->niov = 0;
w->wfd = fd;
}
int
WRK_Flush(struct worker *w)
{
int i;
CHECK_OBJ_NOTNULL(w, WORKER_MAGIC);
if (*w->wfd < 0 || w->niov == 0 || w->werr)
return (w->werr);
VSL(SLT_Debug, 0, "%s %d", __func__, *w->wfd);
i = writev(*w->wfd, w->iov, w->niov);
if (i != w->liov)
w->werr++;
else {
w->liov = 0;
w->niov = 0;
}
return (w->werr);
}
void
WRK_WriteH(struct worker *w, struct http_hdr *hh, const char *suf)
{
CHECK_OBJ_NOTNULL(w, WORKER_MAGIC);
assert(w != NULL);
assert(hh != NULL);
assert(hh->b != NULL);
assert(hh->e != NULL);
WRK_Write(w, hh->b, hh->e - hh->b);
if (suf != NULL)
WRK_Write(w, suf, -1);
}
void
WRK_Write(struct worker *w, const void *ptr, size_t len)
{
CHECK_OBJ_NOTNULL(w, WORKER_MAGIC);
if (len == 0 || *w->wfd < 0)
return;
if (len == -1)
len = strlen(ptr);
if (w->niov == MAX_IOVS)
WRK_Flush(w);
w->iov[w->niov].iov_base = (void*)(uintptr_t)ptr;
w->iov[w->niov++].iov_len = len;
w->liov += len;
}
/*--------------------------------------------------------------------*/
static void
......
......@@ -65,50 +65,12 @@ RES_Error(struct sess *sp, int error, const char *msg)
" </BODY>\r\n"
"</HTML>\r\n");
sbuf_finish(sb);
RES_Write(sp, sbuf_data(sb), sbuf_len(sb));
RES_Flush(sp);
WRK_Write(sp->wrk, sbuf_data(sb), sbuf_len(sb));
WRK_Flush(sp->wrk);
vca_close_session(sp, msg);
}
/*--------------------------------------------------------------------
* Write data to client
* We try to use writev() if possible in order to minimize number of
* syscalls made and packets sent. It also just might allow the worker
* thread to complete the request without holding stuff locked.
*/
void
RES_Flush(struct sess *sp)
{
int i;
if (sp->fd < 0 || sp->wrk->niov == 0)
return;
i = writev(sp->fd, sp->wrk->iov, sp->wrk->niov);
if (i != sp->wrk->liov)
vca_close_session(sp, "remote closed");
sp->wrk->liov = 0;
sp->wrk->niov = 0;
}
void
RES_Write(struct sess *sp, const void *ptr, size_t len)
{
if (sp->fd < 0 || len == 0)
return;
if (len == -1)
len = strlen(ptr);
if (sp->wrk->niov == MAX_IOVS)
RES_Flush(sp);
if (sp->fd < 0)
return;
sp->wrk->iov[sp->wrk->niov].iov_base = (void*)(uintptr_t)ptr;
sp->wrk->iov[sp->wrk->niov++].iov_len = len;
sp->wrk->liov += len;
}
/*--------------------------------------------------------------------*/
static void
......@@ -121,18 +83,18 @@ res_do_304(struct sess *sp, char *p)
VSL(SLT_Status, sp->fd, "%u", 304);
VSL(SLT_Length, sp->fd, "%u", 0);
RES_Write(sp, "HTTP/1.1 304 Not Modified\r\n", -1);
RES_Write(sp, "Via: 1.1 varnish\r\n", -1);
RES_Write(sp, "Last-Modified: ", -1);
RES_Write(sp, p, -1);
RES_Write(sp, "\r\n", -1);
if (strcmp(sp->http->hd[HTTP_HDR_PROTO][HTTP_START], "HTTP/1.1"))
RES_Write(sp, "Connection: close\r\n", -1);
WRK_Write(sp->wrk, "HTTP/1.1 304 Not Modified\r\n", -1);
WRK_Write(sp->wrk, "Via: 1.1 varnish\r\n", -1);
WRK_Write(sp->wrk, "Last-Modified: ", -1);
WRK_Write(sp->wrk, p, -1);
WRK_Write(sp->wrk, "\r\n", -1);
if (strcmp(sp->http->hd[HTTP_HDR_PROTO].b, "HTTP/1.1"))
WRK_Write(sp->wrk, "Connection: close\r\n", -1);
sbuf_printf(sb, "X-Varnish: xid %u\r\n", sp->obj->xid);
sbuf_printf(sb, "\r\n");
sbuf_finish(sb);
RES_Write(sp, sbuf_data(sb), sbuf_len(sb));
RES_Flush(sp);
WRK_Write(sp->wrk, sbuf_data(sb), sbuf_len(sb));
WRK_Flush(sp->wrk);
}
/*--------------------------------------------------------------------*/
......@@ -180,26 +142,26 @@ RES_WriteObj(struct sess *sp)
VSL(SLT_Status, sp->fd, "%u", sp->obj->response);
VSL(SLT_Length, sp->fd, "%u", sp->obj->len);
RES_Write(sp, sp->obj->header, strlen(sp->obj->header));
WRK_Write(sp->wrk, sp->obj->header, strlen(sp->obj->header));
sbuf_clear(sb);
sbuf_printf(sb, "Age: %u\r\n",
sp->obj->age + sp->t_req - sp->obj->entered);
sbuf_printf(sb, "Via: 1.1 varnish\r\n");
sbuf_printf(sb, "X-Varnish: xid %u\r\n", sp->obj->xid);
if (strcmp(sp->http->hd[HTTP_HDR_PROTO][HTTP_START], "HTTP/1.1"))
if (strcmp(sp->http->hd[HTTP_HDR_PROTO].b, "HTTP/1.1"))
sbuf_printf(sb, "Connection: close\r\n");
sbuf_printf(sb, "\r\n");
sbuf_finish(sb);
RES_Write(sp, sbuf_data(sb), sbuf_len(sb));
WRK_Write(sp->wrk, sbuf_data(sb), sbuf_len(sb));
bytes += sbuf_len(sb);
/* XXX: conditional request handling */
if (!strcmp(sp->http->hd[HTTP_HDR_REQ][HTTP_START], "GET")) {
if (!strcmp(sp->http->hd[HTTP_HDR_REQ].b, "GET")) {
TAILQ_FOREACH(st, &sp->obj->store, list) {
assert(st->stevedore != NULL);
u += st->len;
if (st->stevedore->send == NULL) {
RES_Write(sp, st->ptr, st->len);
WRK_Write(sp->wrk, st->ptr, st->len);
continue;
}
st->stevedore->send(st, sp,
......@@ -210,5 +172,5 @@ RES_WriteObj(struct sess *sp)
assert(u == sp->obj->len);
}
SES_ChargeBytes(sp, bytes + u);
RES_Flush(sp);
WRK_Flush(sp->wrk);
}
......@@ -60,7 +60,7 @@ VRT_GetReq(struct sess *sp)
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
assert(sp != NULL);
assert(sp->http != NULL);
return (sp->http->hd[HTTP_HDR_REQ][HTTP_START]);
return (sp->http->hd[HTTP_HDR_REQ].b);
}
/*--------------------------------------------------------------------*/
......
......@@ -5,7 +5,7 @@
* b session field name
* c Request(1)/Response(2) bitfield
* d Supress header to backend (1) / Supress header to client (2)
* e unused
* e Supress header in pass from client to backend
* f unused
* g unused
*
......@@ -15,24 +15,29 @@
*--------------------------------------------------------------------
*/
HTTPH("Keep-Alive", H_Keep_Alive, 3, 3, 0, 0, 0) /* RFC2068 */
#ifndef HTTPH_R_PASS
#define HTTPH_R_PASS (1 << 0)
#define HTTPH_A_PASS (1 << 1)
#endif
HTTPH("Keep-Alive", H_Keep_Alive, 3, 3, HTTPH_R_PASS|HTTPH_A_PASS, 0, 0) /* RFC2068 */
HTTPH("Accept", H_Accept, 1, 0, 0, 0, 0) /* RFC2616 14.1 */
HTTPH("Accept-Charset", H_Accept_Charset, 1, 0, 0, 0, 0) /* RFC2616 14.2 */
HTTPH("Accept-Encoding", H_Accept_Encoding, 1, 0, 0, 0, 0) /* RFC2616 14.3 */
HTTPH("Accept-Language", H_Accept_Language, 1, 0, 0, 0, 0) /* RFC2616 14.4 */
HTTPH("Accept-Ranges", H_Accept_Ranges, 2, 3, 0, 0, 0) /* RFC2616 14.5 */
HTTPH("Accept-Ranges", H_Accept_Ranges, 2, 3, HTTPH_R_PASS|HTTPH_A_PASS, 0, 0) /* RFC2616 14.5 */
HTTPH("Age", H_Age, 2, 0, 0, 0, 0) /* RFC2616 14.6 */
HTTPH("Allow", H_Allow, 2, 0, 0, 0, 0) /* RFC2616 14.7 */
HTTPH("Authorization", H_Authorization, 1, 0, 0, 0, 0) /* RFC2616 14.8 */
HTTPH("Cache-Control", H_Cache_Control, 3, 3, 0, 0, 0) /* RFC2616 14.9 */
HTTPH("Connection", H_Connection, 3, 3, 0, 0, 0) /* RFC2616 14.10 */
HTTPH("Cache-Control", H_Cache_Control, 3, 3, HTTPH_R_PASS|HTTPH_A_PASS, 0, 0) /* RFC2616 14.9 */
HTTPH("Connection", H_Connection, 3, 3, HTTPH_R_PASS|HTTPH_A_PASS, 0, 0) /* RFC2616 14.10 */
HTTPH("Content-Encoding", H_Content_Encoding, 2, 0, 0, 0, 0) /* RFC2616 14.11 */
HTTPH("Content-Langugae", H_Content_Language, 2, 0, 0, 0, 0) /* RFC2616 14.12 */
HTTPH("Content-Length", H_Content_Length, 2, 2, 0, 0, 0) /* RFC2616 14.13 */
HTTPH("Content-Location", H_Content_Location, 2, 0, 0, 0, 0) /* RFC2616 14.14 */
HTTPH("Content-MD5", H_Content_MD5, 2, 0, 0, 0, 0) /* RFC2616 14.15 */
HTTPH("Content-Range", H_Content_Range, 2, 3, 0, 0, 0) /* RFC2616 14.16 */
HTTPH("Content-Range", H_Content_Range, 2, 3, HTTPH_R_PASS|HTTPH_A_PASS, 0, 0) /* RFC2616 14.16 */
HTTPH("Content-Type", H_Content_Type, 2, 0, 0, 0, 0) /* RFC2616 14.17 */
HTTPH("Date", H_Date, 2, 0, 0, 0, 0) /* RFC2616 14.18 */
HTTPH("ETag", H_ETag, 2, 0, 0, 0, 0) /* RFC2616 14.19 */
......@@ -55,10 +60,10 @@ HTTPH("Range", H_Range, 1, 0, 0, 0, 0) /* RFC2616 14.35 */
HTTPH("Referer", H_Referer, 1, 0, 0, 0, 0) /* RFC2616 14.36 */
HTTPH("Retry-After", H_Retry_After, 2, 0, 0, 0, 0) /* RFC2616 14.37 */
HTTPH("Server", H_Server, 2, 0, 0, 0, 0) /* RFC2616 14.38 */
HTTPH("TE", H_TE, 1, 3, 0, 0, 0) /* RFC2616 14.39 */
HTTPH("Trailer", H_Trailer, 1, 3, 0, 0, 0) /* RFC2616 14.40 */
HTTPH("Transfer-Encoding", H_Transfer_Encoding, 2, 3, 0, 0, 0) /* RFC2616 14.41 */
HTTPH("Upgrade", H_Upgrade, 2, 3, 0, 0, 0) /* RFC2616 14.42 */
HTTPH("TE", H_TE, 1, 3, HTTPH_R_PASS|HTTPH_A_PASS, 0, 0) /* RFC2616 14.39 */
HTTPH("Trailer", H_Trailer, 1, 3, HTTPH_R_PASS|HTTPH_A_PASS, 0, 0) /* RFC2616 14.40 */
HTTPH("Transfer-Encoding", H_Transfer_Encoding, 2, 3, HTTPH_R_PASS|HTTPH_A_PASS, 0, 0) /* RFC2616 14.41 */
HTTPH("Upgrade", H_Upgrade, 2, 3, HTTPH_R_PASS|HTTPH_A_PASS, 0, 0) /* RFC2616 14.42 */
HTTPH("User-Agent", H_User_Agent, 1, 0, 0, 0, 0) /* RFC2616 14.43 */
HTTPH("Vary", H_Vary, 2, 0, 0, 0, 0) /* RFC2616 14.44 */
HTTPH("Via", H_Via, 2, 0, 0, 0, 0) /* RFC2616 14.45 */
......
Markdown is supported
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