Skip to content

Commit ea3017a

Browse files
committed
Add URL rewrites to HTTP server
1 parent 2743cd5 commit ea3017a

File tree

10 files changed

+239
-13
lines changed

10 files changed

+239
-13
lines changed

examples/restful_server/restful_server.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ int main(int argc, char *argv[]) {
6464
s_http_server_opts.global_auth_file = argv[++i];
6565
} else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
6666
s_http_server_opts.per_directory_auth_file = argv[++i];
67+
} else if (strcmp(argv[i], "-r") == 0 && i + 1 < argc) {
68+
s_http_server_opts.url_rewrites = argv[++i];
6769
#ifdef NS_ENABLE_SSL
6870
} else if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) {
6971
const char *ssl_cert = argv[++i];

fossa.c

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3231,6 +3231,34 @@ static int find_index_file(char *path, size_t path_len, ns_stat_t *stp) {
32313231
return found;
32323232
}
32333233

3234+
static void uri_to_path(struct http_message *hm, char *buf, size_t buf_len,
3235+
const char *document_root, const char *rewrites) {
3236+
char uri[NS_MAX_PATH];
3237+
struct ns_str a, b, *host_hdr = ns_get_http_header(hm, "Host");
3238+
3239+
ns_url_decode(hm->uri.p, hm->uri.len, uri, sizeof(uri), 0);
3240+
remove_double_dots(uri);
3241+
snprintf(buf, buf_len, "%s%s", document_root, uri);
3242+
3243+
/* Handle URL rewrites */
3244+
while ((rewrites = ns_next_comma_list_entry(rewrites, &a, &b)) != NULL) {
3245+
if (a.len > 1 && a.p[0] == '@' && host_hdr != NULL &&
3246+
host_hdr->len == a.len - 1 &&
3247+
ns_ncasecmp(a.p + 1, host_hdr->p, a.len - 1) == 0) {
3248+
/* This is a virtual host rewrite: @domain.name=document_root_dir */
3249+
snprintf(buf, buf_len, "%.*s%s", (int) b.len, b.p, uri);
3250+
break;
3251+
} else {
3252+
/* This is a usual rewrite, URI=directory */
3253+
int match_len = ns_match_prefix(a.p, a.len, uri);
3254+
if (match_len > 0) {
3255+
snprintf(buf, buf_len, "%.*s%s", (int) b.len, b.p, uri + match_len);
3256+
break;
3257+
}
3258+
}
3259+
}
3260+
}
3261+
32343262
/*
32353263
* Serve given HTTP request according to the `options`.
32363264
*
@@ -3256,15 +3284,12 @@ static int find_index_file(char *path, size_t path_len, ns_stat_t *stp) {
32563284
*/
32573285
void ns_serve_http(struct ns_connection *nc, struct http_message *hm,
32583286
struct ns_serve_http_opts opts) {
3259-
char path[NS_MAX_PATH], tmp[NS_MAX_PATH];
3287+
char path[NS_MAX_PATH];
32603288
ns_stat_t st;
32613289
int stat_result, is_directory;
32623290
uint32_t remote_ip = ntohl(*(uint32_t *) &nc->sa.sin.sin_addr);
32633291

3264-
snprintf(tmp, sizeof(tmp), "%s/%.*s", opts.document_root, (int) hm->uri.len,
3265-
hm->uri.p);
3266-
ns_url_decode(tmp, strlen(tmp), path, sizeof(path), 0);
3267-
remove_double_dots(path);
3292+
uri_to_path(hm, path, sizeof(path), opts.document_root, opts.url_rewrites);
32683293
stat_result = ns_stat(path, &st);
32693294
is_directory = !stat_result && S_ISDIR(st.st_mode);
32703295

@@ -3278,6 +3303,11 @@ void ns_serve_http(struct ns_connection *nc, struct http_message *hm,
32783303
"realm=\"%s\", nonce=\"%lu\"\r\n"
32793304
"Content-Length: 0\r\n\r\n",
32803305
opts.auth_domain, (unsigned long) time(NULL));
3306+
} else if (is_directory && path[strlen(path) - 1] != '/') {
3307+
ns_printf(nc,
3308+
"HTTP/1.1 301 Moved\r\nLocation: %.*s/\r\n"
3309+
"Content-Length: 0\r\n\r\n",
3310+
(int) hm->uri.len, hm->uri.p);
32813311
} else if (stat_result != 0) {
32823312
ns_printf(nc, "%s", "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n");
32833313
} else if (S_ISDIR(st.st_mode) && !find_index_file(path, sizeof(path), &st)) {
@@ -3957,6 +3987,48 @@ const char *ns_next_comma_list_entry(const char *list, struct ns_str *val,
39573987

39583988
return list;
39593989
}
3990+
3991+
/*
3992+
* Match 0-terminated string against a glob pattern.
3993+
* Match is case-insensitive. Return number of bytes matched, or -1 if no match.
3994+
*/
3995+
int ns_match_prefix(const char *pattern, int pattern_len, const char *str) {
3996+
const char *or_str;
3997+
int len, res, i = 0, j = 0;
3998+
3999+
if ((or_str = (const char *) memchr(pattern, '|', pattern_len)) != NULL) {
4000+
res = ns_match_prefix(pattern, or_str - pattern, str);
4001+
return res > 0 ? res : ns_match_prefix(
4002+
or_str + 1,
4003+
(pattern + pattern_len) - (or_str + 1), str);
4004+
}
4005+
4006+
for (; i < pattern_len; i++, j++) {
4007+
if (pattern[i] == '?' && str[j] != '\0') {
4008+
continue;
4009+
} else if (pattern[i] == '$') {
4010+
return str[j] == '\0' ? j : -1;
4011+
} else if (pattern[i] == '*') {
4012+
i++;
4013+
if (pattern[i] == '*') {
4014+
i++;
4015+
len = (int) strlen(str + j);
4016+
} else {
4017+
len = (int) strcspn(str + j, "/");
4018+
}
4019+
if (i == pattern_len) {
4020+
return j + len;
4021+
}
4022+
do {
4023+
res = ns_match_prefix(pattern + i, pattern_len - i, str + j + len);
4024+
} while (res == -1 && len-- > 0);
4025+
return res == -1 ? -1 : j + res + len;
4026+
} else if (lowercase(&pattern[i]) != lowercase(&str[j])) {
4027+
return -1;
4028+
}
4029+
}
4030+
return j;
4031+
}
39604032
#ifdef NS_MODULE_LINES
39614033
#line 1 "src/json-rpc.c"
39624034
/**/

fossa.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ int ns_avprintf(char **buf, size_t size, const char *fmt, va_list ap);
520520
int ns_is_big_endian(void);
521521
const char *ns_next_comma_list_entry(const char *list, struct ns_str *val,
522522
struct ns_str *eq_val);
523+
int ns_match_prefix(const char *pattern, int pattern_len, const char *str);
523524

524525
#ifdef __cplusplus
525526
}
@@ -656,6 +657,23 @@ struct ns_serve_http_opts {
656657

657658
/* IP ACL. By default, NULL, meaning all IPs are allowed to connect */
658659
const char *ip_acl;
660+
661+
/* URL rewrites.
662+
*
663+
* Comma-separated list of `uri_pattern=file_or_directory_path` rewrites.
664+
* When HTTP request is received, Fossa constructs a file name from the
665+
* requested URI by combining `document_root` and the URI. However, if the
666+
* rewrite option is used and `uri_pattern` matches requested URI, then
667+
* `document_root` is ignored. Instead, `file_or_directory_path` is used,
668+
* which should be a full path name or a path relative to the web server's
669+
* current working directory. Note that `uri_pattern`, as all Fossa patterns,
670+
* is a prefix pattern.
671+
*
672+
* If uri_pattern starts with `@` symbol, then Fossa compares it with the
673+
* HOST header of the request. If they are equal, Fossa sets document root
674+
* to `file_or_directory_path`, implementing virtual hosts support.
675+
*/
676+
const char *url_rewrites;
659677
};
660678
void ns_serve_http(struct ns_connection *, struct http_message *,
661679
struct ns_serve_http_opts);

src/http.c

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,34 @@ static int find_index_file(char *path, size_t path_len, ns_stat_t *stp) {
13531353
return found;
13541354
}
13551355

1356+
static void uri_to_path(struct http_message *hm, char *buf, size_t buf_len,
1357+
const char *document_root, const char *rewrites) {
1358+
char uri[NS_MAX_PATH];
1359+
struct ns_str a, b, *host_hdr = ns_get_http_header(hm, "Host");
1360+
1361+
ns_url_decode(hm->uri.p, hm->uri.len, uri, sizeof(uri), 0);
1362+
remove_double_dots(uri);
1363+
snprintf(buf, buf_len, "%s%s", document_root, uri);
1364+
1365+
/* Handle URL rewrites */
1366+
while ((rewrites = ns_next_comma_list_entry(rewrites, &a, &b)) != NULL) {
1367+
if (a.len > 1 && a.p[0] == '@' && host_hdr != NULL &&
1368+
host_hdr->len == a.len - 1 &&
1369+
ns_ncasecmp(a.p + 1, host_hdr->p, a.len - 1) == 0) {
1370+
/* This is a virtual host rewrite: @domain.name=document_root_dir */
1371+
snprintf(buf, buf_len, "%.*s%s", (int) b.len, b.p, uri);
1372+
break;
1373+
} else {
1374+
/* This is a usual rewrite, URI=directory */
1375+
int match_len = ns_match_prefix(a.p, a.len, uri);
1376+
if (match_len > 0) {
1377+
snprintf(buf, buf_len, "%.*s%s", (int) b.len, b.p, uri + match_len);
1378+
break;
1379+
}
1380+
}
1381+
}
1382+
}
1383+
13561384
/*
13571385
* Serve given HTTP request according to the `options`.
13581386
*
@@ -1378,15 +1406,12 @@ static int find_index_file(char *path, size_t path_len, ns_stat_t *stp) {
13781406
*/
13791407
void ns_serve_http(struct ns_connection *nc, struct http_message *hm,
13801408
struct ns_serve_http_opts opts) {
1381-
char path[NS_MAX_PATH], tmp[NS_MAX_PATH];
1409+
char path[NS_MAX_PATH];
13821410
ns_stat_t st;
13831411
int stat_result, is_directory;
13841412
uint32_t remote_ip = ntohl(*(uint32_t *) &nc->sa.sin.sin_addr);
13851413

1386-
snprintf(tmp, sizeof(tmp), "%s/%.*s", opts.document_root, (int) hm->uri.len,
1387-
hm->uri.p);
1388-
ns_url_decode(tmp, strlen(tmp), path, sizeof(path), 0);
1389-
remove_double_dots(path);
1414+
uri_to_path(hm, path, sizeof(path), opts.document_root, opts.url_rewrites);
13901415
stat_result = ns_stat(path, &st);
13911416
is_directory = !stat_result && S_ISDIR(st.st_mode);
13921417

@@ -1400,6 +1425,11 @@ void ns_serve_http(struct ns_connection *nc, struct http_message *hm,
14001425
"realm=\"%s\", nonce=\"%lu\"\r\n"
14011426
"Content-Length: 0\r\n\r\n",
14021427
opts.auth_domain, (unsigned long) time(NULL));
1428+
} else if (is_directory && path[strlen(path) - 1] != '/') {
1429+
ns_printf(nc,
1430+
"HTTP/1.1 301 Moved\r\nLocation: %.*s/\r\n"
1431+
"Content-Length: 0\r\n\r\n",
1432+
(int) hm->uri.len, hm->uri.p);
14031433
} else if (stat_result != 0) {
14041434
ns_printf(nc, "%s", "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n");
14051435
} else if (S_ISDIR(st.st_mode) && !find_index_file(path, sizeof(path), &st)) {

src/http.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,23 @@ struct ns_serve_http_opts {
130130

131131
/* IP ACL. By default, NULL, meaning all IPs are allowed to connect */
132132
const char *ip_acl;
133+
134+
/* URL rewrites.
135+
*
136+
* Comma-separated list of `uri_pattern=file_or_directory_path` rewrites.
137+
* When HTTP request is received, Fossa constructs a file name from the
138+
* requested URI by combining `document_root` and the URI. However, if the
139+
* rewrite option is used and `uri_pattern` matches requested URI, then
140+
* `document_root` is ignored. Instead, `file_or_directory_path` is used,
141+
* which should be a full path name or a path relative to the web server's
142+
* current working directory. Note that `uri_pattern`, as all Fossa patterns,
143+
* is a prefix pattern.
144+
*
145+
* If uri_pattern starts with `@` symbol, then Fossa compares it with the
146+
* HOST header of the request. If they are equal, Fossa sets document root
147+
* to `file_or_directory_path`, implementing virtual hosts support.
148+
*/
149+
const char *url_rewrites;
133150
};
134151
void ns_serve_http(struct ns_connection *, struct http_message *,
135152
struct ns_serve_http_opts);

src/util.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,3 +457,45 @@ const char *ns_next_comma_list_entry(const char *list, struct ns_str *val,
457457

458458
return list;
459459
}
460+
461+
/*
462+
* Match 0-terminated string against a glob pattern.
463+
* Match is case-insensitive. Return number of bytes matched, or -1 if no match.
464+
*/
465+
int ns_match_prefix(const char *pattern, int pattern_len, const char *str) {
466+
const char *or_str;
467+
int len, res, i = 0, j = 0;
468+
469+
if ((or_str = (const char *) memchr(pattern, '|', pattern_len)) != NULL) {
470+
res = ns_match_prefix(pattern, or_str - pattern, str);
471+
return res > 0 ? res : ns_match_prefix(
472+
or_str + 1,
473+
(pattern + pattern_len) - (or_str + 1), str);
474+
}
475+
476+
for (; i < pattern_len; i++, j++) {
477+
if (pattern[i] == '?' && str[j] != '\0') {
478+
continue;
479+
} else if (pattern[i] == '$') {
480+
return str[j] == '\0' ? j : -1;
481+
} else if (pattern[i] == '*') {
482+
i++;
483+
if (pattern[i] == '*') {
484+
i++;
485+
len = (int) strlen(str + j);
486+
} else {
487+
len = (int) strcspn(str + j, "/");
488+
}
489+
if (i == pattern_len) {
490+
return j + len;
491+
}
492+
do {
493+
res = ns_match_prefix(pattern + i, pattern_len - i, str + j + len);
494+
} while (res == -1 && len-- > 0);
495+
return res == -1 ? -1 : j + res + len;
496+
} else if (lowercase(&pattern[i]) != lowercase(&str[j])) {
497+
return -1;
498+
}
499+
}
500+
return j;
501+
}

src/util.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ int ns_avprintf(char **buf, size_t size, const char *fmt, va_list ap);
3636
int ns_is_big_endian(void);
3737
const char *ns_next_comma_list_entry(const char *list, struct ns_str *val,
3838
struct ns_str *eq_val);
39+
int ns_match_prefix(const char *pattern, int pattern_len, const char *str);
3940

4041
#ifdef __cplusplus
4142
}

test/data/rewrites/foo.com/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo_root

test/data/rewrites/msg.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
works

test/unit_test.c

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,9 @@ static void cb1(struct ns_connection *nc, int ev, void *ev_data) {
591591
s_http_server_opts.per_directory_auth_file = "passwords.txt";
592592
s_http_server_opts.auth_domain = "foo.com";
593593
s_http_server_opts.ssi_suffix = ".shtml";
594+
s_http_server_opts.url_rewrites =
595+
"/~joe=./data/rewrites,"
596+
"@foo.com=./data/rewrites/foo.com";
594597
ns_serve_http(nc, hm, s_http_server_opts);
595598
}
596599
}
@@ -805,21 +808,21 @@ static const char *test_http_index(void) {
805808
ASSERT((nc = ns_connect(&mgr, local_addr, cb9)) != NULL);
806809
ns_set_protocol_http_websocket(nc);
807810
nc->user_data = buf;
808-
ns_printf(nc, "%s", "GET /data/dir_with_index HTTP/1.0\n\n");
811+
ns_printf(nc, "%s", "GET /data/dir_with_index/ HTTP/1.0\n\n");
809812

810813
/* Test directory with no index file. */
811814
ASSERT((nc = ns_connect(&mgr, local_addr, cb9)) != NULL);
812815
ns_set_protocol_http_websocket(nc);
813816
nc->user_data = buf2;
814-
ns_printf(nc, "%s", "GET /data/dir_no_index HTTP/1.0\n\n");
817+
ns_printf(nc, "%s", "GET /data/dir_no_index/ HTTP/1.0\n\n");
815818

816819
/* Run event loop. Use more cycles to let file download complete. */
817820
poll_mgr(&mgr, 50);
818821
ns_mgr_free(&mgr);
819822

820823
/* Check that test buffer has been filled by the callback properly. */
821824
ASSERT(strcmp(buf, "foo") == 0);
822-
ASSERT(strcmp(buf2, "116\r\n<html><head><t") == 0);
825+
ASSERT(strcmp(buf2, "118\r\n<html><head><t") == 0);
823826

824827
return NULL;
825828
}
@@ -849,6 +852,44 @@ static const char *test_ssi(void) {
849852
return NULL;
850853
}
851854

855+
static const char *test_http_rewrites(void) {
856+
struct ns_mgr mgr;
857+
struct ns_connection *nc;
858+
const char *local_addr = "127.0.0.1:7377";
859+
char buf[20] = "", buf2[20] = "", buf3[40] = "";
860+
861+
ns_mgr_init(&mgr, NULL);
862+
ASSERT((nc = ns_bind(&mgr, local_addr, cb1)) != NULL);
863+
ns_set_protocol_http_websocket(nc);
864+
865+
/* Test rewrite. */
866+
ASSERT((nc = ns_connect_http(&mgr, cb9, "127.0.0.1:7377/~joe/msg.txt",
867+
"Host: foo.co\r\n", NULL)) != NULL);
868+
nc->user_data = buf;
869+
870+
/* Test rewrite that points to directory, expect redirect */
871+
ASSERT((nc = ns_connect_http(&mgr, cb8, "http://127.0.0.1:7377/~joe", NULL,
872+
NULL)) != NULL);
873+
nc->user_data = buf3;
874+
875+
/* Test domain-based rewrite. */
876+
ASSERT((nc = ns_connect(&mgr, local_addr, cb9)) != NULL);
877+
ns_set_protocol_http_websocket(nc);
878+
nc->user_data = buf2;
879+
ns_printf(nc, "%s", "GET / HTTP/1.0\nHost: foo.com\n\n");
880+
881+
/* Run event loop. Use more cycles to let file download complete. */
882+
poll_mgr(&mgr, 50);
883+
ns_mgr_free(&mgr);
884+
885+
/* Check that test buffer has been filled by the callback properly. */
886+
ASSERT(strcmp(buf, "works\n") == 0);
887+
ASSERT(strcmp(buf2, "foo_root\n") == 0);
888+
ASSERT(strcmp(buf3, "HTTP/1.1 301 Moved\r\nLocation: /~joe/\r\nC") == 0);
889+
890+
return NULL;
891+
}
892+
852893
static void cb3(struct ns_connection *nc, int ev, void *ev_data) {
853894
struct websocket_message *wm = (struct websocket_message *) ev_data;
854895

@@ -2509,6 +2550,7 @@ static const char *run_tests(const char *filter) {
25092550
RUN_TEST(test_http_index);
25102551
RUN_TEST(test_http_parse_header);
25112552
RUN_TEST(test_ssi);
2553+
RUN_TEST(test_http_rewrites);
25122554
RUN_TEST(test_websocket);
25132555
RUN_TEST(test_websocket_big);
25142556
RUN_TEST(test_rpc);

0 commit comments

Comments
 (0)