aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Makefile9
-rw-r--r--regress/Makefile38
-rwxr-xr-xregress/err3
-rwxr-xr-xregress/genbigfile29
-rwxr-xr-xregress/gg.py31
-rwxr-xr-xregress/hello4
-rw-r--r--regress/iri_test.c251
-rwxr-xr-xregress/runtime163
-rwxr-xr-xregress/sha15
-rwxr-xr-xregress/slow6
11 files changed, 549 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index c77ea7c..ab5dd84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ config.h.old
config.log
config.log.old
configure.local
+regress/testdata
+regress/*.pem
+regress/reg.conf
diff --git a/Makefile b/Makefile
index 509eaac..e04c150 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: all static clean test install
+.PHONY: all static clean regress install
all: Makefile.local gmid TAGS
@@ -29,12 +29,15 @@ TAGS: ${SRCS}
-etags ${SRCS} || true
clean:
- rm -f *.o lex.yy.c y.tab.c y.tab.h y.output gmid iri_test
- rm -f Makefile.local
+ rm -f *.o lex.yy.c y.tab.c y.tab.h y.output gmid
+ make -C regress clean
iri_test: iri_test.o iri.o utf8.o
${CC} iri_test.o iri.o utf8.o -o iri_test ${LDFLAGS}
+regress: gmid
+ make -C regress all
+
test: gmid iri_test
@echo "IRI tests"
@echo "=============================="
diff --git a/regress/Makefile b/regress/Makefile
new file mode 100644
index 0000000..67948a4
--- /dev/null
+++ b/regress/Makefile
@@ -0,0 +1,38 @@
+include ../Makefile.local
+
+.PHONY: all clean runtime
+
+all: iri_test runtime
+ ./iri_test
+
+iri_test: iri_test.o ../iri.o ../utf8.o
+ ${CC} iri_test.o ../iri.o ../utf8.o -o iri_test ${LDFLAGS}
+
+key.pem: cert.pem
+
+# XXX: key size is NOT GOOD. This is only for testing. Smaller keys
+# are quicker to generate. DON'T DO THIS AT HOME.
+cert.pem:
+ printf ".\n.\n.\n.\n.\nlocalhost\n.\n" | \
+ openssl req -x509 -newkey rsa:1024 \
+ -keyout key.pem \
+ -out cert.pem \
+ -days 365 -nodes
+ @echo
+
+clean:
+ rm -f *.o iri_test cert.pem key.pem
+ rm -rf testdata
+
+testdata:
+ mkdir testdata
+ ./genbigfile testdata/bigfile
+ ./sha testdata/bigfile testdata/bigfile.sha
+ printf "# hello world\n" > testdata/index.gmi
+ ./sha testdata/index.gmi testdata/index.gmi.sha
+ cp hello slow err testdata/
+ mkdir testdata/dir
+ cp testdata/index.gmi testdata/dir/foo.gmi
+
+runtime: testdata cert.pem
+ ./runtime
diff --git a/regress/err b/regress/err
new file mode 100755
index 0000000..2bb8d86
--- /dev/null
+++ b/regress/err
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exit 1
diff --git a/regress/genbigfile b/regress/genbigfile
new file mode 100755
index 0000000..a4183f0
--- /dev/null
+++ b/regress/genbigfile
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+set -e
+
+dotimes() {
+ if which jot 2>/dev/null >/dev/null; then
+ jot "$@"
+ elif which seq 2>/dev/null >/dev/null; then
+ seq "$@"
+ else
+ echo "no jot/seq binary found"
+ exit 1
+ fi
+}
+
+file="$1"
+
+if [ -z "$file" ]; then
+ echo "USAGE: $(dirname "$0") <filename>"
+ exit 1
+fi
+
+printf "" > "$file"
+
+for i in `dotimes 1024`; do
+ for j in `dotimes 1024`; do
+ echo "a" >> "$file"
+ done
+done
diff --git a/regress/gg.py b/regress/gg.py
new file mode 100755
index 0000000..e421dcd
--- /dev/null
+++ b/regress/gg.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+# GeminiGet, aka gg
+# USAGE: ./gg path [port]
+
+import os
+import socket
+import ssl
+import urllib.parse
+import sys
+
+hostname = 'localhost'
+path = sys.argv[1]
+
+port = 1965
+if len(sys.argv) > 2:
+ port = int(sys.argv[2])
+
+s = socket.create_connection((hostname, port))
+context = ssl.SSLContext()
+context.check_hostname = False
+context.verify_mode = ssl.CERT_NONE
+s = context.wrap_socket(s, server_hostname = hostname)
+s.sendall(("gemini://" + hostname + ":" + str(port) + path + "\r\n").encode('UTF-8'))
+
+try:
+ fp = s.makefile("rb")
+ for line in fp.read().splitlines():
+ print(line.decode('UTF-8'))
+except:
+ pass
diff --git a/regress/hello b/regress/hello
new file mode 100755
index 0000000..d70b64f
--- /dev/null
+++ b/regress/hello
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+printf "20 text/gemini\r\n"
+echo "# hello world"
diff --git a/regress/iri_test.c b/regress/iri_test.c
new file mode 100644
index 0000000..4710530
--- /dev/null
+++ b/regress/iri_test.c
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2020 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and 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 <err.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "../gmid.h"
+
+#define TEST(iri, fail, exp, descr) \
+ if (!run_test(iri, fail, exp)) { \
+ fprintf(stderr, "%s:%d: error: %s\n", \
+ __FILE__, __LINE__, descr); \
+ exit(1); \
+ }
+
+#define IRI(schema, host, port, path, query, frag) \
+ ((struct iri){(char*)schema, (char*)host, (char*)port, \
+ 0, (char*)path, (char*)query, \
+ (char*)frag})
+
+#define DIFF(wanted, got, field) \
+ if (wanted->field == NULL || got->field == NULL || \
+ strcmp(wanted->field, got->field)) { \
+ fprintf(stderr, #field ":\n\tgot: %s\n\twanted: %s\n", \
+ got->field, wanted->field); \
+ return 0; \
+ }
+
+#define PASS 0
+#define FAIL 1
+
+int diff_iri(struct iri*, struct iri*);
+int run_test(const char*, int, struct iri);
+
+int
+diff_iri(struct iri *p, struct iri *exp)
+{
+ DIFF(p, exp, schema);
+ DIFF(p, exp, host);
+ DIFF(p, exp, port);
+ DIFF(p, exp, path);
+ DIFF(p, exp, query);
+ DIFF(p, exp, fragment);
+ return 1;
+}
+
+int
+run_test(const char *iri, int should_fail, struct iri expected)
+{
+ int failed, ok = 1;
+ char *iri_copy;
+ struct iri parsed;
+ const char *error;
+
+ if ((iri_copy = strdup(iri)) == NULL)
+ err(1, "strdup");
+
+ fprintf(stderr, "=> %s\n", iri);
+ failed = !parse_iri(iri_copy, &parsed, &error);
+
+ if (failed && should_fail)
+ goto done;
+
+ if (error != NULL)
+ fprintf(stderr, "> %s\n", error);
+
+ ok = !failed && !should_fail;
+ if (ok)
+ ok = diff_iri(&expected, &parsed);
+
+done:
+ free(iri_copy);
+ return ok;
+}
+
+int
+main(void)
+{
+ struct iri empty = IRI("", "", "", "", "", "");
+
+ TEST("http://omarpolo.com",
+ PASS,
+ IRI("http", "omarpolo.com", "", "", "", ""),
+ "can parse iri with empty path");
+
+ /* schema */
+ TEST("omarpolo.com", FAIL, empty, "FAIL when the schema is missing");
+ TEST("gemini:/omarpolo.com", FAIL, empty, "FAIL with invalid marker");
+ TEST("gemini//omarpolo.com", FAIL, empty, "FAIL with invalid marker");
+ TEST("h!!p://omarpolo.com", FAIL, empty, "FAIL with invalid schema");
+ TEST("GEMINI://omarpolo.com",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "", "", ""),
+ "Schemas are case insensitive.");
+
+ /* authority */
+ TEST("gemini://omarpolo.com",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "", "", ""),
+ "can parse authority with empty path");
+ TEST("gemini://omarpolo.com/",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "", "", ""),
+ "can parse authority with empty path (alt)")
+ TEST("gemini://omarpolo.com:1965",
+ PASS,
+ IRI("gemini", "omarpolo.com", "1965", "", "", ""),
+ "can parse with port and empty path");
+ TEST("gemini://omarpolo.com:1965/",
+ PASS,
+ IRI("gemini", "omarpolo.com", "1965", "", "", ""),
+ "can parse with port and empty path")
+ TEST("gemini://omarpolo.com:196s",
+ FAIL,
+ empty,
+ "FAIL with invalid port number");
+ TEST("gemini://OmArPoLo.CoM",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "", "", ""),
+ "host is case-insensitive");
+
+ /* path */
+ TEST("gemini://omarpolo.com/foo/bar/baz",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "foo/bar/baz", "", ""),
+ "parse simple paths");
+ TEST("gemini://omarpolo.com/foo//bar///baz",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "foo/bar/baz", "", ""),
+ "parse paths with multiple slashes");
+ TEST("gemini://omarpolo.com/foo/./bar/./././baz",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "foo/bar/baz", "", ""),
+ "parse paths with . elements");
+ TEST("gemini://omarpolo.com/foo/bar/../bar/baz",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "foo/bar/baz", "", ""),
+ "parse paths with .. elements");
+ TEST("gemini://omarpolo.com/foo/../foo/bar/../bar/baz/../baz",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "foo/bar/baz", "", ""),
+ "parse paths with multiple .. elements");
+ TEST("gemini://omarpolo.com/foo/..",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "", "", ""),
+ "parse paths with a trailing ..");
+ TEST("gemini://omarpolo.com/foo/../",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "", "", ""),
+ "parse paths with a trailing ..");
+ TEST("gemini://omarpolo.com/foo/../..",
+ FAIL,
+ empty,
+ "reject paths that would escape the root");
+ TEST("gemini://omarpolo.com/foo/../../",
+ FAIL,
+ empty,
+ "reject paths that would escape the root")
+ TEST("gemini://omarpolo.com/foo/../foo/../././/bar/baz/.././.././/",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "", "", ""),
+ "parse path with lots of cleaning available");
+ TEST("gemini://omarpolo.com//foo",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "foo", "", ""),
+ "Trim initial slashes");
+ TEST("gemini://omarpolo.com/////foo",
+ PASS,
+ IRI("gemini", "omarpolo.com", "", "foo", "", ""),
+ "Trim initial slashes (pt. 2)");
+
+ /* query */
+ TEST("foo://example.com/foo/?gne",
+ PASS,
+ IRI("foo", "example.com", "", "foo/", "gne", ""),
+ "parse query strings");
+ TEST("foo://example.com/foo/?gne&foo",
+ PASS,
+ IRI("foo", "example.com", "", "foo/", "gne&foo", ""),
+ "parse query strings");
+ TEST("foo://example.com/foo/?gne%2F",
+ PASS,
+ IRI("foo", "example.com", "", "foo/", "gne/", ""),
+ "parse query strings");
+
+ /* fragment */
+ TEST("foo://bar.co/#foo",
+ PASS,
+ IRI("foo", "bar.co", "", "", "", "foo"),
+ "can recognize fragments");
+
+ /* percent encoding */
+ TEST("foo://bar.com/caf%C3%A8.gmi",
+ PASS,
+ IRI("foo", "bar.com", "", "cafè.gmi", "", ""),
+ "can decode");
+ TEST("foo://bar.com/caff%C3%A8%20macchiato.gmi",
+ PASS,
+ IRI("foo", "bar.com", "", "caffè macchiato.gmi", "", ""),
+ "can decode");
+ TEST("foo://bar.com/caff%C3%A8+macchiato.gmi",
+ PASS,
+ IRI("foo", "bar.com", "", "caffè+macchiato.gmi", "", ""),
+ "can decode");
+ TEST("foo://bar.com/foo%2F..%2F..",
+ FAIL,
+ empty,
+ "conversion and checking are done in the correct order");
+ TEST("foo://bar.com/foo%00?baz",
+ FAIL,
+ empty,
+ "rejects %00");
+
+ /* IRI */
+ TEST("foo://bar.com/cafè.gmi",
+ PASS,
+ IRI("foo", "bar.com", "", "cafè.gmi", "" , ""),
+ "decode IRI (with a 2-byte utf8 seq)");
+ TEST("foo://bar.com/世界.gmi",
+ PASS,
+ IRI("foo", "bar.com", "", "世界.gmi", "" , ""),
+ "decode IRI");
+ TEST("foo://bar.com/😼.gmi",
+ PASS,
+ IRI("foo", "bar.com", "", "😼.gmi", "" , ""),
+ "decode IRI (with a 3-byte utf8 seq)");
+ TEST("foo://bar.com/😼/𤭢.gmi",
+ PASS,
+ IRI("foo", "bar.com", "", "😼/𤭢.gmi", "" , ""),
+ "decode IRI (with a 3-byte and a 4-byte utf8 seq)");
+ TEST("foo://bar.com/世界/\xC0\x80",
+ FAIL,
+ empty,
+ "reject invalid sequence (overlong NUL)");
+
+ return 0;
+}
diff --git a/regress/runtime b/regress/runtime
new file mode 100755
index 0000000..e9f193c
--- /dev/null
+++ b/regress/runtime
@@ -0,0 +1,163 @@
+#!/bin/sh
+
+set -e
+
+# usage: config <global config> <stuff for localhost>
+# generates a configuration file reg.conf
+config() {
+ cat <<EOF > reg.conf
+daemon off
+ipv6 off
+port 10965
+$1
+server "localhost" {
+ cert "cert.pem"
+ key "key.pem"
+ root "testdata"
+ $2
+}
+EOF
+}
+
+checkconf() {
+ ./../gmid -n -c reg.conf
+}
+
+# usage: get <path>
+# return the body of the request on stdout
+get() {
+ (./gg.py "$1" 10965 | sed 1d) || true
+}
+
+# usage: head <path>
+# return the meta response line on stdout
+head() {
+ (./gg.py "$1" 10965 | sed 1q) || true
+}
+
+run() {
+ # filter out logs for GET requests
+ (./../gmid -c reg.conf 2>&1 | grep -v GET) >&2 &
+ pid=$!
+}
+
+# usage: check [exit-message]
+# check if gmid is still running
+check() {
+ if ! ps $pid >/dev/null; then
+ echo ${1:-"gmid crashed?"}
+ exit 1
+ fi
+}
+
+# quit gmid
+quit() {
+ pkill gmid || true
+ wait || true
+}
+
+# usage: eq a b errmsg
+# if a and b aren't equal strings, exit with errmsg
+eq() {
+ if ! [ "$1" = "$2" ]; then
+ echo "$3: \"$1\" not equal \"$2\""
+ exit 1
+ fi
+}
+
+onexit() {
+ rm -f bigfile bigfile.sha
+ quit
+}
+
+# tests
+
+trap 'onexit' INT TERM EXIT
+
+endl=`printf "\r\n"`
+lf=`echo`
+
+config "" ""
+checkconf
+run
+
+eq "$(head /)" "20 text/gemini" "Unexpected head for /"
+eq "$(get /)" "# hello world$ln" "Unexpected body for /"
+echo OK GET /
+
+eq "$(head /foo)" "51 not found" "Unexpected head /foo"
+eq "$(get /foo)" "" "Unexpected body /foo"
+echo OK GET /foo
+
+# should redirect if asked for a directory but without the trailing /
+eq "$(head /dir)" "30 /dir/" "Unexpected redirect for /dir"
+eq "$(get /dir)" "" "Unexpected body for redirect"
+echo OK GET /dir
+
+# 51 for a directory without index.gmi
+eq "$(head /dir/)" "51 not found" "Unexpected head for /"
+eq "$(get /dir/)" "" "Unexpected body for error"
+echo OK GET /dir/
+
+eq "$(head /dir/foo.gmi)" "20 text/gemini" "Unexpected head for /dir/foo.gmi"
+eq "$(get /dir/foo.gmi)" "# hello world$ln" "Unexpected body for /dir/foo.gmi"
+echo OK GET /dir/foo.gmi
+
+# try a big file
+eq "$(head /bigfile)" "20 application/octet-stream" "Unexpected head for /bigfile"
+get /bigfile > bigfile
+./sha bigfile bigfile.sha
+eq "$(cat bigfile.sha)" "$(cat testdata/bigfile.sha)" "Unexpected sha for /bigfile"
+echo OK GET /bigfile
+
+# shouldn't be executing cgi scripts
+eq "$(head /hello)" "20 application/octet-stream" "Unexpected head for /hello"
+echo OK GET /hello
+
+check "should be running"
+quit
+
+# try with custom mime
+config 'mime "text/x-funny-text" "gmi"' 'default type "application/x-trash"'
+checkconf
+run
+
+eq "$(head /)" "20 text/x-funny-text" "Unexpected head for /"
+echo OK GET / with custom mime
+
+eq "$(head /hello)" "20 application/x-trash" "Unexpected head for /hello"
+echo OK GET /hello with custom mime
+
+check "should be running"
+quit
+
+# try with custom lang
+config '' 'lang "it"'
+checkconf
+run
+
+eq "$(head /)" "20 text/gemini; lang=it" "Unexpected head for /"
+echo OK GET / with custom lang
+
+check "should be running"
+quit
+
+# finally try with CGI scripts
+config '' 'cgi ""'
+checkconf
+run
+
+eq "$(head /hello)" "20 text/gemini" "Unexpected head for /hello"
+eq "$(get /hello)" "# hello world$ln" "Unexpected body for /hello"
+echo OK GET /hello with cgi
+
+eq "$(head /slow)" "20 text/gemini" "Unexpected head for /slow"
+eq "$(get /slow)" "# hello world$ln" "Unexpected body for /slow"
+echo OK GET /slow with cgi
+
+eq "$(head /err)" "" "Unexpected head for /err"
+eq "$(get /err)" "" "Unexpected body for /err"
+echo OK GET /err with cgi
+
+check "should be running"
+quit
diff --git a/regress/sha b/regress/sha
new file mode 100755
index 0000000..12cf582
--- /dev/null
+++ b/regress/sha
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# USAGE: ./sha in out
+# writes the sha256 of in to file out
+
+if which sha256 2>/dev/null >/dev/null; then
+ exec sha256 < "$1" > "$2"
+fi
+
+if which sha256sum 2>/dev/null >/dev/null; then
+ exec sha256sum "$1" | awk '{print $1}' > "$2"
+fi
+
+echo "No sha binary found"
+exit 1
diff --git a/regress/slow b/regress/slow
new file mode 100755
index 0000000..2ceb52c
--- /dev/null
+++ b/regress/slow
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+printf "20 "
+sleep 1
+printf "text/gemini\r\n"
+echo "# hello world"