mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-30 22:13:49 +00:00
Parse the config file on arenas
read_jconf now reads the file and builds the JSON tree in a scratch arena that is released before it returns, and copies the parsed strings into one process-lifetime arena instead of a malloc per string. parse_addr and the config parser share one address splitter. Port test_jconf to C++ and cover every field copied out of the parse tree, a re-read, and the densest file the size limit accepts.
This commit is contained in:
+8
-2
@@ -113,7 +113,12 @@ Pools are reserved at startup and only touched slots become resident.
|
||||
3. Event layer over libuv (`src/event/`).
|
||||
4. Crypto codecs held by value: AEAD, stream and AEAD-2022.
|
||||
5. Services: the cache on a fixed slab, `IntrusiveList` for ACL rules and
|
||||
the manager, the pooled resolver.
|
||||
the manager, the pooled resolver, and `read_jconf` on arenas (the file and
|
||||
JSON tree in a scratch arena released before it returns, the parsed
|
||||
strings in one process-lifetime arena). ACL rules and IP-set nodes still
|
||||
come from `malloc` at startup: the IP-set trie frees nodes as it merges
|
||||
prefixes, and its size depends on the ACL file, so a bump arena would
|
||||
have to reserve for the worst case.
|
||||
6. One TCP engine for ss-tunnel, ss-local, ss-redir and ss-server.
|
||||
7. One UDP engine.
|
||||
8. Library heaps for libuv, c-ares and Mbed TLS, `heap_seal()`, and the
|
||||
@@ -122,7 +127,8 @@ Pools are reserved at startup and only touched slots become resident.
|
||||
(`ss_local_start`/`stop`/`wait`/`destroy`) and the RAII
|
||||
`shadowsocks/shadowsocks.hpp`.
|
||||
10. Removal of `ss_event`, the C resolver, the C codecs (now test oracles),
|
||||
uthash and `ss_list`.
|
||||
uthash and `ss_list`, and the unit tests ported to C++
|
||||
(`tests/test_helpers.hpp`).
|
||||
|
||||
## Allocation audit
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# violations, and also when it loses some, so the number here always goes
|
||||
# down. Remove a file once it is rewritten on the src/core primitives.
|
||||
src/ipset.cpp 3
|
||||
src/jconf.cpp 16
|
||||
src/jconf.cpp 6
|
||||
src/json.cpp 2
|
||||
src/local.cpp 1
|
||||
src/manager.cpp 14
|
||||
|
||||
+135
-73
@@ -34,6 +34,11 @@
|
||||
#include "string.h"
|
||||
|
||||
#include "core.h"
|
||||
#include "core/arena.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <string_view>
|
||||
|
||||
#define check_json_value_type(value, expected_type, message) \
|
||||
do { \
|
||||
@@ -41,15 +46,115 @@
|
||||
FATAL((message)); \
|
||||
} while (0)
|
||||
|
||||
/*
|
||||
* The parsed configuration lives as long as the process, so its strings come
|
||||
* from one arena reserved when the file is read. The file text and the JSON
|
||||
* tree use a second, scratch arena that is released before read_jconf
|
||||
* returns. Nothing here goes back to malloc per string.
|
||||
*/
|
||||
namespace {
|
||||
|
||||
alignas(ss::MonotonicArena) unsigned char conf_arena_storage[sizeof(ss::MonotonicArena)];
|
||||
ss::MonotonicArena *conf_arena;
|
||||
|
||||
// The arena is never destroyed: callers keep jconf_t pointers until exit.
|
||||
ss::MonotonicArena &
|
||||
config_arena(std::size_t bytes)
|
||||
{
|
||||
if (conf_arena == nullptr) {
|
||||
conf_arena = ::new (conf_arena_storage) ss::MonotonicArena(); // heap-ok: placement into static storage
|
||||
} else {
|
||||
// A second read_jconf replaces the previous result (conf is static).
|
||||
*conf_arena = ss::MonotonicArena();
|
||||
}
|
||||
if (!conf_arena->reserve(bytes)) {
|
||||
FATAL("No enough memory.");
|
||||
}
|
||||
return *conf_arena;
|
||||
}
|
||||
|
||||
char *
|
||||
conf_strndup(std::string_view text)
|
||||
{
|
||||
auto copy = conf_arena->intern(text);
|
||||
if (!copy) {
|
||||
FATAL("No enough memory.");
|
||||
}
|
||||
return const_cast<char *>(copy->data());
|
||||
}
|
||||
|
||||
// json-parser's allocator hooks. The scratch Region is calloc'ed and never
|
||||
// reused within one parse, so `zero` already holds.
|
||||
void *
|
||||
scratch_alloc(size_t size, int, void *user)
|
||||
{
|
||||
auto bytes = static_cast<ss::MonotonicArena *>(user)->allocate(size);
|
||||
return bytes ? bytes->data() : nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
scratch_free(void *, void *)
|
||||
{
|
||||
}
|
||||
|
||||
struct AddrParts {
|
||||
std::string_view host;
|
||||
std::string_view port; // empty when there is none
|
||||
};
|
||||
|
||||
// Split "host", "host:port", "[v6]" or "[v6]:port". IPv6 literals without
|
||||
// brackets and anything else with several colons stay whole.
|
||||
AddrParts
|
||||
split_addr(const char *str)
|
||||
{
|
||||
// Plain pointer arithmetic: string_view::substr can throw out_of_range,
|
||||
// which would pull in the C++ runtime.
|
||||
std::string_view text(str);
|
||||
auto slice = [&](std::size_t from, std::size_t to) { return std::string_view(str + from, to - from); };
|
||||
struct ss_ip ip;
|
||||
if (ss_ip_init(&ip, str) != -1) {
|
||||
return { text, {} };
|
||||
}
|
||||
if (text.front() == '[') {
|
||||
auto end = text.find(']');
|
||||
if (end == std::string_view::npos) {
|
||||
return { text, {} };
|
||||
}
|
||||
AddrParts parts{ slice(1, end), {} };
|
||||
if (end + 1 < text.size() && text[end + 1] == ':') {
|
||||
parts.port = slice(end + 2, text.size());
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
auto colon = text.find(':');
|
||||
if (colon == std::string_view::npos || text.find(':', colon + 1) != std::string_view::npos) {
|
||||
return { text, {} };
|
||||
}
|
||||
return { slice(0, colon), slice(colon + 1, text.size()) };
|
||||
}
|
||||
|
||||
void
|
||||
parse_conf_addr(const char *str, ss_addr_t *addr)
|
||||
{
|
||||
if (str == NULL) {
|
||||
return;
|
||||
}
|
||||
AddrParts parts = *str == '\0' ? AddrParts{} : split_addr(str);
|
||||
addr->host = conf_strndup(parts.host);
|
||||
addr->port = parts.port.empty() ? NULL : conf_strndup(parts.port);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static char *
|
||||
to_string(const json_value *value)
|
||||
{
|
||||
if (value->type == json_string) {
|
||||
return ss_strndup(value->u.string.ptr, value->u.string.length);
|
||||
return conf_strndup(std::string_view(value->u.string.ptr, value->u.string.length));
|
||||
} else if (value->type == json_integer) {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%" PRId64, value->u.integer);
|
||||
return strdup(buf);
|
||||
return conf_strndup(buf);
|
||||
} else if (value->type == json_null) {
|
||||
return NULL;
|
||||
} else {
|
||||
@@ -83,56 +188,14 @@ parse_addr(const char *str_in, ss_addr_t *addr)
|
||||
{
|
||||
if (str_in == NULL)
|
||||
return;
|
||||
|
||||
int ret = -1, n = 0, len;
|
||||
char *str = strdup(str_in);
|
||||
len = strlen(str_in);
|
||||
|
||||
struct ss_ip ip;
|
||||
if (ss_ip_init(&ip, str) != -1) {
|
||||
addr->host = str;
|
||||
if (*str_in == '\0') {
|
||||
addr->host = strdup(str_in);
|
||||
addr->port = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (str[0] == '[') {
|
||||
char *end = strchr(str, ']');
|
||||
if (end != NULL) {
|
||||
addr->host = ss_strndup(str + 1, end - str - 1);
|
||||
if (end[1] == ':' && end[2] != '\0') {
|
||||
addr->port = strdup(end + 2);
|
||||
} else {
|
||||
addr->port = NULL;
|
||||
}
|
||||
free(str);
|
||||
return;
|
||||
}
|
||||
|
||||
addr->host = str;
|
||||
addr->port = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
char *pch = strchr(str, ':');
|
||||
while (pch != NULL) {
|
||||
n++;
|
||||
ret = pch - str;
|
||||
pch = strchr(pch + 1, ':');
|
||||
}
|
||||
|
||||
if (ret == -1 || n != 1) {
|
||||
addr->host = strdup(str);
|
||||
addr->port = NULL;
|
||||
} else {
|
||||
addr->host = ss_strndup(str, ret);
|
||||
if (ret < len - 1) {
|
||||
addr->port = strdup(str + ret + 1);
|
||||
} else {
|
||||
addr->port = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
free(str);
|
||||
AddrParts parts = split_addr(str_in);
|
||||
addr->host = ss_strndup(parts.host.data(), parts.host.size());
|
||||
addr->port = parts.port.empty() ? NULL : ss_strndup(parts.port.data(), parts.port.size());
|
||||
}
|
||||
|
||||
/* CLI endpoints are strict; keep the legacy JSON address parser unchanged. */
|
||||
@@ -263,7 +326,6 @@ read_jconf(const char *file)
|
||||
|
||||
memset(&conf, 0, sizeof(jconf_t));
|
||||
|
||||
char *buf;
|
||||
json_value *obj;
|
||||
|
||||
FILE *f = fopen(file, "rb");
|
||||
@@ -283,22 +345,31 @@ read_jconf(const char *file)
|
||||
FATAL("Too large config file.");
|
||||
}
|
||||
|
||||
buf = (char *)ss_malloc(pos + 1);
|
||||
if (buf == NULL) {
|
||||
// Every string kept in conf copies part of the file plus a NUL: a JSON
|
||||
// string or key, an integer printed back in decimal, or the two halves of
|
||||
// an address (whose unsplit copy is kept too). 16x the file size is a
|
||||
// comfortable bound.
|
||||
std::size_t len = static_cast<std::size_t>(pos);
|
||||
config_arena(16 * len + 4096);
|
||||
|
||||
// json-parser needs a json_value (and an array or object slot) for every
|
||||
// value, each costing at least one byte of input.
|
||||
ss::MonotonicArena scratch;
|
||||
if (!scratch.reserve(len + 1 + 128 * (len + 1))) {
|
||||
FATAL("No enough memory.");
|
||||
}
|
||||
char *buf = reinterpret_cast<char *>(scratch.allocate(len + 1, 1)->data());
|
||||
|
||||
int nread = fread(buf, pos, 1, f);
|
||||
if (!nread) {
|
||||
if (len > 0 && fread(buf, len, 1, f) != 1) {
|
||||
FATAL("Failed to read the config file.");
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
buf[pos] = '\0'; // end of string
|
||||
buf[len] = '\0'; // end of string
|
||||
|
||||
json_settings settings = { 0UL, 0, NULL, NULL, NULL };
|
||||
json_settings settings = { 0UL, 0, scratch_alloc, scratch_free, &scratch, 0 };
|
||||
char error_buf[512];
|
||||
obj = json_parse_ex(&settings, buf, pos, error_buf);
|
||||
obj = json_parse_ex(&settings, buf, len, error_buf);
|
||||
|
||||
if (obj == NULL) {
|
||||
FATAL(error_buf);
|
||||
@@ -316,15 +387,11 @@ read_jconf(const char *file)
|
||||
break;
|
||||
}
|
||||
json_value *v = value->u.array.values[j];
|
||||
char *addr_str = to_string(v);
|
||||
parse_addr(addr_str, conf.remote_addr + j);
|
||||
ss_free(addr_str);
|
||||
parse_conf_addr(to_string(v), conf.remote_addr + j);
|
||||
conf.remote_num = j + 1;
|
||||
}
|
||||
} else if (value->type == json_string) {
|
||||
char* tmp_str = to_string(value);
|
||||
parse_addr(tmp_str, conf.remote_addr);
|
||||
ss_free(tmp_str);
|
||||
parse_conf_addr(to_string(value), conf.remote_addr);
|
||||
conf.remote_num = 1;
|
||||
}
|
||||
} else if (strcmp(name, "port_password") == 0) {
|
||||
@@ -335,8 +402,8 @@ read_jconf(const char *file)
|
||||
}
|
||||
json_value *v = value->u.object.values[j].value;
|
||||
if (v->type == json_string) {
|
||||
conf.port_password[j].port = ss_strndup(value->u.object.values[j].name,
|
||||
value->u.object.values[j].name_length);
|
||||
conf.port_password[j].port = conf_strndup(
|
||||
std::string_view(value->u.object.values[j].name, value->u.object.values[j].name_length));
|
||||
conf.port_password[j].password = to_string(v);
|
||||
conf.port_password_num = j + 1;
|
||||
}
|
||||
@@ -364,9 +431,7 @@ read_jconf(const char *file)
|
||||
conf.user = to_string(value);
|
||||
} else if (strcmp(name, "plugin") == 0) {
|
||||
conf.plugin = to_string(value);
|
||||
// NOLINTNEXTLINE(clang-analyzer-unix.Malloc): config strings live for the process lifetime
|
||||
if (conf.plugin && strlen(conf.plugin) == 0) {
|
||||
ss_free(conf.plugin);
|
||||
conf.plugin = NULL;
|
||||
}
|
||||
} else if (strcmp(name, "plugin_opts") == 0) {
|
||||
@@ -409,12 +474,11 @@ read_jconf(const char *file)
|
||||
if (v->type == json_string) {
|
||||
char *dscp_str = to_string(v);
|
||||
int dscp = parse_dscp(dscp_str);
|
||||
char *port = ss_strndup(value->u.object.values[j].name,
|
||||
value->u.object.values[j].name_length);
|
||||
char *port = conf_strndup(std::string_view(value->u.object.values[j].name,
|
||||
value->u.object.values[j].name_length));
|
||||
conf.dscp[j].port = port;
|
||||
conf.dscp[j].dscp = dscp;
|
||||
conf.dscp_num = j + 1;
|
||||
ss_free(dscp_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,7 +499,6 @@ read_jconf(const char *file)
|
||||
LOGI("ignore unknown mode: %s, use tcp_only as fallback",
|
||||
mode_str);
|
||||
|
||||
ss_free(mode_str);
|
||||
} else if (strcmp(name, "mtu") == 0) {
|
||||
conf.mtu = to_nonnegative_int(value, name);
|
||||
} else if (strcmp(name, "mptcp") == 0) {
|
||||
@@ -479,7 +542,6 @@ read_jconf(const char *file)
|
||||
FATAL("Invalid config file");
|
||||
}
|
||||
|
||||
ss_free(buf);
|
||||
json_value_free(obj);
|
||||
// The tree and the file text go with the scratch arena.
|
||||
return &conf;
|
||||
}
|
||||
|
||||
+216
-177
@@ -1,253 +1,292 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
// Configuration parsing: the address splitters and read_jconf, whose result
|
||||
// lives in the configuration arena.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "test_helpers.h"
|
||||
#ifndef _WIN32
|
||||
#include <sys/wait.h>
|
||||
#endif
|
||||
|
||||
extern "C" int verbose;
|
||||
int verbose = 0;
|
||||
|
||||
#include "netutils.h"
|
||||
#include "netutils.h" // defines ss_addr_t, which jconf.h uses
|
||||
#include "jconf.h"
|
||||
|
||||
static void
|
||||
test_parse_addr_ipv4_with_port(void)
|
||||
namespace {
|
||||
|
||||
bool
|
||||
same(const char *a, const char *b)
|
||||
{
|
||||
ss_addr_t addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
parse_addr("192.168.1.1:8080", &addr);
|
||||
assert(addr.host != NULL);
|
||||
assert(addr.port != NULL);
|
||||
assert(strcmp(addr.host, "192.168.1.1") == 0);
|
||||
assert(strcmp(addr.port, "8080") == 0);
|
||||
free_addr(&addr);
|
||||
return a == nullptr ? b == nullptr : b != nullptr && std::strcmp(a, b) == 0;
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_addr_ipv6_with_port(void)
|
||||
struct AddrCase {
|
||||
const char *input;
|
||||
const char *host;
|
||||
const char *port;
|
||||
};
|
||||
|
||||
// parse_addr keeps anything it cannot split whole, so malformed bracketed
|
||||
// IPv6 and IPv6-like host names are not rewritten.
|
||||
constexpr AddrCase kAddrCases[] = {
|
||||
{ "192.168.1.1:8080", "192.168.1.1", "8080" },
|
||||
{ "[::1]:443", "::1", "443" },
|
||||
{ "example.com:1234", "example.com", "1234" },
|
||||
{ "10.0.0.1", "10.0.0.1", nullptr },
|
||||
{ "::1", "::1", nullptr },
|
||||
{ "[::1", "[::1", nullptr },
|
||||
{ "bad::host", "bad::host", nullptr },
|
||||
{ "[::1]:", "::1", nullptr },
|
||||
{ "[::1]junk", "::1", nullptr },
|
||||
{ "host:", "host", nullptr },
|
||||
{ "", "", nullptr },
|
||||
};
|
||||
|
||||
void
|
||||
test_parse_addr()
|
||||
{
|
||||
ss_addr_t addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
parse_addr("[::1]:443", &addr);
|
||||
assert(addr.host != NULL);
|
||||
assert(addr.port != NULL);
|
||||
assert(strcmp(addr.host, "::1") == 0);
|
||||
assert(strcmp(addr.port, "443") == 0);
|
||||
free_addr(&addr);
|
||||
for (const AddrCase &c : kAddrCases) {
|
||||
ss_addr_t addr = {};
|
||||
parse_addr(c.input, &addr);
|
||||
SS_CHECK(same(addr.host, c.host));
|
||||
SS_CHECK(same(addr.port, c.port));
|
||||
free_addr(&addr);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_addr_hostname_with_port(void)
|
||||
void
|
||||
write_config(const char *path, const char *body)
|
||||
{
|
||||
ss_addr_t addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
parse_addr("example.com:1234", &addr);
|
||||
assert(addr.host != NULL);
|
||||
assert(addr.port != NULL);
|
||||
assert(strcmp(addr.host, "example.com") == 0);
|
||||
assert(strcmp(addr.port, "1234") == 0);
|
||||
free_addr(&addr);
|
||||
FILE *f = std::fopen(path, "wb");
|
||||
SS_CHECK(f != nullptr);
|
||||
std::fputs(body, f);
|
||||
std::fclose(f);
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_addr_no_port(void)
|
||||
{
|
||||
ss_addr_t addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
parse_addr("10.0.0.1", &addr);
|
||||
assert(addr.host != NULL);
|
||||
assert(strcmp(addr.host, "10.0.0.1") == 0);
|
||||
/* Port may be NULL when no port is specified */
|
||||
free_addr(&addr);
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_addr_ipv6_no_port(void)
|
||||
{
|
||||
ss_addr_t addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
parse_addr("::1", &addr);
|
||||
assert(addr.host != NULL);
|
||||
assert(strcmp(addr.host, "::1") == 0);
|
||||
free_addr(&addr);
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_addr_malformed_bracketed_ipv6_is_not_rewritten(void)
|
||||
{
|
||||
ss_addr_t addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
parse_addr("[::1", &addr);
|
||||
assert(addr.host != NULL);
|
||||
assert(addr.port == NULL);
|
||||
assert(strcmp(addr.host, "[::1") == 0);
|
||||
free_addr(&addr);
|
||||
}
|
||||
|
||||
static void
|
||||
test_parse_addr_invalid_ipv6_like_host_is_not_rewritten(void)
|
||||
{
|
||||
ss_addr_t addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
parse_addr("bad::host", &addr);
|
||||
assert(addr.host != NULL);
|
||||
assert(addr.port == NULL);
|
||||
assert(strcmp(addr.host, "bad::host") == 0);
|
||||
free_addr(&addr);
|
||||
}
|
||||
|
||||
static void
|
||||
test_read_jconf_preserves_64bit_integer_strings(void)
|
||||
void
|
||||
test_read_jconf_preserves_64bit_integer_strings()
|
||||
{
|
||||
char path[4096];
|
||||
FILE *f = test_tempfile(path, sizeof(path));
|
||||
assert(f != NULL);
|
||||
fprintf(f, "{\n");
|
||||
fprintf(f, "\"server\":\"127.0.0.1\",\n");
|
||||
fprintf(f, "\"server_port\":4294967296,\n");
|
||||
fprintf(f, "\"password\":\"password\",\n");
|
||||
fprintf(f, "\"method\":\"aes-128-gcm\"\n");
|
||||
fprintf(f, "}\n");
|
||||
fclose(f);
|
||||
SS_CHECK(f != nullptr);
|
||||
std::fclose(f);
|
||||
write_config(path, "{\n\"server\":\"127.0.0.1\",\n\"server_port\":4294967296,\n"
|
||||
"\"password\":\"password\",\n\"method\":\"aes-128-gcm\"\n}\n");
|
||||
|
||||
jconf_t *conf = read_jconf(path);
|
||||
assert(conf != NULL);
|
||||
assert(conf->remote_port != NULL);
|
||||
assert(strcmp(conf->remote_port, "4294967296") == 0);
|
||||
|
||||
remove(path);
|
||||
SS_CHECK(conf != nullptr);
|
||||
SS_CHECK(same(conf->remote_port, "4294967296"));
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
static void
|
||||
write_minimal_config(FILE *f, const char *extra_line)
|
||||
{
|
||||
fprintf(f, "{\n");
|
||||
fprintf(f, "\"server\":\"127.0.0.1\",\n");
|
||||
fprintf(f, "\"server_port\":8388,\n");
|
||||
fprintf(f, "\"password\":\"password\",\n");
|
||||
fprintf(f, "\"method\":\"aes-128-gcm\"");
|
||||
if (extra_line != NULL) {
|
||||
fprintf(f, ",\n%s", extra_line);
|
||||
}
|
||||
fprintf(f, "\n}\n");
|
||||
}
|
||||
|
||||
static const char *test_executable;
|
||||
static void
|
||||
test_read_jconf_rejects_out_of_range_int_options(void)
|
||||
// Every field that read_jconf copies out of the parse tree, which is gone by
|
||||
// the time it returns.
|
||||
void
|
||||
test_read_jconf_copies_out_of_the_parse_tree()
|
||||
{
|
||||
char path[4096];
|
||||
FILE *f = test_tempfile(path, sizeof(path));
|
||||
assert(f != NULL);
|
||||
write_minimal_config(f, "\"tcp_incoming_sndbuf\":2147483648");
|
||||
fclose(f);
|
||||
SS_CHECK(f != nullptr);
|
||||
std::fclose(f);
|
||||
write_config(path,
|
||||
"{\n"
|
||||
" \"server\": [\"192.168.1.1:8080\", \"[::1]:443\", \"example.com\", \"bad::host\"],\n"
|
||||
" \"server_port\": 8388,\n"
|
||||
" \"local_address\": \"127.0.0.1\",\n"
|
||||
" \"local_port\": \"1080\",\n"
|
||||
" \"password\": \"pass\\u00e9word\",\n"
|
||||
" \"method\": \"chacha20-ietf-poly1305\",\n"
|
||||
" \"timeout\": 60,\n"
|
||||
" \"plugin\": \"\",\n"
|
||||
" \"plugin_opts\": \"a=b;c=d\",\n"
|
||||
" \"mode\": \"tcp_and_udp\",\n"
|
||||
" \"port_password\": {\"8381\": \"one\", \"8382\": \"two\", \"8383\": 3},\n"
|
||||
" \"dscp\": {\"8381\": \"EF\", \"8382\": \"AF41\"},\n"
|
||||
" \"acl\": null,\n"
|
||||
" \"max_connections\": 64\n"
|
||||
"}\n");
|
||||
|
||||
jconf_t *conf = read_jconf(path);
|
||||
SS_CHECK(conf != nullptr);
|
||||
SS_CHECK(conf->remote_num == 4);
|
||||
SS_CHECK(same(conf->remote_addr[0].host, "192.168.1.1") && same(conf->remote_addr[0].port, "8080"));
|
||||
SS_CHECK(same(conf->remote_addr[1].host, "::1") && same(conf->remote_addr[1].port, "443"));
|
||||
SS_CHECK(same(conf->remote_addr[2].host, "example.com") && conf->remote_addr[2].port == nullptr);
|
||||
SS_CHECK(same(conf->remote_addr[3].host, "bad::host") && conf->remote_addr[3].port == nullptr);
|
||||
SS_CHECK(same(conf->remote_port, "8388"));
|
||||
SS_CHECK(same(conf->local_addr, "127.0.0.1"));
|
||||
SS_CHECK(same(conf->local_port, "1080"));
|
||||
SS_CHECK(same(conf->password, "pass\xc3\xa9word"));
|
||||
SS_CHECK(same(conf->method, "chacha20-ietf-poly1305"));
|
||||
SS_CHECK(same(conf->timeout, "60"));
|
||||
SS_CHECK(conf->plugin == nullptr);
|
||||
SS_CHECK(same(conf->plugin_opts, "a=b;c=d"));
|
||||
SS_CHECK(conf->mode == TCP_AND_UDP);
|
||||
// Non-string passwords are skipped, as before.
|
||||
SS_CHECK(conf->port_password_num == 2);
|
||||
SS_CHECK(same(conf->port_password[0].port, "8381") && same(conf->port_password[0].password, "one"));
|
||||
SS_CHECK(same(conf->port_password[1].port, "8382") && same(conf->port_password[1].password, "two"));
|
||||
SS_CHECK(conf->dscp_num == 2);
|
||||
SS_CHECK(same(conf->dscp[0].port, "8381") && conf->dscp[0].dscp == 46);
|
||||
SS_CHECK(same(conf->dscp[1].port, "8382") && conf->dscp[1].dscp == 34);
|
||||
SS_CHECK(conf->acl == nullptr);
|
||||
SS_CHECK(conf->max_connections == 64);
|
||||
|
||||
// A second read replaces the first one.
|
||||
write_config(path, "{\"server\":\"10.0.0.1:1\",\"method\":\"aes-256-gcm\",\"password\":\"x\"}");
|
||||
conf = read_jconf(path);
|
||||
SS_CHECK(conf->remote_num == 1);
|
||||
SS_CHECK(same(conf->remote_addr[0].host, "10.0.0.1") && same(conf->remote_addr[0].port, "1"));
|
||||
SS_CHECK(same(conf->method, "aes-256-gcm"));
|
||||
SS_CHECK(conf->remote_port == nullptr && conf->port_password_num == 0);
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
// The largest accepted file must fit both arenas: many short values cost the
|
||||
// most tree per input byte.
|
||||
void
|
||||
test_read_jconf_dense_file_fits()
|
||||
{
|
||||
char path[4096];
|
||||
FILE *f = test_tempfile(path, sizeof(path));
|
||||
SS_CHECK(f != nullptr);
|
||||
std::fputs("{\"method\":\"aes-128-gcm\",\"password\":\"p\",\"server\":\"h\",\"x\":[", f);
|
||||
long written = std::ftell(f);
|
||||
while (written < MAX_CONF_SIZE - 16) {
|
||||
std::fputs("[0],", f);
|
||||
written += 4;
|
||||
}
|
||||
std::fputs("0]}", f);
|
||||
std::fclose(f);
|
||||
|
||||
jconf_t *conf = read_jconf(path);
|
||||
SS_CHECK(same(conf->method, "aes-128-gcm"));
|
||||
SS_CHECK(same(conf->remote_addr[0].host, "h"));
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
const char *test_executable;
|
||||
|
||||
void
|
||||
test_read_jconf_rejects_out_of_range_int_options()
|
||||
{
|
||||
char path[4096];
|
||||
FILE *f = test_tempfile(path, sizeof(path));
|
||||
SS_CHECK(f != nullptr);
|
||||
std::fclose(f);
|
||||
write_config(path, "{\n\"server\":\"127.0.0.1\",\n\"server_port\":8388,\n\"password\":\"password\",\n"
|
||||
"\"method\":\"aes-128-gcm\",\n\"tcp_incoming_sndbuf\":2147483648\n}\n");
|
||||
|
||||
#ifdef _WIN32
|
||||
char command[3 * 4096];
|
||||
assert(snprintf(command, sizeof(command), "\"%s\" --read-config \"%s\"",
|
||||
test_executable, path) < (int)sizeof(command));
|
||||
STARTUPINFOA startup = {0};
|
||||
PROCESS_INFORMATION child = {0};
|
||||
startup.cb = sizeof(startup);
|
||||
assert(CreateProcessA(test_executable, command, NULL, NULL, FALSE, 0,
|
||||
NULL, NULL, &startup, &child));
|
||||
SS_CHECK(std::snprintf(command, sizeof(command), "\"%s\" --read-config \"%s\"", test_executable, path)
|
||||
< static_cast<int>(sizeof(command)));
|
||||
STARTUPINFOA startup = {};
|
||||
PROCESS_INFORMATION child = {};
|
||||
startup.cb = sizeof(startup);
|
||||
SS_CHECK(CreateProcessA(test_executable, command, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup,
|
||||
&child));
|
||||
CloseHandle(child.hThread);
|
||||
assert(WaitForSingleObject(child.hProcess, 10000) == WAIT_OBJECT_0);
|
||||
SS_CHECK(WaitForSingleObject(child.hProcess, 10000) == WAIT_OBJECT_0);
|
||||
DWORD status;
|
||||
assert(GetExitCodeProcess(child.hProcess, &status));
|
||||
SS_CHECK(GetExitCodeProcess(child.hProcess, &status));
|
||||
CloseHandle(child.hProcess);
|
||||
/* FATAL exits with -1, which is a valid nonzero DWORD exit status. */
|
||||
assert(status != 0);
|
||||
// FATAL exits with -1, which is a valid nonzero DWORD exit status.
|
||||
SS_CHECK(status != 0);
|
||||
#else
|
||||
std::fflush(stdout); // the child's FATAL exit would flush it again
|
||||
pid_t pid = fork();
|
||||
assert(pid >= 0);
|
||||
SS_CHECK(pid >= 0);
|
||||
if (pid == 0) {
|
||||
read_jconf(path);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
int status;
|
||||
assert(waitpid(pid, &status, 0) == pid);
|
||||
assert(WIFEXITED(status));
|
||||
assert(WEXITSTATUS(status) != 0);
|
||||
SS_CHECK(waitpid(pid, &status, 0) == pid);
|
||||
SS_CHECK(WIFEXITED(status));
|
||||
SS_CHECK(WEXITSTATUS(status) != 0);
|
||||
#endif
|
||||
|
||||
remove(path);
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
static void
|
||||
test_server_endpoints(void)
|
||||
void
|
||||
test_server_endpoints()
|
||||
{
|
||||
const char *valid[][3] = {
|
||||
const AddrCase valid[] = {
|
||||
{ "example.com:8388", "example.com", "8388" },
|
||||
{ "127.0.0.1:1", "127.0.0.1", "1" },
|
||||
{ "[2001:db8::1]:65535", "2001:db8::1", "65535" },
|
||||
{ "[fe80::1%eth0]:8388", "fe80::1%eth0", "8388" },
|
||||
{ "[fe80::1%3]:8388", "fe80::1%3", "8388" },
|
||||
{ "2001:db8::1:8388", "2001:db8::1:8388", NULL },
|
||||
{ "fe80::1%eth0", "fe80::1%eth0", NULL },
|
||||
{ "::", "::", NULL },
|
||||
{ "[::1]", "::1", NULL },
|
||||
{ "example.com", "example.com", NULL }
|
||||
{ "2001:db8::1:8388", "2001:db8::1:8388", nullptr },
|
||||
{ "fe80::1%eth0", "fe80::1%eth0", nullptr },
|
||||
{ "::", "::", nullptr },
|
||||
{ "[::1]", "::1", nullptr },
|
||||
{ "example.com", "example.com", nullptr },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(valid) / sizeof(valid[0]); i++) {
|
||||
ss_addr_t addr = { 0 };
|
||||
assert(parse_server_endpoint(valid[i][0], &addr) == 0);
|
||||
assert(strcmp(addr.host, valid[i][1]) == 0);
|
||||
assert(valid[i][2] ? addr.port && strcmp(addr.port, valid[i][2]) == 0 : addr.port == NULL);
|
||||
for (const AddrCase &c : valid) {
|
||||
ss_addr_t addr = {};
|
||||
SS_CHECK(parse_server_endpoint(c.input, &addr) == 0);
|
||||
SS_CHECK(same(addr.host, c.host));
|
||||
SS_CHECK(same(addr.port, c.port));
|
||||
free_addr(&addr);
|
||||
}
|
||||
const char *invalid[] = { "", ":8388", "host:", "host:0", "host:65536",
|
||||
"host:-1", "host:+1", "host:abc", "host:80:90", "[::1", "[::1]junk",
|
||||
"[::1]:", "[::1]:0", "[::1]:65536", "[::1]:80:90", "[]:80",
|
||||
"[example.com]:80", "[127.0.0.1]:80", "[bad::host]:80", "host]:80",
|
||||
"[fe80::1%]:80", "[fe80::1%a%b]:80", "host :80", "ss://host:80" };
|
||||
for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) {
|
||||
ss_addr_t addr = { 0 };
|
||||
assert(parse_server_endpoint(invalid[i], &addr) == -1);
|
||||
assert(addr.host == NULL && addr.port == NULL);
|
||||
"host:-1", "host:+1", "host:abc", "host:80:90", "[::1", "[::1]junk",
|
||||
"[::1]:", "[::1]:0", "[::1]:65536", "[::1]:80:90", "[]:80",
|
||||
"[example.com]:80", "[127.0.0.1]:80", "[bad::host]:80", "host]:80",
|
||||
"[fe80::1%]:80", "[fe80::1%a%b]:80", "host :80", "ss://host:80" };
|
||||
for (const char *input : invalid) {
|
||||
ss_addr_t addr = {};
|
||||
SS_CHECK(parse_server_endpoint(input, &addr) == -1);
|
||||
SS_CHECK(addr.host == nullptr && addr.port == nullptr);
|
||||
}
|
||||
ss_addr_t addr[2] = { { 0 }, { 0 } };
|
||||
assert(parse_server_endpoint("[::1]:8388", &addr[0]) == 0);
|
||||
assert(parse_server_endpoint("localhost:8389", &addr[1]) == 0);
|
||||
assert(complete_server_ports(addr, 2, NULL, 0) == 0);
|
||||
assert(complete_server_ports(addr, 2, "9000", 0) == 0);
|
||||
assert(strcmp(addr[0].port, "8388") == 0 && strcmp(addr[1].port, "8389") == 0);
|
||||
assert(complete_server_ports(addr, 2, NULL, 1) == -1);
|
||||
ss_addr_t addr[2] = {};
|
||||
SS_CHECK(parse_server_endpoint("[::1]:8388", &addr[0]) == 0);
|
||||
SS_CHECK(parse_server_endpoint("localhost:8389", &addr[1]) == 0);
|
||||
SS_CHECK(complete_server_ports(addr, 2, nullptr, 0) == 0);
|
||||
SS_CHECK(complete_server_ports(addr, 2, "9000", 0) == 0);
|
||||
SS_CHECK(same(addr[0].port, "8388") && same(addr[1].port, "8389"));
|
||||
SS_CHECK(complete_server_ports(addr, 2, nullptr, 1) == -1);
|
||||
free_addr(&addr[1]);
|
||||
memset(&addr[1], 0, sizeof(addr[1]));
|
||||
assert(parse_server_endpoint("::1", &addr[1]) == 0);
|
||||
assert(complete_server_ports(addr, 2, NULL, 0) == -1);
|
||||
assert(complete_server_ports(addr, 2, "8388", 1) == 0);
|
||||
assert(strcmp(addr[1].port, "8388") == 0);
|
||||
addr[1] = {};
|
||||
SS_CHECK(parse_server_endpoint("::1", &addr[1]) == 0);
|
||||
SS_CHECK(complete_server_ports(addr, 2, nullptr, 0) == -1);
|
||||
SS_CHECK(complete_server_ports(addr, 2, "8388", 1) == 0);
|
||||
SS_CHECK(same(addr[1].port, "8388"));
|
||||
free_addr(&addr[0]);
|
||||
free_addr(&addr[1]);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
test_executable = argv[0];
|
||||
test_network_init();
|
||||
if (argc == 3 && strcmp(argv[1], "--read-config") == 0) {
|
||||
if (argc == 3 && std::strcmp(argv[1], "--read-config") == 0) {
|
||||
read_jconf(argv[2]);
|
||||
return 0;
|
||||
}
|
||||
test_server_endpoints();
|
||||
test_parse_addr_ipv4_with_port();
|
||||
test_parse_addr_ipv6_with_port();
|
||||
test_parse_addr_hostname_with_port();
|
||||
test_parse_addr_no_port();
|
||||
test_parse_addr_ipv6_no_port();
|
||||
test_parse_addr_malformed_bracketed_ipv6_is_not_rewritten();
|
||||
test_parse_addr_invalid_ipv6_like_host_is_not_rewritten();
|
||||
test_read_jconf_preserves_64bit_integer_strings();
|
||||
test_read_jconf_rejects_out_of_range_int_options();
|
||||
SS_RUN(test_server_endpoints);
|
||||
SS_RUN(test_parse_addr);
|
||||
SS_RUN(test_read_jconf_preserves_64bit_integer_strings);
|
||||
SS_RUN(test_read_jconf_copies_out_of_the_parse_tree);
|
||||
SS_RUN(test_read_jconf_dense_file_fits);
|
||||
SS_RUN(test_read_jconf_rejects_out_of_range_int_options);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user