mirror of
https://github.com/jart/cosmopolitan.git
synced 2025-04-15 20:28:45 +00:00
Transcode ISO-8859-1 in HTTP headers
If we keep making changes like this, redbean might not be a toy anymore. Additional steps are also being taken now to prevent ANSI control codes sent by the client from slipping into logs.
This commit is contained in:
parent
dcbd2b8668
commit
a1677d605a
14 changed files with 675 additions and 161 deletions
54
net/http/decodelatin1.c
Normal file
54
net/http/decodelatin1.c
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||||
|
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||||
|
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||||
|
│ Copyright 2021 Justine Alexandra Roberts Tunney │
|
||||||
|
│ │
|
||||||
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||||
|
│ any purpose with or without fee is hereby granted, provided that the │
|
||||||
|
│ above copyright notice and this permission notice appear in all copies. │
|
||||||
|
│ │
|
||||||
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||||
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||||
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||||
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||||
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||||
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||||
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
|
#include "libc/mem/mem.h"
|
||||||
|
#include "libc/str/str.h"
|
||||||
|
#include "net/http/http.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes ISO-8859-1 to UTF-8.
|
||||||
|
*
|
||||||
|
* @param data is input value
|
||||||
|
* @param size if -1 implies strlen
|
||||||
|
* @param out_size if non-NULL receives output length on success
|
||||||
|
* @return allocated NUL-terminated buffer, or NULL w/ errno
|
||||||
|
*/
|
||||||
|
char *DecodeLatin1(const char *data, size_t size, size_t *out_size) {
|
||||||
|
int c;
|
||||||
|
char *r, *q;
|
||||||
|
const char *p, *e;
|
||||||
|
if (size == -1) size = strlen(data);
|
||||||
|
if ((r = malloc(size * 2 + 1))) {
|
||||||
|
q = r;
|
||||||
|
p = data;
|
||||||
|
e = p + size;
|
||||||
|
while (p < e) {
|
||||||
|
c = *p++ & 0xff;
|
||||||
|
if (c < 0200) {
|
||||||
|
*q++ = c;
|
||||||
|
} else {
|
||||||
|
*q++ = 0300 | c >> 6;
|
||||||
|
*q++ = 0200 | c & 077;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (out_size) *out_size = q - r;
|
||||||
|
*q++ = '\0';
|
||||||
|
if ((q = realloc(r, q - r))) r = q;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
85
net/http/encodehttpheadervalue.c
Normal file
85
net/http/encodehttpheadervalue.c
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||||
|
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||||
|
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||||
|
│ Copyright 2021 Justine Alexandra Roberts Tunney │
|
||||||
|
│ │
|
||||||
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||||
|
│ any purpose with or without fee is hereby granted, provided that the │
|
||||||
|
│ above copyright notice and this permission notice appear in all copies. │
|
||||||
|
│ │
|
||||||
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||||
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||||
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||||
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||||
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||||
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||||
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
|
#include "libc/errno.h"
|
||||||
|
#include "libc/mem/mem.h"
|
||||||
|
#include "libc/str/str.h"
|
||||||
|
#include "libc/str/thompike.h"
|
||||||
|
#include "net/http/http.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes HTTP header value.
|
||||||
|
*
|
||||||
|
* This operation involves the following:
|
||||||
|
*
|
||||||
|
* 1. Trim whitespace.
|
||||||
|
* 2. Turn UTF-8 into ISO-8859-1.
|
||||||
|
* 3. Make sure no C0 or C1 control codes are present (except tab).
|
||||||
|
*
|
||||||
|
* If the input value isn't thompson-pike encoded then this
|
||||||
|
* implementation will fall back to latin1 in most cases.
|
||||||
|
*
|
||||||
|
* @param data is input value
|
||||||
|
* @param size if -1 implies strlen
|
||||||
|
* @param out_size if non-NULL receives output length on success
|
||||||
|
* @return allocated NUL-terminated string, or NULL w/ errno
|
||||||
|
*/
|
||||||
|
char *EncodeHttpHeaderValue(const char *data, size_t size, size_t *out_size) {
|
||||||
|
bool t;
|
||||||
|
wint_t x;
|
||||||
|
char *r, *q;
|
||||||
|
const char *p, *e;
|
||||||
|
if (size == -1) size = strlen(data);
|
||||||
|
if ((r = malloc(size + 1))) {
|
||||||
|
t = 0;
|
||||||
|
q = r;
|
||||||
|
p = data;
|
||||||
|
e = p + size;
|
||||||
|
while (p < e) {
|
||||||
|
x = *p++ & 0xff;
|
||||||
|
if (x >= 0300) {
|
||||||
|
if (p < e && ThomPikeCont(*p)) {
|
||||||
|
if (ThomPikeLen(x) == 2) {
|
||||||
|
x = ThomPikeMerge(ThomPikeByte(x), *p++);
|
||||||
|
} else {
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!t) {
|
||||||
|
if (x == ' ' || x == '\t') {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
t = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((0x20 <= x && x <= 0x7E) || (0xA0 <= x && x <= 0xFF) || x == '\t') {
|
||||||
|
*q++ = x;
|
||||||
|
} else {
|
||||||
|
free(r);
|
||||||
|
errno = EILSEQ;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (q > r && (q[-1] == ' ' || q[-1] == '\t')) --q;
|
||||||
|
if (out_size) *out_size = q - r;
|
||||||
|
*q++ = '\0';
|
||||||
|
if ((q = realloc(r, q - r))) r = q;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
|
@ -121,6 +121,10 @@ unsigned ParseHttpVersion(const char *, size_t);
|
||||||
int64_t ParseHttpDateTime(const char *, size_t);
|
int64_t ParseHttpDateTime(const char *, size_t);
|
||||||
const char *GetHttpReason(int);
|
const char *GetHttpReason(int);
|
||||||
const char *GetHttpHeaderName(int);
|
const char *GetHttpHeaderName(int);
|
||||||
|
char *DecodeLatin1(const char *, size_t, size_t *);
|
||||||
|
bool IsValidHttpToken(const char *, size_t);
|
||||||
|
char *EncodeHttpHeaderValue(const char *, size_t, size_t *);
|
||||||
|
char *VisualizeControlCodes(const char *, size_t, size_t *);
|
||||||
|
|
||||||
COSMOPOLITAN_C_END_
|
COSMOPOLITAN_C_END_
|
||||||
#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */
|
#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */
|
||||||
|
|
54
net/http/isvalidhttptoken.c
Normal file
54
net/http/isvalidhttptoken.c
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||||
|
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||||
|
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||||
|
│ Copyright 2021 Justine Alexandra Roberts Tunney │
|
||||||
|
│ │
|
||||||
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||||
|
│ any purpose with or without fee is hereby granted, provided that the │
|
||||||
|
│ above copyright notice and this permission notice appear in all copies. │
|
||||||
|
│ │
|
||||||
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||||
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||||
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||||
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||||
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||||
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||||
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
|
#include "libc/str/str.h"
|
||||||
|
#include "net/http/http.h"
|
||||||
|
|
||||||
|
// http/1.1 token dispatch
|
||||||
|
// 0 is CTLs, SP, ()<>@,;:\"/[]?={}
|
||||||
|
// 1 is legal ascii
|
||||||
|
static const char kHttpToken[256] = {
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10
|
||||||
|
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 0x20
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, // 0x30
|
||||||
|
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 0x50
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 0x70
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xa0
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xb0
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xc0
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xd0
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xe0
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xf0
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsValidHttpToken(const char *s, size_t n) {
|
||||||
|
size_t i;
|
||||||
|
if (!n) return false;
|
||||||
|
if (n == -1) n = strlen(s);
|
||||||
|
for (i = 0; i < n; ++i) {
|
||||||
|
if (!kHttpToken[s[i] & 0xff]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -47,9 +47,19 @@ void DestroyHttpRequest(struct HttpRequest *r) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses HTTP request.
|
* Parses HTTP request.
|
||||||
|
*
|
||||||
|
* This parser is responsible for determining the length of a message
|
||||||
|
* and slicing the strings inside it. Performance is attained using
|
||||||
|
* perfect hash tables. No memory allocation is performed for normal
|
||||||
|
* messages. Line folding is forbidden. State persists across calls so
|
||||||
|
* that fragmented messages can be handled efficiently. A limitation on
|
||||||
|
* message size is imposed to make the header data structures smaller.
|
||||||
|
* All other things are permissive to the greatest extent possible.
|
||||||
|
* Further functions are provided for the interpretation, validation,
|
||||||
|
* and sanitization of various fields.
|
||||||
*/
|
*/
|
||||||
int ParseHttpRequest(struct HttpRequest *r, const char *p, size_t n) {
|
int ParseHttpRequest(struct HttpRequest *r, const char *p, size_t n) {
|
||||||
int c, h;
|
int c, h, i;
|
||||||
struct HttpRequestHeader *x;
|
struct HttpRequestHeader *x;
|
||||||
for (n = MIN(n, LIMIT); r->i < n; ++r->i) {
|
for (n = MIN(n, LIMIT); r->i < n; ++r->i) {
|
||||||
c = p[r->i] & 0xff;
|
c = p[r->i] & 0xff;
|
||||||
|
@ -122,14 +132,16 @@ int ParseHttpRequest(struct HttpRequest *r, const char *p, size_t n) {
|
||||||
/* fallthrough */
|
/* fallthrough */
|
||||||
case HVAL:
|
case HVAL:
|
||||||
if (c == '\r' || c == '\n') {
|
if (c == '\r' || c == '\n') {
|
||||||
|
i = r->i;
|
||||||
|
while (i > r->a && (p[i - 1] == ' ' || p[i - 1] == '\t')) --i;
|
||||||
if ((h = GetHttpHeader(p + r->k.a, r->k.b - r->k.a)) != -1) {
|
if ((h = GetHttpHeader(p + r->k.a, r->k.b - r->k.a)) != -1) {
|
||||||
r->headers[h].a = r->a;
|
r->headers[h].a = r->a;
|
||||||
r->headers[h].b = r->i;
|
r->headers[h].b = i;
|
||||||
} else if ((x = realloc(r->xheaders.p, (r->xheaders.n + 1) *
|
} else if ((x = realloc(r->xheaders.p, (r->xheaders.n + 1) *
|
||||||
sizeof(*r->xheaders.p)))) {
|
sizeof(*r->xheaders.p)))) {
|
||||||
x[r->xheaders.n].k = r->k;
|
x[r->xheaders.n].k = r->k;
|
||||||
x[r->xheaders.n].v.a = r->a;
|
x[r->xheaders.n].v.a = r->a;
|
||||||
x[r->xheaders.n].v.b = r->i;
|
x[r->xheaders.n].v.b = i;
|
||||||
r->xheaders.p = x;
|
r->xheaders.p = x;
|
||||||
++r->xheaders.n;
|
++r->xheaders.n;
|
||||||
}
|
}
|
||||||
|
|
93
net/http/visualizecontrolcodes.c
Normal file
93
net/http/visualizecontrolcodes.c
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||||
|
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||||
|
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||||
|
│ Copyright 2021 Justine Alexandra Roberts Tunney │
|
||||||
|
│ │
|
||||||
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||||
|
│ any purpose with or without fee is hereby granted, provided that the │
|
||||||
|
│ above copyright notice and this permission notice appear in all copies. │
|
||||||
|
│ │
|
||||||
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||||
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||||
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||||
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||||
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||||
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||||
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
|
#include "libc/mem/mem.h"
|
||||||
|
#include "libc/str/str.h"
|
||||||
|
#include "libc/str/thompike.h"
|
||||||
|
#include "libc/str/tpenc.h"
|
||||||
|
#include "net/http/http.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out control codes from string.
|
||||||
|
*
|
||||||
|
* This is useful for logging data like HTTP messages, where we don't
|
||||||
|
* want full blown C string literal escaping, but we don't want things
|
||||||
|
* like raw ANSI control codes from untrusted devices in our terminals.
|
||||||
|
*
|
||||||
|
* @param data is input value
|
||||||
|
* @param size if -1 implies strlen
|
||||||
|
* @param out_size if non-NULL receives output length on success
|
||||||
|
* @return allocated NUL-terminated buffer, or NULL w/ errno
|
||||||
|
*/
|
||||||
|
char *VisualizeControlCodes(const char *data, size_t size, size_t *out_size) {
|
||||||
|
uint64_t w;
|
||||||
|
char *r, *q;
|
||||||
|
unsigned i, n;
|
||||||
|
wint_t x, a, b;
|
||||||
|
const char *p, *e;
|
||||||
|
if (size == -1) size = strlen(data);
|
||||||
|
if ((r = malloc(size * 6 + 1))) {
|
||||||
|
q = r;
|
||||||
|
p = data;
|
||||||
|
e = p + size;
|
||||||
|
while (p < e) {
|
||||||
|
x = *p++ & 0xff;
|
||||||
|
if (x >= 0300) {
|
||||||
|
a = ThomPikeByte(x);
|
||||||
|
n = ThomPikeLen(x) - 1;
|
||||||
|
if (p + n <= e) {
|
||||||
|
for (i = 0;;) {
|
||||||
|
b = p[i] & 0xff;
|
||||||
|
if (!ThomPikeCont(b)) break;
|
||||||
|
a = ThomPikeMerge(a, b);
|
||||||
|
if (++i == n) {
|
||||||
|
x = a;
|
||||||
|
p += i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (0x80 <= x && x < 0xA0) {
|
||||||
|
q[0] = '\\';
|
||||||
|
q[1] = 'u';
|
||||||
|
q[2] = '0';
|
||||||
|
q[3] = '0';
|
||||||
|
q[4] = "0123456789abcdef"[(x & 0xF0) >> 4];
|
||||||
|
q[5] = "0123456789abcdef"[(x & 0x0F) >> 0];
|
||||||
|
q += 6;
|
||||||
|
} else {
|
||||||
|
if (0x00 <= x && x < 0x20) {
|
||||||
|
if (x != '\t' && x != '\r' && x != '\n') {
|
||||||
|
x += 0x2400; /* Control Pictures */
|
||||||
|
}
|
||||||
|
} else if (x == 0x7F) {
|
||||||
|
x = 0x2421;
|
||||||
|
}
|
||||||
|
w = tpenc(x);
|
||||||
|
do {
|
||||||
|
*q++ = w;
|
||||||
|
} while ((w >>= 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (out_size) *out_size = q - r;
|
||||||
|
*q++ = '\0';
|
||||||
|
if ((q = realloc(r, q - r))) r = q;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
28
test/net/http/decodelatin1_test.c
Normal file
28
test/net/http/decodelatin1_test.c
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||||
|
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||||
|
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||||
|
│ Copyright 2021 Justine Alexandra Roberts Tunney │
|
||||||
|
│ │
|
||||||
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||||
|
│ any purpose with or without fee is hereby granted, provided that the │
|
||||||
|
│ above copyright notice and this permission notice appear in all copies. │
|
||||||
|
│ │
|
||||||
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||||
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||||
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||||
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||||
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||||
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||||
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
|
#include "libc/testlib/testlib.h"
|
||||||
|
#include "net/http/http.h"
|
||||||
|
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
TEST(DecodeLatin1, test) {
|
||||||
|
EXPECT_STREQ("", DecodeLatin1(NULL, 0, 0));
|
||||||
|
EXPECT_STREQ("¥atta", DecodeLatin1("\245atta", -1, &n));
|
||||||
|
EXPECT_EQ(6, n);
|
||||||
|
}
|
82
test/net/http/encodehttpheadervalue_test.c
Normal file
82
test/net/http/encodehttpheadervalue_test.c
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||||
|
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||||
|
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||||
|
│ Copyright 2021 Justine Alexandra Roberts Tunney │
|
||||||
|
│ │
|
||||||
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||||
|
│ any purpose with or without fee is hereby granted, provided that the │
|
||||||
|
│ above copyright notice and this permission notice appear in all copies. │
|
||||||
|
│ │
|
||||||
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||||
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||||
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||||
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||||
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||||
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||||
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
|
#include "libc/errno.h"
|
||||||
|
#include "libc/mem/mem.h"
|
||||||
|
#include "libc/runtime/gc.internal.h"
|
||||||
|
#include "libc/stdio/stdio.h"
|
||||||
|
#include "libc/testlib/ezbench.h"
|
||||||
|
#include "libc/testlib/testlib.h"
|
||||||
|
#include "net/http/http.h"
|
||||||
|
|
||||||
|
char *p;
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
TEST(EncodeHttpHeaderValue, emptyStrings_arePermitted) {
|
||||||
|
EXPECT_STREQ("", gc(EncodeHttpHeaderValue(NULL, 0, 0)));
|
||||||
|
EXPECT_STREQ("", gc(EncodeHttpHeaderValue("", 0, 0)));
|
||||||
|
EXPECT_STREQ("", gc(EncodeHttpHeaderValue(" ", 1, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EncodeHttpHeaderValue, testPadded_trimsWhitespace) {
|
||||||
|
EXPECT_STREQ("hello \tthere",
|
||||||
|
gc(EncodeHttpHeaderValue(" \thello \tthere\t ", -1, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EncodeHttpHeaderValue, testUtf8_isConvertedToLatin1) {
|
||||||
|
EXPECT_STREQ("\241\377\300", gc(EncodeHttpHeaderValue("¡ÿÀ", -1, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EncodeHttpHeaderValue, testUtf8_nonLatin1ResultsInError) {
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("☻", -1, 0)));
|
||||||
|
EXPECT_EQ(EILSEQ, errno);
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("𐌰", -1, 0)));
|
||||||
|
EXPECT_EQ(EILSEQ, errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EncodeHttpHeaderValue, testLatin1_willJustWorkIfYoureLucky) {
|
||||||
|
EXPECT_STREQ("\241\377\300",
|
||||||
|
gc(EncodeHttpHeaderValue("\241\377\300", -1, &n)));
|
||||||
|
EXPECT_EQ(3, n);
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("\377\241\300", -1, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EncodeHttpHeaderValue, testC0_isForbiddenExceptHorizontalTab) {
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("\0", 1, 0)));
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("\r", 1, 0)));
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("\n", 1, 0)));
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("\v", 1, 0)));
|
||||||
|
EXPECT_STREQ("", gc(EncodeHttpHeaderValue("\t", 1, 0)));
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("\177", 1, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EncodeHttpHeaderValue, testC1_isForbidden) {
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("\205", 1, 0)));
|
||||||
|
EXPECT_EQ(NULL, gc(EncodeHttpHeaderValue("\302\205", 2, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCH(EncodeHttpHeaderValue, bench) {
|
||||||
|
n = 22851;
|
||||||
|
p = gc(malloc(n));
|
||||||
|
memset(p, 'a', n);
|
||||||
|
EZBENCH2("EncodeHttpHeaderValue ascii", donothing,
|
||||||
|
free(EncodeHttpHeaderValue(p, n, 0)));
|
||||||
|
memset(p, '\300', n);
|
||||||
|
EZBENCH2("EncodeHttpHeaderValue latin1", donothing,
|
||||||
|
free(EncodeHttpHeaderValue(p, n, 0)));
|
||||||
|
}
|
|
@ -49,6 +49,10 @@ void TearDown(void) {
|
||||||
DestroyHttpRequest(req);
|
DestroyHttpRequest(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TEST(ParseHttpRequest, soLittleState) { */
|
||||||
|
/* ASSERT_EQ(280, sizeof(struct HttpRequest)); */
|
||||||
|
/* } */
|
||||||
|
|
||||||
TEST(ParseHttpRequest, testEmpty_tooShort) {
|
TEST(ParseHttpRequest, testEmpty_tooShort) {
|
||||||
EXPECT_EQ(0, ParseHttpRequest(req, "", 0));
|
EXPECT_EQ(0, ParseHttpRequest(req, "", 0));
|
||||||
}
|
}
|
||||||
|
@ -186,3 +190,12 @@ X-User-Agent: hi\r\n\
|
||||||
EXPECT_STREQ("X-User-Agent", gc(slice(m, req->xheaders.p[0].k)));
|
EXPECT_STREQ("X-User-Agent", gc(slice(m, req->xheaders.p[0].k)));
|
||||||
EXPECT_STREQ("hi", gc(slice(m, req->xheaders.p[0].v)));
|
EXPECT_STREQ("hi", gc(slice(m, req->xheaders.p[0].v)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ParseHttpRequest, testHeaderValuesWithWhitespace_getsTrimmed) {
|
||||||
|
static const char m[] = "\
|
||||||
|
OPTIONS * HTTP/1.0\r\n\
|
||||||
|
User-Agent: \t hi there \t \r\n\
|
||||||
|
\r\n";
|
||||||
|
EXPECT_EQ(strlen(m), ParseHttpRequest(req, m, strlen(m)));
|
||||||
|
EXPECT_STREQ("hi there", gc(slice(m, req->headers[kHttpUserAgent])));
|
||||||
|
}
|
||||||
|
|
34
test/net/http/visualizecontrolcodes_test.c
Normal file
34
test/net/http/visualizecontrolcodes_test.c
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||||
|
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||||
|
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||||
|
│ Copyright 2021 Justine Alexandra Roberts Tunney │
|
||||||
|
│ │
|
||||||
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||||
|
│ any purpose with or without fee is hereby granted, provided that the │
|
||||||
|
│ above copyright notice and this permission notice appear in all copies. │
|
||||||
|
│ │
|
||||||
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||||
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||||
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||||
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||||
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||||
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||||
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
|
#include "libc/testlib/ezbench.h"
|
||||||
|
#include "libc/testlib/hyperion.h"
|
||||||
|
#include "libc/testlib/testlib.h"
|
||||||
|
#include "net/http/http.h"
|
||||||
|
|
||||||
|
TEST(VisualizeControlCodes, test) {
|
||||||
|
EXPECT_STREQ("hello", VisualizeControlCodes("hello", -1, 0));
|
||||||
|
EXPECT_STREQ("hello\r\n", VisualizeControlCodes("hello\r\n", -1, 0));
|
||||||
|
EXPECT_STREQ("hello␁␂␡", VisualizeControlCodes("hello\1\2\177", -1, 0));
|
||||||
|
EXPECT_STREQ("hello\\u0085", VisualizeControlCodes("hello\302\205", -1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCH(VisualizeControlCodes, bench) {
|
||||||
|
EZBENCH2("VisualizeControlCodes", donothing,
|
||||||
|
free(VisualizeControlCodes(kHyperion, kHyperionSize, 0)));
|
||||||
|
}
|
|
@ -68,6 +68,7 @@ o/$(MODE)/tool/net/redbean.com.dbg: \
|
||||||
o/$(MODE)/tool/net/redbean.html.zip.o \
|
o/$(MODE)/tool/net/redbean.html.zip.o \
|
||||||
o/$(MODE)/tool/net/redbean.lua.zip.o \
|
o/$(MODE)/tool/net/redbean.lua.zip.o \
|
||||||
o/$(MODE)/tool/net/redbean-form.lua.zip.o \
|
o/$(MODE)/tool/net/redbean-form.lua.zip.o \
|
||||||
|
o/$(MODE)/tool/net/redbean-xhr.lua.zip.o \
|
||||||
o/$(MODE)/tool/net/.init.lua.zip.o \
|
o/$(MODE)/tool/net/.init.lua.zip.o \
|
||||||
o/$(MODE)/tool/net/.reload.lua.zip.o \
|
o/$(MODE)/tool/net/.reload.lua.zip.o \
|
||||||
o/$(MODE)/tool/net/net.pkg \
|
o/$(MODE)/tool/net/net.pkg \
|
||||||
|
|
2
tool/net/redbean-xhr.lua
Normal file
2
tool/net/redbean-xhr.lua
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
-- redbean xhr handler demo
|
||||||
|
SetHeader('X-Custom-Header', 'hello ' .. GetHeader('x-custom-header'))
|
|
@ -244,6 +244,8 @@ struct Parser {
|
||||||
int c;
|
int c;
|
||||||
const char *data;
|
const char *data;
|
||||||
int size;
|
int size;
|
||||||
|
bool isform;
|
||||||
|
bool islatin1;
|
||||||
char *p;
|
char *p;
|
||||||
char *q;
|
char *q;
|
||||||
};
|
};
|
||||||
|
@ -296,6 +298,7 @@ static bool heartless;
|
||||||
static bool printport;
|
static bool printport;
|
||||||
static bool heartbeat;
|
static bool heartbeat;
|
||||||
static bool daemonize;
|
static bool daemonize;
|
||||||
|
static bool logbodies;
|
||||||
static bool terminated;
|
static bool terminated;
|
||||||
static bool uniprocess;
|
static bool uniprocess;
|
||||||
static bool invalidated;
|
static bool invalidated;
|
||||||
|
@ -316,12 +319,12 @@ static uint32_t clientaddrsize;
|
||||||
|
|
||||||
static lua_State *L;
|
static lua_State *L;
|
||||||
static void *content;
|
static void *content;
|
||||||
static uint8_t *zmap;
|
|
||||||
static uint8_t *zdir;
|
static uint8_t *zdir;
|
||||||
|
static uint8_t *zmap;
|
||||||
static size_t hdrsize;
|
static size_t hdrsize;
|
||||||
static size_t msgsize;
|
static size_t msgsize;
|
||||||
static size_t amtread;
|
static size_t amtread;
|
||||||
static size_t zmapsize;
|
static size_t zsize;
|
||||||
static char *luaheaderp;
|
static char *luaheaderp;
|
||||||
static const char *pidpath;
|
static const char *pidpath;
|
||||||
static const char *logpath;
|
static const char *logpath;
|
||||||
|
@ -513,7 +516,7 @@ static wontreturn void PrintUsage(FILE *f, int rc) {
|
||||||
|
|
||||||
static void GetOpts(int argc, char *argv[]) {
|
static void GetOpts(int argc, char *argv[]) {
|
||||||
int opt;
|
int opt;
|
||||||
while ((opt = getopt(argc, argv, "zhduvml:p:w:r:c:L:P:U:G:B:")) != -1) {
|
while ((opt = getopt(argc, argv, "zhduvmbl:p:w:r:c:L:P:U:G:B:")) != -1) {
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
case 'v':
|
case 'v':
|
||||||
__log_level++;
|
__log_level++;
|
||||||
|
@ -527,6 +530,9 @@ static void GetOpts(int argc, char *argv[]) {
|
||||||
case 'm':
|
case 'm':
|
||||||
logmessages = true;
|
logmessages = true;
|
||||||
break;
|
break;
|
||||||
|
case 'b':
|
||||||
|
logbodies = true;
|
||||||
|
break;
|
||||||
case 'z':
|
case 'z':
|
||||||
printport = true;
|
printport = true;
|
||||||
break;
|
break;
|
||||||
|
@ -543,7 +549,7 @@ static void GetOpts(int argc, char *argv[]) {
|
||||||
CHECK_EQ(1, inet_pton(AF_INET, optarg, &serveraddr.sin_addr));
|
CHECK_EQ(1, inet_pton(AF_INET, optarg, &serveraddr.sin_addr));
|
||||||
break;
|
break;
|
||||||
case 'B':
|
case 'B':
|
||||||
serverheader = optarg;
|
serverheader = emptytonull(EncodeHttpHeaderValue(optarg, -1, 0));
|
||||||
break;
|
break;
|
||||||
case 'L':
|
case 'L':
|
||||||
logpath = optarg;
|
logpath = optarg;
|
||||||
|
@ -777,13 +783,18 @@ static void IndexAssets(void) {
|
||||||
|
|
||||||
static void OpenZip(const char *path) {
|
static void OpenZip(const char *path) {
|
||||||
int fd;
|
int fd;
|
||||||
|
uint8_t *p;
|
||||||
struct stat st;
|
struct stat st;
|
||||||
CHECK_NE(-1, (fd = open(path, O_RDONLY)));
|
CHECK_NE(-1, (fd = open(path, O_RDONLY)));
|
||||||
CHECK_NE(-1, fstat(fd, &st));
|
CHECK_NE(-1, fstat(fd, &st));
|
||||||
CHECK((zmapsize = st.st_size));
|
CHECK((zsize = st.st_size));
|
||||||
CHECK_NE(MAP_FAILED,
|
CHECK_NE(MAP_FAILED,
|
||||||
(zmap = mmap(NULL, zmapsize, PROT_READ, MAP_SHARED, fd, 0)));
|
(zmap = mmap(NULL, zsize, PROT_READ, MAP_SHARED, fd, 0)));
|
||||||
CHECK_NOTNULL((zdir = zipfindcentraldir(zmap, zmapsize)));
|
CHECK_NOTNULL((zdir = zipfindcentraldir(zmap, zsize)));
|
||||||
|
if (endswith(path, ".com.dbg") && (p = memmem(zmap, zsize, "MZqFpD", 6))) {
|
||||||
|
zsize -= p - zmap;
|
||||||
|
zmap = p;
|
||||||
|
}
|
||||||
close(fd);
|
close(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -856,6 +867,11 @@ static void EmitParamVal(struct Parser *u, struct Params *h, bool t) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ParseLatin1(struct Parser *u) {
|
||||||
|
*u->p++ = 0300 | u->c >> 6;
|
||||||
|
*u->p++ = 0200 | u->c & 077;
|
||||||
|
}
|
||||||
|
|
||||||
static void ParseEscape(struct Parser *u) {
|
static void ParseEscape(struct Parser *u) {
|
||||||
int a, b;
|
int a, b;
|
||||||
a = u->i < u->size ? u->data[u->i++] & 0xff : 0;
|
a = u->i < u->size ? u->data[u->i++] & 0xff : 0;
|
||||||
|
@ -868,10 +884,12 @@ static void ParsePath(struct Parser *u, struct Buffer *h) {
|
||||||
u->c = u->data[u->i++] & 0xff;
|
u->c = u->data[u->i++] & 0xff;
|
||||||
if (u->c == '#' || u->c == '?') {
|
if (u->c == '#' || u->c == '?') {
|
||||||
break;
|
break;
|
||||||
} else if (u->c != '%') {
|
} else if (u->c == '%') {
|
||||||
*u->p++ = u->c;
|
|
||||||
} else {
|
|
||||||
ParseEscape(u);
|
ParseEscape(u);
|
||||||
|
} else if (u->c >= 0200 && u->islatin1) {
|
||||||
|
ParseLatin1(u);
|
||||||
|
} else {
|
||||||
|
*u->p++ = u->c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h->p = u->q;
|
h->p = u->q;
|
||||||
|
@ -879,24 +897,20 @@ static void ParsePath(struct Parser *u, struct Buffer *h) {
|
||||||
u->q = u->p;
|
u->q = u->p;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ParseParams(struct Parser *u, struct Params *h, bool isform) {
|
static void ParseParams(struct Parser *u, struct Params *h) {
|
||||||
bool t = false;
|
bool t = false;
|
||||||
while (u->i < u->size) {
|
while (u->i < u->size) {
|
||||||
switch ((u->c = u->data[u->i++] & 0xff)) {
|
u->c = u->data[u->i++] & 0xff;
|
||||||
default:
|
if (u->c == '#') {
|
||||||
*u->p++ = u->c;
|
|
||||||
break;
|
break;
|
||||||
case '+':
|
} else if (u->c == '%') {
|
||||||
*u->p++ = isform ? ' ' : '+';
|
|
||||||
break;
|
|
||||||
case '%':
|
|
||||||
ParseEscape(u);
|
ParseEscape(u);
|
||||||
break;
|
} else if (u->c == '+') {
|
||||||
case '&':
|
*u->p++ = u->isform ? ' ' : '+';
|
||||||
|
} else if (u->c == '&') {
|
||||||
EmitParamVal(u, h, t);
|
EmitParamVal(u, h, t);
|
||||||
t = false;
|
t = false;
|
||||||
break;
|
} else if (u->c == '=') {
|
||||||
case '=':
|
|
||||||
if (!t) {
|
if (!t) {
|
||||||
if (u->p > u->q) {
|
if (u->p > u->q) {
|
||||||
EmitParamKey(u, h);
|
EmitParamKey(u, h);
|
||||||
|
@ -905,22 +919,24 @@ static void ParseParams(struct Parser *u, struct Params *h, bool isform) {
|
||||||
} else {
|
} else {
|
||||||
*u->p++ = '=';
|
*u->p++ = '=';
|
||||||
}
|
}
|
||||||
break;
|
} else if (u->c >= 0200 && u->islatin1) {
|
||||||
case '#':
|
ParseLatin1(u);
|
||||||
goto EndOfParams;
|
} else {
|
||||||
|
*u->p++ = u->c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EndOfParams:
|
|
||||||
EmitParamVal(u, h, t);
|
EmitParamVal(u, h, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ParseFragment(struct Parser *u, struct Buffer *h) {
|
static void ParseFragment(struct Parser *u, struct Buffer *h) {
|
||||||
while (u->i < u->size) {
|
while (u->i < u->size) {
|
||||||
u->c = u->data[u->i++] & 0xff;
|
u->c = u->data[u->i++] & 0xff;
|
||||||
if (u->c != '%') {
|
if (u->c == '%') {
|
||||||
*u->p++ = u->c;
|
|
||||||
} else {
|
|
||||||
ParseEscape(u);
|
ParseEscape(u);
|
||||||
|
} else if (u->c >= 0200 && u->islatin1) {
|
||||||
|
ParseLatin1(u);
|
||||||
|
} else {
|
||||||
|
*u->p++ = u->c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h->p = u->q;
|
h->p = u->q;
|
||||||
|
@ -936,13 +952,15 @@ static bool ParseRequestUri(void) {
|
||||||
struct Parser u;
|
struct Parser u;
|
||||||
u.i = 0;
|
u.i = 0;
|
||||||
u.c = '/';
|
u.c = '/';
|
||||||
|
u.isform = false;
|
||||||
|
u.islatin1 = true;
|
||||||
u.data = inbuf.p + msg.uri.a;
|
u.data = inbuf.p + msg.uri.a;
|
||||||
u.size = msg.uri.b - msg.uri.a;
|
u.size = msg.uri.b - msg.uri.a;
|
||||||
memset(&request, 0, sizeof(request));
|
memset(&request, 0, sizeof(request));
|
||||||
if (!u.size || *u.data != '/') return false;
|
if (!u.size || *u.data != '/') return false;
|
||||||
u.q = u.p = FreeLater(xmalloc(u.size));
|
u.q = u.p = FreeLater(xmalloc(u.size * 2));
|
||||||
if (u.c == '/') ParsePath(&u, &request.path);
|
if (u.c == '/') ParsePath(&u, &request.path);
|
||||||
if (u.c == '?') ParseParams(&u, &request.params, false);
|
if (u.c == '?') ParseParams(&u, &request.params);
|
||||||
if (u.c == '#') ParseFragment(&u, &request.fragment);
|
if (u.c == '#') ParseFragment(&u, &request.fragment);
|
||||||
return u.i == u.size && !IsForbiddenPath(&request.path);
|
return u.i == u.size && !IsForbiddenPath(&request.path);
|
||||||
}
|
}
|
||||||
|
@ -951,18 +969,20 @@ static void ParseFormParams(void) {
|
||||||
struct Parser u;
|
struct Parser u;
|
||||||
u.i = 0;
|
u.i = 0;
|
||||||
u.c = 0;
|
u.c = 0;
|
||||||
|
u.isform = true;
|
||||||
|
u.islatin1 = false;
|
||||||
u.data = inbuf.p + hdrsize;
|
u.data = inbuf.p + hdrsize;
|
||||||
u.size = msgsize - hdrsize;
|
u.size = msgsize - hdrsize;
|
||||||
u.q = u.p = FreeLater(xmalloc(u.size));
|
u.q = u.p = FreeLater(xmalloc(u.size));
|
||||||
ParseParams(&u, &request.params, true);
|
ParseParams(&u, &request.params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void *AddRange(char *content, long start, long length) {
|
static void *AddRange(char *content, long start, long length) {
|
||||||
intptr_t mend, mstart;
|
intptr_t mend, mstart;
|
||||||
if (!__builtin_add_overflow((intptr_t)content, start, &mstart) ||
|
if (!__builtin_add_overflow((intptr_t)content, start, &mstart) ||
|
||||||
!__builtin_add_overflow(mstart, length, &mend) ||
|
!__builtin_add_overflow(mstart, length, &mend) ||
|
||||||
((intptr_t)zmap <= mstart && mstart <= (intptr_t)zmap + zmapsize) ||
|
((intptr_t)zmap <= mstart && mstart <= (intptr_t)zmap + zsize) ||
|
||||||
((intptr_t)zmap <= mend && mend <= (intptr_t)zmap + zmapsize)) {
|
((intptr_t)zmap <= mend && mend <= (intptr_t)zmap + zsize)) {
|
||||||
return (void *)mstart;
|
return (void *)mstart;
|
||||||
} else {
|
} else {
|
||||||
abort();
|
abort();
|
||||||
|
@ -1002,6 +1022,7 @@ static char *SetStatus(int code, const char *reason) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static char *AppendHeader(char *p, const char *k, const char *v) {
|
static char *AppendHeader(char *p, const char *k, const char *v) {
|
||||||
|
if (!v) return p;
|
||||||
return AppendCrlf(stpcpy(AppendHeaderName(p, k), v));
|
return AppendCrlf(stpcpy(AppendHeaderName(p, k), v));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1213,43 +1234,6 @@ static char *ServeAsset(struct Asset *a, const char *path, size_t pathlen) {
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool IsValidFieldName(const char *s, size_t n) {
|
|
||||||
size_t i;
|
|
||||||
if (!n) return false;
|
|
||||||
for (i = 0; i < n; ++i) {
|
|
||||||
if (!(0x20 < s[i] && s[i] < 0x7F) || s[i] == ':') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool IsValidFieldContent(const char *s, size_t n) {
|
|
||||||
size_t i;
|
|
||||||
for (i = 0; i < n; ++i) {
|
|
||||||
if (!(0x20 <= s[i] && s[i] < 0x7F)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int LuaIsValidFieldName(lua_State *L) {
|
|
||||||
size_t size;
|
|
||||||
const char *data;
|
|
||||||
data = luaL_checklstring(L, 1, &size);
|
|
||||||
lua_pushboolean(L, IsValidFieldName(data, size));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int LuaIsValidFieldContent(lua_State *L) {
|
|
||||||
size_t size;
|
|
||||||
const char *data;
|
|
||||||
data = luaL_checklstring(L, 1, &size);
|
|
||||||
lua_pushboolean(L, IsValidFieldContent(data, size));
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int LuaServeAsset(lua_State *L) {
|
static int LuaServeAsset(lua_State *L) {
|
||||||
size_t pathlen;
|
size_t pathlen;
|
||||||
struct Asset *a;
|
struct Asset *a;
|
||||||
|
@ -1263,6 +1247,7 @@ static int LuaServeAsset(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static int LuaRespond(lua_State *L, char *respond(int, const char *)) {
|
static int LuaRespond(lua_State *L, char *respond(int, const char *)) {
|
||||||
|
char *p;
|
||||||
int code;
|
int code;
|
||||||
size_t reasonlen;
|
size_t reasonlen;
|
||||||
const char *reason;
|
const char *reason;
|
||||||
|
@ -1271,14 +1256,16 @@ static int LuaRespond(lua_State *L, char *respond(int, const char *)) {
|
||||||
return luaL_argerror(L, 1, "bad status code");
|
return luaL_argerror(L, 1, "bad status code");
|
||||||
}
|
}
|
||||||
if (lua_isnoneornil(L, 2)) {
|
if (lua_isnoneornil(L, 2)) {
|
||||||
reason = GetHttpReason(code);
|
luaheaderp = respond(code, GetHttpReason(code));
|
||||||
} else {
|
} else {
|
||||||
reason = lua_tolstring(L, 2, &reasonlen);
|
reason = lua_tolstring(L, 2, &reasonlen);
|
||||||
if (reasonlen > 128 || !IsValidFieldContent(reason, reasonlen)) {
|
if (reasonlen < 128 && (p = EncodeHttpHeaderValue(reason, reasonlen, 0))) {
|
||||||
|
luaheaderp = respond(code, p);
|
||||||
|
free(p);
|
||||||
|
} else {
|
||||||
return luaL_argerror(L, 2, "invalid");
|
return luaL_argerror(L, 2, "invalid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
luaheaderp = respond(code, reason);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1368,21 +1355,28 @@ static int LuaGetPayload(lua_State *L) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void LuaPushLatin1(lua_State *L, const char *s, size_t n) {
|
||||||
|
char *t;
|
||||||
|
size_t m;
|
||||||
|
t = DecodeLatin1(s, n, &m);
|
||||||
|
lua_pushlstring(L, t, m);
|
||||||
|
free(t);
|
||||||
|
}
|
||||||
|
|
||||||
static int LuaGetHeader(lua_State *L) {
|
static int LuaGetHeader(lua_State *L) {
|
||||||
int h;
|
int h;
|
||||||
const char *key, *val;
|
const char *key;
|
||||||
size_t i, keylen, vallen;
|
size_t i, keylen;
|
||||||
key = luaL_checklstring(L, 1, &keylen);
|
key = luaL_checklstring(L, 1, &keylen);
|
||||||
if ((h = GetHttpHeader(key, keylen)) != -1) {
|
if ((h = GetHttpHeader(key, keylen)) != -1) {
|
||||||
val = inbuf.p + msg.headers[h].a;
|
LuaPushLatin1(L, inbuf.p + msg.headers[h].a,
|
||||||
vallen = msg.headers[h].b - msg.headers[h].a;
|
msg.headers[h].b - msg.headers[h].a);
|
||||||
lua_pushlstring(L, val, vallen);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
for (i = 0; i < msg.xheaders.n; ++i) {
|
for (i = 0; i < msg.xheaders.n; ++i) {
|
||||||
if (!CompareSlicesCase(key, keylen, inbuf.p + msg.xheaders.p[i].k.a,
|
if (!CompareSlicesCase(key, keylen, inbuf.p + msg.xheaders.p[i].k.a,
|
||||||
msg.xheaders.p[i].k.b - msg.xheaders.p[i].k.a)) {
|
msg.xheaders.p[i].k.b - msg.xheaders.p[i].k.a)) {
|
||||||
lua_pushlstring(L, inbuf.p + msg.xheaders.p[i].v.a,
|
LuaPushLatin1(L, inbuf.p + msg.xheaders.p[i].v.a,
|
||||||
msg.xheaders.p[i].v.b - msg.xheaders.p[i].v.a);
|
msg.xheaders.p[i].v.b - msg.xheaders.p[i].v.a);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
@ -1393,42 +1387,47 @@ static int LuaGetHeader(lua_State *L) {
|
||||||
|
|
||||||
static int LuaGetHeaders(lua_State *L) {
|
static int LuaGetHeaders(lua_State *L) {
|
||||||
size_t i;
|
size_t i;
|
||||||
|
char *name;
|
||||||
lua_newtable(L);
|
lua_newtable(L);
|
||||||
for (i = 0; i < kHttpHeadersMax; ++i) {
|
for (i = 0; i < kHttpHeadersMax; ++i) {
|
||||||
if (msg.headers[i].b - msg.headers[i].a) {
|
if (msg.headers[i].b - msg.headers[i].a) {
|
||||||
lua_pushlstring(L, inbuf.p + msg.headers[i].a,
|
LuaPushLatin1(L, inbuf.p + msg.headers[i].a,
|
||||||
msg.headers[i].b - msg.headers[i].a);
|
msg.headers[i].b - msg.headers[i].a);
|
||||||
lua_setfield(L, -2, GetHttpHeaderName(i));
|
lua_setfield(L, -2, GetHttpHeaderName(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (i = 0; i < msg.xheaders.n; ++i) {
|
for (i = 0; i < msg.xheaders.n; ++i) {
|
||||||
lua_pushlstring(L, inbuf.p + msg.xheaders.p[i].v.a,
|
LuaPushLatin1(L, inbuf.p + msg.xheaders.p[i].v.a,
|
||||||
msg.xheaders.p[i].v.b - msg.xheaders.p[i].v.a);
|
msg.xheaders.p[i].v.b - msg.xheaders.p[i].v.a);
|
||||||
lua_setfield(
|
lua_setfield(L, -2,
|
||||||
L, -2,
|
(name = DecodeLatin1(
|
||||||
FreeLater(strndup(inbuf.p + msg.xheaders.p[i].k.a,
|
inbuf.p + msg.xheaders.p[i].k.a,
|
||||||
msg.xheaders.p[i].k.b - msg.xheaders.p[i].k.a)));
|
msg.xheaders.p[i].k.b - msg.xheaders.p[i].k.a, 0)));
|
||||||
|
free(name);
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int LuaSetHeader(lua_State *L) {
|
static int LuaSetHeader(lua_State *L) {
|
||||||
|
int h;
|
||||||
char *p;
|
char *p;
|
||||||
ssize_t rc;
|
ssize_t rc;
|
||||||
const char *key, *val;
|
const char *key, *val, *eval;
|
||||||
size_t i, keylen, vallen;
|
size_t i, keylen, vallen, evallen;
|
||||||
key = luaL_checklstring(L, 1, &keylen);
|
key = luaL_checklstring(L, 1, &keylen);
|
||||||
val = luaL_checklstring(L, 2, &vallen);
|
val = luaL_checklstring(L, 2, &vallen);
|
||||||
if (!IsValidFieldName(key, keylen)) {
|
if ((h = GetHttpHeader(key, keylen)) == -1) {
|
||||||
|
if (!IsValidHttpToken(key, keylen)) {
|
||||||
return luaL_argerror(L, 1, "invalid");
|
return luaL_argerror(L, 1, "invalid");
|
||||||
}
|
}
|
||||||
if (!IsValidFieldContent(val, vallen)) {
|
}
|
||||||
|
if (!(eval = EncodeHttpHeaderValue(val, vallen, &evallen))) {
|
||||||
return luaL_argerror(L, 2, "invalid");
|
return luaL_argerror(L, 2, "invalid");
|
||||||
}
|
}
|
||||||
if (!luaheaderp) {
|
if (!luaheaderp) {
|
||||||
p = SetStatus(200, "OK");
|
p = SetStatus(200, "OK");
|
||||||
} else {
|
} else {
|
||||||
while (luaheaderp - hdrbuf.p + keylen + 2 + vallen + 2 + 512 > hdrbuf.n) {
|
while (luaheaderp - hdrbuf.p + keylen + 2 + evallen + 2 + 512 > hdrbuf.n) {
|
||||||
hdrbuf.n += hdrbuf.n >> 1;
|
hdrbuf.n += hdrbuf.n >> 1;
|
||||||
p = xrealloc(hdrbuf.p, hdrbuf.n);
|
p = xrealloc(hdrbuf.p, hdrbuf.n);
|
||||||
luaheaderp = p + (luaheaderp - hdrbuf.p);
|
luaheaderp = p + (luaheaderp - hdrbuf.p);
|
||||||
|
@ -1436,30 +1435,31 @@ static int LuaSetHeader(lua_State *L) {
|
||||||
}
|
}
|
||||||
p = luaheaderp;
|
p = luaheaderp;
|
||||||
}
|
}
|
||||||
switch (GetHttpHeader(key, keylen)) {
|
switch (h) {
|
||||||
case kHttpDate:
|
case kHttpDate:
|
||||||
case kHttpContentRange:
|
case kHttpContentRange:
|
||||||
case kHttpContentLength:
|
case kHttpContentLength:
|
||||||
case kHttpContentEncoding:
|
case kHttpContentEncoding:
|
||||||
return luaL_argerror(L, 1, "abstracted");
|
return luaL_argerror(L, 1, "abstracted");
|
||||||
case kHttpConnection:
|
case kHttpConnection:
|
||||||
if (vallen != 5 || memcmp(val, "close", 5)) {
|
if (evallen != 5 || memcmp(eval, "close", 5)) {
|
||||||
return luaL_argerror(L, 2, "unsupported");
|
return luaL_argerror(L, 2, "unsupported");
|
||||||
}
|
}
|
||||||
connectionclose = true;
|
connectionclose = true;
|
||||||
break;
|
break;
|
||||||
case kHttpContentType:
|
case kHttpContentType:
|
||||||
p = AppendContentType(p, val);
|
p = AppendContentType(p, eval);
|
||||||
break;
|
break;
|
||||||
case kHttpServer:
|
case kHttpServer:
|
||||||
branded = true;
|
branded = true;
|
||||||
p = AppendHeader(p, key, val);
|
p = AppendHeader(p, "Server", eval);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
p = AppendHeader(p, key, val);
|
p = AppendHeader(p, key, eval);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
luaheaderp = p;
|
luaheaderp = p;
|
||||||
|
free(eval);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1579,6 +1579,17 @@ static int LuaDecodeBase64(lua_State *L) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int LuaVisualizeControlCodes(lua_State *L) {
|
||||||
|
char *p;
|
||||||
|
size_t size, n;
|
||||||
|
const char *data;
|
||||||
|
data = luaL_checklstring(L, 1, &size);
|
||||||
|
p = VisualizeControlCodes(data, size, &n);
|
||||||
|
lua_pushlstring(L, p, n);
|
||||||
|
free(p);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static int LuaPopcnt(lua_State *L) {
|
static int LuaPopcnt(lua_State *L) {
|
||||||
lua_pushinteger(L, popcnt(luaL_checkinteger(L, 1)));
|
lua_pushinteger(L, popcnt(luaL_checkinteger(L, 1)));
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -1642,8 +1653,6 @@ static const luaL_Reg kLuaFuncs[] = {
|
||||||
{"GetUri", LuaGetUri}, //
|
{"GetUri", LuaGetUri}, //
|
||||||
{"GetVersion", LuaGetVersion}, //
|
{"GetVersion", LuaGetVersion}, //
|
||||||
{"HasParam", LuaHasParam}, //
|
{"HasParam", LuaHasParam}, //
|
||||||
{"IsValidFieldContent", LuaIsValidFieldContent}, //
|
|
||||||
{"IsValidFieldName", LuaIsValidFieldName}, //
|
|
||||||
{"LoadAsset", LuaLoadAsset}, //
|
{"LoadAsset", LuaLoadAsset}, //
|
||||||
{"ParseDate", LuaParseDate}, //
|
{"ParseDate", LuaParseDate}, //
|
||||||
{"ServeAsset", LuaServeAsset}, //
|
{"ServeAsset", LuaServeAsset}, //
|
||||||
|
@ -1748,15 +1757,35 @@ static char *HandleRedirect(struct Redirect *r) {
|
||||||
kHttpMethod[msg.method], request.path.n, request.path.p,
|
kHttpMethod[msg.method], request.path.n, request.path.p,
|
||||||
r->location);
|
r->location);
|
||||||
p = SetStatus(307, "Temporary Redirect");
|
p = SetStatus(307, "Temporary Redirect");
|
||||||
p = AppendHeader(p, "Location", r->location);
|
p = AppendHeader(p, "Location",
|
||||||
|
FreeLater(EncodeHttpHeaderValue(r->location, -1, 0)));
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void LogMessage(const char *d, const char *s, size_t n) {
|
static void LogMessage(const char *d, const char *s, size_t n) {
|
||||||
|
char *s2, *s3;
|
||||||
|
size_t n2, n3;
|
||||||
if (!logmessages) return;
|
if (!logmessages) return;
|
||||||
while (n && (s[n - 1] == '\r' || s[n - 1] == '\n')) --n;
|
while (n && (s[n - 1] == '\r' || s[n - 1] == '\n')) --n;
|
||||||
LOGF("%s %s %,ld byte message\n%.*s", clientaddrstr, d, n, n, s);
|
if ((s2 = DecodeLatin1(s, n, &n2))) {
|
||||||
|
if ((s3 = VisualizeControlCodes(s2, n2, &n3))) {
|
||||||
|
LOGF("%s %s %,ld byte message\n%.*s", clientaddrstr, d, n, n3, s3);
|
||||||
|
free(s3);
|
||||||
|
}
|
||||||
|
free(s2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void LogBody(const char *d, const char *s, size_t n) {
|
||||||
|
char *s2;
|
||||||
|
size_t n2;
|
||||||
|
if (!n || !logbodies) return;
|
||||||
|
while (n && (s[n - 1] == '\r' || s[n - 1] == '\n')) --n;
|
||||||
|
if ((s2 = VisualizeControlCodes(s, n, &n2))) {
|
||||||
|
LOGF("%s %s %,ld byte payload\n%.*s", clientaddrstr, d, n, n2, s2);
|
||||||
|
free(s2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static ssize_t SendMessageString(const char *s) {
|
static ssize_t SendMessageString(const char *s) {
|
||||||
|
@ -1872,6 +1901,7 @@ static char *HandleMessage(void) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
msgsize = need; /* we are now synchronized */
|
msgsize = need; /* we are now synchronized */
|
||||||
|
LogBody("received", inbuf.p + hdrsize, msgsize - hdrsize);
|
||||||
if (httpversion != 101 || IsConnectionClose()) {
|
if (httpversion != 101 || IsConnectionClose()) {
|
||||||
connectionclose = true;
|
connectionclose = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,16 +58,38 @@ local function main()
|
||||||
Write('<p><em>none</em>\n')
|
Write('<p><em>none</em>\n')
|
||||||
end
|
end
|
||||||
|
|
||||||
Write('<h3>post request html form demo</h3>\n')
|
Write([[
|
||||||
Write('<form action="/tool/net/redbean-form.lua" method="post">\n')
|
<h3>post request html form demo</h3>
|
||||||
Write('<input type="text" id="firstname" name="firstname">\n')
|
<form action="/tool/net/redbean-form.lua" method="post">
|
||||||
Write('<label for="firstname">first name</label>\n')
|
<input type="text" id="firstname" name="firstname">
|
||||||
Write('<br>\n')
|
<label for="firstname">first name</label>
|
||||||
Write('<input type="text" id="lastname" name="lastname">\n')
|
<br>
|
||||||
Write('<label for="lastname">last name</label>\n')
|
<input type="text" id="lastname" name="lastname">
|
||||||
Write('<br>\n')
|
<label for="lastname">last name</label>
|
||||||
Write('<input type="submit" value="Submit">\n')
|
<br>
|
||||||
Write('</form>\n')
|
<input type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
]])
|
||||||
|
|
||||||
|
Write([[
|
||||||
|
<h3>xmlhttprequest request demo</h3>
|
||||||
|
<input id="x" value="lâtìn1">
|
||||||
|
<label for="x">name</label><br>
|
||||||
|
<button id="send">send (via http header)</button><br>
|
||||||
|
<div id="result"></div>
|
||||||
|
<script>
|
||||||
|
function OnSend() {
|
||||||
|
var r = new XMLHttpRequest();
|
||||||
|
r.onload = function() {
|
||||||
|
document.getElementById("result").innerText = this.getResponseHeader('X-Custom-Header');
|
||||||
|
};
|
||||||
|
r.open('POST', '/tool/net/redbean-xhr.lua');
|
||||||
|
r.setRequestHeader('X-Custom-Header', document.getElementById('x').value);
|
||||||
|
r.send();
|
||||||
|
}
|
||||||
|
document.getElementById('send').onclick = OnSend;
|
||||||
|
</script>
|
||||||
|
]])
|
||||||
end
|
end
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
Loading…
Add table
Reference in a new issue