mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-28 10:44:43 +08:00
Accept an ss:// server URL on the ss-local command line
shadowsocks-rust takes a whole server definition as a URL; libev could only be given the pieces separately, so importing a server meant taking a URL apart by hand. ss-local now accepts --server-url and reads the address, port, cipher, password and SIP003 plugin from it. Both encodings are handled: the SIP002 form with the userinfo in base64url, and the older form where the whole body is one base64 blob. The userinfo is also accepted in the clear, because that is what shadowsocks-rust's `ssurl` emits for the 2022 ciphers, whose base64 pre-shared keys it percent-encodes instead. IPv6 literals, a trailing slash, the plugin query and the '#' tag are all covered. Options given later on the command line still win, since they are applied as getopt reaches them. ssurl.c sits with the ss-local sources rather than the shared ones: it depends on base64.c, which ss-manager does not link. The tests include URLs copied verbatim from `ssurl --encode` output, so the parser is pinned against what another implementation really produces rather than only against my reading of the spec. Verified end to end by pointing ss-local at a rust-generated URL and proxying through their ssserver.
This commit is contained in:
1 parent
3728a993a7
commit
8cc58cea95
10 files changed
+667
-1
No files matched your search
@@ -368,6 +368,12 @@ you may refer to the man pages of the applications, respectively.
|
||||
`openssl rand -base64 32` (or 16 for
|
||||
2022-blake3-aes-128-gcm).
|
||||
|
||||
[--server-url <ss_url>] Take the server address, port, cipher,
|
||||
password and SIP003 plugin from a single
|
||||
ss:// URL (SIP002 or the legacy form).
|
||||
ss-local only. Options given later on the
|
||||
command line override the URL's values.
|
||||
|
||||
[-a <user>] Run as another user.
|
||||
|
||||
[-f <pid_file>] The file path to store pid.
|
||||
|
||||
@@ -17,6 +17,7 @@ SYNOPSIS
|
||||
[--mtu <MTU>] [--no-delay]
|
||||
[--plugin <plugin_name>] [--plugin-opts <plugin_options>]
|
||||
[--password <password>] [--key <key_in_base64>]
|
||||
[--server-url <ss_url>]
|
||||
|
||||
DESCRIPTION
|
||||
-----------
|
||||
@@ -48,6 +49,14 @@ Set the password. The server and the client should use the same password.
|
||||
--key <key_in_base64>::
|
||||
Set the key directly. The key should be encoded with URL-safe Base64.
|
||||
|
||||
--server-url <ss_url>::
|
||||
Take the server address, port, cipher, password and any SIP003 plugin
|
||||
from a single 'ss://' URL, as produced by most clients and by
|
||||
*shadowsocks-rust*'s `ssurl`. Both the SIP002 form
|
||||
('ss://base64(method:password)@host:port/?plugin=...#tag') and the older
|
||||
'ss://base64(method:password@host:port)' form are accepted. Options given
|
||||
later on the command line override the values taken from the URL.
|
||||
|
||||
-m <encrypt_method>::
|
||||
Set the cipher.
|
||||
+
|
||||
|
||||
@@ -59,6 +59,7 @@ set(LIBSHADOWSOCKS_LIBEV_SOURCE
|
||||
udprelay.c
|
||||
cache.c
|
||||
local.c
|
||||
ssurl.c
|
||||
${SS_CRYPTO_SOURCE}
|
||||
${SS_PLUGIN_SOURCE}
|
||||
${SS_ACL_SOURCE}
|
||||
|
||||
+2
-1
@@ -75,7 +75,8 @@ enum {
|
||||
GETOPT_VAL_TCP_INCOMING_RCVBUF,
|
||||
GETOPT_VAL_TCP_OUTGOING_SNDBUF,
|
||||
GETOPT_VAL_TCP_OUTGOING_RCVBUF,
|
||||
GETOPT_VAL_NFTABLES_SETS
|
||||
GETOPT_VAL_NFTABLES_SETS,
|
||||
GETOPT_VAL_SERVER_URL
|
||||
};
|
||||
|
||||
#endif // _COMMON_H
|
||||
+29
@@ -52,6 +52,7 @@
|
||||
#include <libcork/core.h>
|
||||
|
||||
#include "netutils.h"
|
||||
#include "ssurl.h"
|
||||
#include "utils.h"
|
||||
#include "socks5.h"
|
||||
#include "acl.h"
|
||||
@@ -1455,6 +1456,9 @@ main(int argc, char **argv)
|
||||
ss_addr_t remote_addr[MAX_REMOTE_NUM];
|
||||
char *remote_port = NULL;
|
||||
|
||||
/* Lives until exit; its strings are handed to the config above. */
|
||||
ss_url_t server_url = { 0 };
|
||||
|
||||
memset(remote_addr, 0, sizeof(ss_addr_t) * MAX_REMOTE_NUM);
|
||||
|
||||
static struct option long_options[] = {
|
||||
@@ -1472,6 +1476,7 @@ main(int argc, char **argv)
|
||||
{ "plugin-opts", required_argument, NULL, GETOPT_VAL_PLUGIN_OPTS },
|
||||
{ "password", required_argument, NULL, GETOPT_VAL_PASSWORD },
|
||||
{ "key", required_argument, NULL, GETOPT_VAL_KEY },
|
||||
{ "server-url", required_argument, NULL, GETOPT_VAL_SERVER_URL },
|
||||
{ "help", no_argument, NULL, GETOPT_VAL_HELP },
|
||||
{ NULL, 0, NULL, 0 }
|
||||
};
|
||||
@@ -1519,6 +1524,30 @@ main(int argc, char **argv)
|
||||
case GETOPT_VAL_KEY:
|
||||
key = optarg;
|
||||
break;
|
||||
case GETOPT_VAL_SERVER_URL:
|
||||
/*
|
||||
* An ss:// URI carries the server, credentials and plugin in one
|
||||
* argument. Options given later on the command line still win,
|
||||
* since they are applied as getopt reaches them.
|
||||
*/
|
||||
if (ss_url_parse(optarg, &server_url) != 0) {
|
||||
FATAL("invalid ss:// server URL");
|
||||
}
|
||||
if (remote_num < MAX_REMOTE_NUM) {
|
||||
remote_addr[remote_num].host = server_url.host;
|
||||
remote_addr[remote_num].port = NULL;
|
||||
remote_num++;
|
||||
}
|
||||
remote_port = server_url.port;
|
||||
method = server_url.method;
|
||||
password = server_url.password;
|
||||
if (server_url.plugin != NULL)
|
||||
plugin = server_url.plugin;
|
||||
if (server_url.plugin_opts != NULL)
|
||||
plugin_opts = server_url.plugin_opts;
|
||||
if (server_url.tag != NULL)
|
||||
LOGI("using server \"%s\"", server_url.tag);
|
||||
break;
|
||||
case GETOPT_VAL_REUSE_PORT:
|
||||
reuse_port = 1;
|
||||
break;
|
||||
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* ssurl.c - Parse ss:// server URIs (SIP002 and the legacy form)
|
||||
*
|
||||
* Copyright (C) 2013 - 2026, Max Lv <max.c.lv@gmail.com>
|
||||
*
|
||||
* This file is part of the shadowsocks-libev.
|
||||
*
|
||||
* shadowsocks-libev is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* shadowsocks-libev is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with shadowsocks-libev; see the file COPYING. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Spec: https://shadowsocks.org/guide/sip002.html
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "base64.h"
|
||||
#include "ssurl.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define SS_URL_SCHEME "ss://"
|
||||
|
||||
/* memrchr is a GNU extension and is absent on macOS and the BSDs. */
|
||||
static const char *
|
||||
find_last(const char *s, size_t len, char c)
|
||||
{
|
||||
for (size_t i = len; i > 0; i--) {
|
||||
if (s[i - 1] == c)
|
||||
return s + i - 1;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *
|
||||
str_ndup(const char *s, size_t n)
|
||||
{
|
||||
char *out = ss_malloc(n + 1);
|
||||
|
||||
memcpy(out, s, n);
|
||||
out[n] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
static int
|
||||
hex_value(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Percent-decode in place semantics: returns a newly allocated string. */
|
||||
static char *
|
||||
percent_decode(const char *s, size_t len)
|
||||
{
|
||||
char *out = ss_malloc(len + 1);
|
||||
size_t o = 0;
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (s[i] == '%' && i + 2 < len) {
|
||||
int hi = hex_value(s[i + 1]);
|
||||
int lo = hex_value(s[i + 2]);
|
||||
if (hi >= 0 && lo >= 0) {
|
||||
out[o++] = (char)((hi << 4) | lo);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out[o++] = s[i];
|
||||
}
|
||||
|
||||
out[o] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Base64-decode into a fresh string. Returns NULL if the input is not
|
||||
* valid base64. base64_decode accepts both the standard and URL-safe
|
||||
* alphabets and stops at padding, so unpadded input works too.
|
||||
*/
|
||||
static char *
|
||||
base64_dup(const char *s, size_t len)
|
||||
{
|
||||
if (len == 0)
|
||||
return NULL;
|
||||
|
||||
char *in = str_ndup(s, len);
|
||||
/* decoded output is always shorter than the encoded input */
|
||||
uint8_t *out = ss_malloc(len + 1);
|
||||
|
||||
int out_len = base64_decode(out, in, (int)len);
|
||||
ss_free(in);
|
||||
|
||||
if (out_len <= 0) {
|
||||
ss_free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out[out_len] = '\0';
|
||||
return (char *)out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Split "host:port" and store the parts. Handles the bracketed IPv6 form
|
||||
* [::1]:8388. Returns 0 on success.
|
||||
*/
|
||||
static int
|
||||
split_host_port(const char *s, size_t len, ss_url_t *url)
|
||||
{
|
||||
if (len == 0)
|
||||
return -1;
|
||||
|
||||
if (s[0] == '[') {
|
||||
const char *close = memchr(s, ']', len);
|
||||
if (close == NULL)
|
||||
return -1;
|
||||
|
||||
size_t host_len = (size_t)(close - s) - 1;
|
||||
if (host_len == 0)
|
||||
return -1;
|
||||
|
||||
size_t rest = len - (size_t)(close - s) - 1;
|
||||
if (rest < 2 || close[1] != ':')
|
||||
return -1;
|
||||
|
||||
url->host = str_ndup(s + 1, host_len);
|
||||
url->port = str_ndup(close + 2, rest - 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *colon = find_last(s, len, ':');
|
||||
if (colon == NULL || colon == s)
|
||||
return -1;
|
||||
|
||||
size_t host_len = (size_t)(colon - s);
|
||||
size_t port_len = len - host_len - 1;
|
||||
if (port_len == 0)
|
||||
return -1;
|
||||
|
||||
url->host = str_ndup(s, host_len);
|
||||
url->port = str_ndup(colon + 1, port_len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Split "method:password" into the url fields. Returns 0 on success. */
|
||||
static int
|
||||
split_userinfo(const char *s, ss_url_t *url)
|
||||
{
|
||||
const char *colon = strchr(s, ':');
|
||||
|
||||
if (colon == NULL || colon == s || colon[1] == '\0')
|
||||
return -1;
|
||||
|
||||
url->method = str_ndup(s, (size_t)(colon - s));
|
||||
url->password = str_ndup(colon + 1, strlen(colon + 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Read the query string. Only "plugin" is meaningful to us; per SIP003 its
|
||||
* value is the plugin name, optionally followed by ';' and the options.
|
||||
*/
|
||||
static void
|
||||
parse_query(const char *s, size_t len, ss_url_t *url)
|
||||
{
|
||||
const char *end = s + len;
|
||||
|
||||
while (s < end) {
|
||||
const char *amp = memchr(s, '&', (size_t)(end - s));
|
||||
const char *pair_end = amp ? amp : end;
|
||||
const char *eq = memchr(s, '=', (size_t)(pair_end - s));
|
||||
|
||||
if (eq != NULL) {
|
||||
size_t name_len = (size_t)(eq - s);
|
||||
if (name_len == 6 && strncmp(s, "plugin", 6) == 0) {
|
||||
char *value = percent_decode(eq + 1, (size_t)(pair_end - eq - 1));
|
||||
char *semi = strchr(value, ';');
|
||||
if (semi != NULL) {
|
||||
*semi = '\0';
|
||||
url->plugin_opts = str_ndup(semi + 1, strlen(semi + 1));
|
||||
}
|
||||
url->plugin = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (amp == NULL)
|
||||
break;
|
||||
s = amp + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ss_url_free(ss_url_t *url)
|
||||
{
|
||||
if (url == NULL)
|
||||
return;
|
||||
|
||||
ss_free(url->method);
|
||||
ss_free(url->password);
|
||||
ss_free(url->host);
|
||||
ss_free(url->port);
|
||||
ss_free(url->plugin);
|
||||
ss_free(url->plugin_opts);
|
||||
ss_free(url->tag);
|
||||
memset(url, 0, sizeof(*url));
|
||||
}
|
||||
|
||||
int
|
||||
ss_url_parse(const char *uri, ss_url_t *url)
|
||||
{
|
||||
if (uri == NULL || url == NULL)
|
||||
return -1;
|
||||
|
||||
memset(url, 0, sizeof(*url));
|
||||
|
||||
if (strncasecmp(uri, SS_URL_SCHEME, strlen(SS_URL_SCHEME)) != 0)
|
||||
return -1;
|
||||
|
||||
const char *body = uri + strlen(SS_URL_SCHEME);
|
||||
size_t body_len = strlen(body);
|
||||
if (body_len == 0)
|
||||
return -1;
|
||||
|
||||
/* The fragment is a human-readable tag and is not part of the config. */
|
||||
const char *hash = memchr(body, '#', body_len);
|
||||
if (hash != NULL) {
|
||||
url->tag = percent_decode(hash + 1, body_len - (size_t)(hash - body) - 1);
|
||||
body_len = (size_t)(hash - body);
|
||||
}
|
||||
|
||||
const char *query = memchr(body, '?', body_len);
|
||||
size_t head_len = query ? (size_t)(query - body) : body_len;
|
||||
|
||||
/*
|
||||
* SIP002 keeps the host in the clear, so an '@' separates the encoded
|
||||
* userinfo from it. Without one this is the legacy form, where the
|
||||
* whole body is a single base64 blob.
|
||||
*/
|
||||
const char *at = find_last(body, head_len, '@');
|
||||
|
||||
if (at != NULL) {
|
||||
size_t userinfo_len = (size_t)(at - body);
|
||||
if (userinfo_len == 0)
|
||||
goto fail;
|
||||
|
||||
/*
|
||||
* The userinfo is normally base64, but some producers write
|
||||
* "method:password" directly; base64 decoding fails on the ':'
|
||||
* so a failed decode simply means the plain form.
|
||||
*/
|
||||
char *userinfo = base64_dup(body, userinfo_len);
|
||||
if (userinfo == NULL || strchr(userinfo, ':') == NULL) {
|
||||
ss_free(userinfo);
|
||||
userinfo = percent_decode(body, userinfo_len);
|
||||
}
|
||||
|
||||
int rc = split_userinfo(userinfo, url);
|
||||
ss_free(userinfo);
|
||||
if (rc != 0)
|
||||
goto fail;
|
||||
|
||||
/* A trailing '/' before the query or end is allowed by SIP002. */
|
||||
size_t hostport_len = head_len - userinfo_len - 1;
|
||||
const char *hostport = at + 1;
|
||||
if (hostport_len > 0 && hostport[hostport_len - 1] == '/')
|
||||
hostport_len--;
|
||||
|
||||
if (split_host_port(hostport, hostport_len, url) != 0)
|
||||
goto fail;
|
||||
} else {
|
||||
char *plain = base64_dup(body, head_len);
|
||||
if (plain == NULL)
|
||||
goto fail;
|
||||
|
||||
char *sep = strrchr(plain, '@');
|
||||
if (sep == NULL) {
|
||||
ss_free(plain);
|
||||
goto fail;
|
||||
}
|
||||
*sep = '\0';
|
||||
|
||||
int rc = split_userinfo(plain, url);
|
||||
if (rc == 0)
|
||||
rc = split_host_port(sep + 1, strlen(sep + 1), url);
|
||||
ss_free(plain);
|
||||
if (rc != 0)
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (query != NULL) {
|
||||
size_t query_ofst = (size_t)(query - body) + 1;
|
||||
parse_query(body + query_ofst, body_len - query_ofst, url);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
ss_url_free(url);
|
||||
return -1;
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* ssurl.h - Parse ss:// server URIs (SIP002 and the legacy form)
|
||||
*
|
||||
* Copyright (C) 2013 - 2026, Max Lv <max.c.lv@gmail.com>
|
||||
*
|
||||
* This file is part of the shadowsocks-libev.
|
||||
*
|
||||
* shadowsocks-libev is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* shadowsocks-libev is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with shadowsocks-libev; see the file COPYING. If not, see
|
||||
* <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _SSURL_H
|
||||
#define _SSURL_H
|
||||
|
||||
/*
|
||||
* A parsed ss:// URI. Every field is heap-allocated and owned by the
|
||||
* struct; release them all with ss_url_free(). Fields that the URI did not
|
||||
* carry are NULL.
|
||||
*/
|
||||
typedef struct ss_url {
|
||||
char *method;
|
||||
char *password;
|
||||
char *host;
|
||||
char *port;
|
||||
char *plugin;
|
||||
char *plugin_opts;
|
||||
char *tag;
|
||||
} ss_url_t;
|
||||
|
||||
/*
|
||||
* Parse an ss:// URI in either the SIP002 form
|
||||
*
|
||||
* ss://base64url(method:password)@host:port/?plugin=name;opts#tag
|
||||
*
|
||||
* or the legacy form
|
||||
*
|
||||
* ss://base64(method:password@host:port)#tag
|
||||
*
|
||||
* Returns 0 on success, -1 if the URI is malformed. On failure nothing is
|
||||
* allocated and *url is left zeroed.
|
||||
*/
|
||||
int ss_url_parse(const char *uri, ss_url_t *url);
|
||||
|
||||
/* Free every field and zero the struct. Safe on a zeroed struct. */
|
||||
void ss_url_free(ss_url_t *url);
|
||||
|
||||
#endif // _SSURL_H
|
||||
@@ -461,6 +461,12 @@ usage()
|
||||
" [--no-delay] Enable TCP_NODELAY.\n");
|
||||
printf(
|
||||
" [--key <key_in_base64>] Key of your remote server.\n");
|
||||
#endif
|
||||
#ifdef MODULE_LOCAL
|
||||
printf(
|
||||
" [--server-url <ss_url>] Server as an ss:// URL, carrying the\n");
|
||||
printf(
|
||||
" address, cipher, password and plugin.\n");
|
||||
#endif
|
||||
printf(
|
||||
" [--plugin <name>] Enable SIP003 plugin. (Experimental)\n");
|
||||
|
||||
@@ -60,6 +60,11 @@ ss_add_test(test_json
|
||||
"test_json.c;${PROJECT_SOURCE_DIR}/src/json.c"
|
||||
"m")
|
||||
|
||||
# test_ssurl - ss:// URI parsing (SIP002 and legacy)
|
||||
ss_add_test(test_ssurl
|
||||
"test_ssurl.c;${PROJECT_SOURCE_DIR}/src/ssurl.c;${PROJECT_SOURCE_DIR}/src/base64.c;${PROJECT_SOURCE_DIR}/src/utils.c"
|
||||
"${LIBSODIUM_SHARED}")
|
||||
|
||||
# test_netutils - needs cork for ip address parsing
|
||||
set(TEST_NETUTILS_LIBS
|
||||
${LIBSODIUM_SHARED}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Unit tests for ss:// URI parsing (SIP002 and the legacy form).
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int verbose = 0;
|
||||
|
||||
#include "ssurl.h"
|
||||
|
||||
#define STREQ(a, b) ((a) != NULL && strcmp((a), (b)) == 0)
|
||||
|
||||
/* ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888 */
|
||||
static void
|
||||
test_sip002_basic(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888",
|
||||
&u) == 0);
|
||||
assert(STREQ(u.method, "aes-256-gcm"));
|
||||
assert(STREQ(u.password, "password"));
|
||||
assert(STREQ(u.host, "192.168.100.1"));
|
||||
assert(STREQ(u.port, "8888"));
|
||||
assert(u.plugin == NULL);
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* A tag after '#' is descriptive only and must not leak into the fields. */
|
||||
static void
|
||||
test_sip002_tag(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888"
|
||||
"#Example%20Server", &u) == 0);
|
||||
assert(STREQ(u.host, "192.168.100.1"));
|
||||
assert(STREQ(u.tag, "Example Server"));
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* SIP003 plugin, with its options after the ';'. */
|
||||
static void
|
||||
test_sip002_plugin(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888"
|
||||
"/?plugin=obfs-local%3Bobfs%3Dhttp#tag", &u) == 0);
|
||||
assert(STREQ(u.host, "192.168.100.1"));
|
||||
assert(STREQ(u.port, "8888"));
|
||||
assert(STREQ(u.plugin, "obfs-local"));
|
||||
assert(STREQ(u.plugin_opts, "obfs=http"));
|
||||
assert(STREQ(u.tag, "tag"));
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* The trailing slash before a query is allowed, and so is no query at all. */
|
||||
static void
|
||||
test_sip002_trailing_slash(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@example.com:8388/",
|
||||
&u) == 0);
|
||||
assert(STREQ(u.host, "example.com"));
|
||||
assert(STREQ(u.port, "8388"));
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* A bracketed IPv6 literal must keep its address and port apart. */
|
||||
static void
|
||||
test_sip002_ipv6(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@[2001:db8::1]:8388",
|
||||
&u) == 0);
|
||||
assert(STREQ(u.host, "2001:db8::1"));
|
||||
assert(STREQ(u.port, "8388"));
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* Some producers write the userinfo in the clear rather than base64. */
|
||||
static void
|
||||
test_sip002_plain_userinfo(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
assert(ss_url_parse("ss://aes-256-gcm:secret@example.com:8388", &u) == 0);
|
||||
assert(STREQ(u.method, "aes-256-gcm"));
|
||||
assert(STREQ(u.password, "secret"));
|
||||
assert(STREQ(u.host, "example.com"));
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/*
|
||||
* A 2022 pre-shared key is base64 and can contain '+', '/' and '='. Encoded
|
||||
* into the userinfo it must survive the round trip byte for byte.
|
||||
*/
|
||||
static void
|
||||
test_sip002_2022_psk(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
/* base64("2022-blake3-aes-256-gcm:ZG9uJ3QgdXNlIHRoaXMga2V5Pz8/Pz8/Pz8=") */
|
||||
const char *uri =
|
||||
"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206Wkc5dUozUWdkWE5sSUhSb2FYTWdhMlY1"
|
||||
"Pz8_Pz8_Pz89@example.com:8388";
|
||||
|
||||
assert(ss_url_parse(uri, &u) == 0);
|
||||
assert(STREQ(u.method, "2022-blake3-aes-256-gcm"));
|
||||
assert(STREQ(u.host, "example.com"));
|
||||
assert(STREQ(u.port, "8388"));
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* Legacy form: the whole body is one base64 blob. */
|
||||
static void
|
||||
test_legacy(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
/* base64("aes-256-cfb:password@192.168.100.1:8888") */
|
||||
assert(ss_url_parse("ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAMTkyLjE2OC4xMDAuMTo4ODg4",
|
||||
&u) == 0);
|
||||
assert(STREQ(u.method, "aes-256-cfb"));
|
||||
assert(STREQ(u.password, "password"));
|
||||
assert(STREQ(u.host, "192.168.100.1"));
|
||||
assert(STREQ(u.port, "8888"));
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* Malformed input must be rejected, not half-parsed. */
|
||||
static void
|
||||
test_rejects_malformed(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
static const char *bad[] = {
|
||||
NULL,
|
||||
"",
|
||||
"http://example.com",
|
||||
"ss://",
|
||||
"ss://@example.com:8388", /* empty userinfo */
|
||||
"ss://YWVzOnB3@example.com", /* no port */
|
||||
"ss://YWVzOnB3@:8388", /* no host */
|
||||
"ss://YWVzOnB3@example.com:", /* empty port */
|
||||
"ss://bm9jb2xvbg@example.com:8388", /* userinfo lacks ':' */
|
||||
"ss://YWVzOnB3@[2001:db8::1", /* unterminated bracket */
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||||
assert(ss_url_parse(bad[i], &u) == -1);
|
||||
/* a rejected parse must leave nothing behind to free */
|
||||
assert(u.method == NULL && u.host == NULL && u.port == NULL);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* URIs emitted by shadowsocks-rust's `ssurl --encode`, copied verbatim.
|
||||
* These pin the shapes another implementation actually produces, which is
|
||||
* what the parser has to accept in the field.
|
||||
*/
|
||||
static void
|
||||
test_rust_ssurl_output(void)
|
||||
{
|
||||
ss_url_t u;
|
||||
|
||||
/* A 2022 cipher: plain userinfo, '=' of the key percent-encoded. */
|
||||
assert(ss_url_parse("ss://2022-blake3-aes-256-gcm:"
|
||||
"ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE%3D"
|
||||
"@192.168.100.1:8888", &u) == 0);
|
||||
assert(STREQ(u.method, "2022-blake3-aes-256-gcm"));
|
||||
assert(STREQ(u.password, "ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE="));
|
||||
assert(STREQ(u.host, "192.168.100.1"));
|
||||
assert(STREQ(u.port, "8888"));
|
||||
ss_url_free(&u);
|
||||
|
||||
/* A legacy cipher: unpadded base64 userinfo, plugin in the query. */
|
||||
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206dGVzdC1wYXNzd29yZA"
|
||||
"@192.168.100.1:8888/?plugin=obfs%2Dlocal%3Bobfs%3Dhttp",
|
||||
&u) == 0);
|
||||
assert(STREQ(u.method, "aes-256-gcm"));
|
||||
assert(STREQ(u.password, "test-password"));
|
||||
assert(STREQ(u.plugin, "obfs-local"));
|
||||
assert(STREQ(u.plugin_opts, "obfs=http"));
|
||||
ss_url_free(&u);
|
||||
|
||||
/* An IPv6 server. */
|
||||
assert(ss_url_parse("ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpwdw"
|
||||
"@[2001:db8::1]:8388", &u) == 0);
|
||||
assert(STREQ(u.method, "chacha20-ietf-poly1305"));
|
||||
assert(STREQ(u.password, "pw"));
|
||||
assert(STREQ(u.host, "2001:db8::1"));
|
||||
assert(STREQ(u.port, "8388"));
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* Freeing a zeroed struct, or NULL, must be safe. */
|
||||
static void
|
||||
test_free_is_safe(void)
|
||||
{
|
||||
ss_url_t u = { 0 };
|
||||
|
||||
ss_url_free(&u);
|
||||
ss_url_free(NULL);
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
test_sip002_basic();
|
||||
test_sip002_tag();
|
||||
test_sip002_plugin();
|
||||
test_sip002_trailing_slash();
|
||||
test_sip002_ipv6();
|
||||
test_sip002_plain_userinfo();
|
||||
test_sip002_2022_psk();
|
||||
test_legacy();
|
||||
test_rust_ssurl_output();
|
||||
test_rejects_malformed();
|
||||
test_free_is_safe();
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user