diff --git a/TODO.txt b/TODO.txt index b009364..49d4f37 100644 --- a/TODO.txt +++ b/TODO.txt @@ -18,11 +18,11 @@ Milestone: v0.3.0 [~] HTTP Client API [ ] Document HttpParseHeaders() [ ] HttpClient man page - [ ] Uia man page + [ ] Uri man page [ ] Test on other platforms [ ] Option to pretty-print Json -[ ] Simple command line tool to make matrix requests +[x] Simple command line tool to make matrix requests - Built on HTTP client API [ ] Simple command line tool for working with JSON - Like a simpler version of jq diff --git a/src/include/HttpClient.h b/src/include/HttpClient.h index f655ad9..6f4b180 100644 --- a/src/include/HttpClient.h +++ b/src/include/HttpClient.h @@ -41,7 +41,7 @@ extern void HttpRequestHeader(HttpClientContext *, char *, char *); extern void -HttpRequestSendHeaders(HttpClientContext *); + HttpRequestSendHeaders(HttpClientContext *); extern HttpStatus HttpRequestSend(HttpClientContext *); diff --git a/tools/bin/td b/tools/bin/td index 6cd2e95..d9e3ec4 100644 --- a/tools/bin/td +++ b/tools/bin/td @@ -35,6 +35,8 @@ CFLAGS="${CFLAGS} ${DEFINES} ${INCLUDES}" LDFLAGS="${LDFLAGS} ${STATIC}" +MAIN="Telodendria" + if [ "$DEBUG" = "1" ]; then CFLAGS="$CFLAGS -O0 -g -pg" LDFLAGS="-lm -pthread -v" @@ -97,26 +99,41 @@ recipe_build() { do_rebuild=0 objs="" for src in $(find . -name '*.c' | cut -d '/' -f 2-); do - obj=$(echo "../build/$src" | sed -e 's/\.c$/\.o/') - objs="$objs $obj" + obj=$(echo "build/$src" | sed -e 's/\.c$/\.o/') + + if [ $(basename "$obj" .o) != "$MAIN" ]; then + objs="$objs $obj" + fi - if [ $(mod_time "$src") -ge $(mod_time "$obj") ]; then + if [ $(mod_time "$src") -ge $(mod_time "../$obj") ]; then echo "CC $(basename $obj)" - obj_dir=$(dirname "$obj") + obj_dir=$(dirname "../$obj") mkdir -p "$obj_dir" - if ! $CC $CFLAGS -Iinclude -c -o "$obj" "$src"; then + if ! $CC $CFLAGS -Iinclude -c -o "../$obj" "$src"; then exit 1 fi do_rebuild=1 fi done - if [ $do_rebuild -eq 1 ] || [ ! -f "../build/$PROG" ]; then + cd .. + if [ $do_rebuild -eq 1 ] || [ ! -f "build/$PROG" ]; then echo "LD $PROG" - $CC $LDFLAGS -o "../build/$PROG" $objs - else - echo "Up to date." + $CC $LDFLAGS -o "build/$PROG" $objs "build/$MAIN.o" fi + + for src in $(find tools/src -name '*.c'); do + out=$(basename "$src" .c) + out="build/tools/$out" + + if [ $(mod_time "$src") -ge $(mod_time "$out") ]; then + echo "CC $(basename $out)" + mkdir -p "$(dirname $out)" + if ! $CC $CFLAGS $LDFLAGS -Isrc/include -o "$out" $objs "$src"; then + exit 1 + fi + fi + done } recipe_run() { @@ -136,7 +153,7 @@ recipe_clean() { # Format the source code by updating the copyright headers and # then running indent(1) on each source code file. recipe_format() { - find src -name '*.c' -or -name '*.h' | while IFS= read -r src; do + find . -name '*.c' -or -name '*.h' | while IFS= read -r src; do if [ -t 1 ]; then printf "FMT %s%*c\r" $(basename "$src") "16" " " fi diff --git a/tools/env.sh b/tools/env.sh index 084a93b..9ffa4b1 100644 --- a/tools/env.sh +++ b/tools/env.sh @@ -4,14 +4,17 @@ echo "Telodendria Development Environment" echo "-----------------------------------" echo echo "Tools available:" -find tools/bin -type f | grep -v CVS | while IFS= read -r tool; do +find tools/bin -type f | grep -v CVS | grep -v '#' | while IFS= read -r tool; do echo "- $(basename $tool)" chmod +x "$tool" done +find tools/src -type f -name '*.c' | while IFS= read -r tool; do + echo "- $(basename $tool .c)" +done if which makewhatis 2>&1 > /dev/null; then makewhatis "$(pwd)/man" fi -export PATH="$(pwd)/tools/bin:$PATH" +export PATH="$(pwd)/tools/bin:$(pwd)/build/tools:$PATH" export MANPATH="$(pwd)/man:$MANPATH" diff --git a/tools/src/http.c b/tools/src/http.c new file mode 100644 index 0000000..c93f5b1 --- /dev/null +++ b/tools/src/http.c @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022-2023 Jordan Bancino <@jordan:bancino.net> + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define FLAG_HEADERS (1 << 0) + +/* TODO: Will eventually be provided by the Stream API */ +static void +StreamCopy(FILE * in, FILE * out) +{ + int c; + + /* TODO: This should be buffered, not char-by-char */ + while ((c = fgetc(in)) != EOF) + { + fputc(c, out); + } +} + +static void +usage(char *prog) +{ + fprintf(stderr, "Usage: %s [-h] METHOD url\n", prog); +} + +int +main(int argc, char **argv) +{ + HttpClientContext *cx; + HttpStatus res; + HttpRequestMethod method = HTTP_GET; + Uri *uri; + + HashMap *requestHeaders = HashMapCreate(); + char *key; + char *val; + + int flags = 0; + int requestFlags = HTTP_NONE; + + int ch; + + while ((ch = getopt(argc, argv, "iH:X:")) != -1) + { + switch (ch) + { + case 'i': + flags |= FLAG_HEADERS; + break; + case 'X': + method = HttpRequestMethodFromString(optarg); + if (!method) + { + fprintf(stderr, "Unknown request method: %s\n", optarg); + return -1; + } + break; + case 'H': + key = optarg; + val = optarg; + + while (*val && *val != ':') + { + val++; + } + + *val = '\0'; + val++; + + while (*val && isspace(*val)) + { + val++; + } + + HashMapSet(requestHeaders, key, StrDuplicate(val)); + break; + default: + usage(argv[0]); + return -1; + } + } + + if (argc - optind < 1) + { + usage(argv[0]); + return -1; + } + + uri = UriParse(argv[optind]); + if (!uri) + { + fprintf(stderr, "Failed to parse URI: %s\n", argv[optind]); + return -1; + } + + if (!uri->port) + { + if (strcmp(uri->proto, "https") == 0) + { + uri->port = 443; + } + else if (strcmp(uri->proto, "http") == 0) + { + uri->port = 80; + } + } + + if (!uri->port) + { + fprintf(stderr, "Unknown protocol: %s\n", uri->proto); + UriFree(uri); + return -1; + } + + if (strcmp(uri->proto, "https") == 0) + { + requestFlags |= HTTP_TLS; + } + + cx = HttpRequest(method, requestFlags, uri->port, uri->host, uri->path); + + if (!cx) + { + fprintf(stderr, "Failed to connect.\n"); + UriFree(uri); + return -1; + } + + while (HashMapIterate(requestHeaders, &key, (void **) &val)) + { + HttpRequestHeader(cx, key, val); + Free(val); + } + + HttpRequestSendHeaders(cx); + HashMapFree(requestHeaders); + + /* Only send stdin if it's not attached to a TTY. This prevents us + * from blocking if no pipe is provided */ + if (!isatty(fileno(stdin))) + { + StreamCopy(stdin, HttpClientStream(cx)); + } + + res = HttpRequestSend(cx); + + if (!res) + { + fprintf(stderr, "Failed to send request.\n"); + HttpClientContextFree(cx); + UriFree(uri); + return -1; + } + + if (flags & FLAG_HEADERS) + { + HashMap *responseHeaders = HttpResponseHeaders(cx); + + printf("HTTP/1.0 %d %s\n", res, HttpStatusToString(res)); + + while (HashMapIterate(responseHeaders, &key, (void **) &val)) + { + printf("%s: %s\n", key, val); + } + + printf("\n"); + } + + StreamCopy(HttpClientStream(cx), stdout); + + HttpClientContextFree(cx); + UriFree(uri); + + return !(res == HTTP_OK); +}