From 26867f4767924990f9591f26a9ebdcb5b2c2d2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Wed, 25 Jan 2023 01:03:57 +0100 Subject: [PATCH] add files. it mostly compiles on posix now --- .gitignore | 7 + Makefile | 21 + README.md | 8 + espfsbuilder/.gitignore | 4 + espfsbuilder/Makefile | 12 + espfsbuilder/logging.h | 124 +++ espfsbuilder/main.c | 560 ++++++++++++++ lib/espfs/espfs.c | 319 ++++++++ lib/espfs/espfs.h | 41 + lib/espfs/espfsformat.h | 25 + lib/heatshrink/heatshrink_common.h | 17 + lib/heatshrink/heatshrink_config.h | 40 + lib/heatshrink/heatshrink_decoder.c | 374 +++++++++ lib/heatshrink/heatshrink_decoder.h | 97 +++ lib/heatshrink/heatshrink_encoder.c | 607 +++++++++++++++ lib/heatshrink/heatshrink_encoder.h | 106 +++ lib/include/auth.h | 19 + lib/include/cgiwebsocket.h | 33 + lib/include/httpd-platform.h | 39 + lib/include/httpd-utils.h | 10 + lib/include/httpd.h | 204 +++++ lib/include/httpdespfs.h | 13 + lib/include/logging.h | 193 +++++ lib/src/cgiwebsocket.c | 415 ++++++++++ lib/src/httpd-loop.c | 235 ++++++ lib/src/httpd.c | 1090 +++++++++++++++++++++++++++ lib/src/httpdespfs.c | 402 ++++++++++ lib/src/port/httpd-freertos.c | 56 ++ lib/src/port/httpd-posix.c | 87 +++ lib/src/utils/base64.c | 112 +++ lib/src/utils/base64.h | 6 + lib/src/utils/sha1.c | 174 +++++ lib/src/utils/sha1.h | 34 + lib/todo/esphttpclient/LICENSE | 9 + lib/todo/esphttpclient/README.md | 78 ++ lib/todo/esphttpclient/httpclient.c | 589 +++++++++++++++ lib/todo/httpclient.h | 96 +++ lib/todo/uptime.c | 59 ++ lib/todo/uptime.h | 22 + lib/todo/webpages-espfs.h | 3 + lib/todo/webpages.espfs.ld | 12 + main.c | 58 ++ 42 files changed, 6410 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 espfsbuilder/.gitignore create mode 100644 espfsbuilder/Makefile create mode 100755 espfsbuilder/logging.h create mode 100644 espfsbuilder/main.c create mode 100644 lib/espfs/espfs.c create mode 100644 lib/espfs/espfs.h create mode 100644 lib/espfs/espfsformat.h create mode 100644 lib/heatshrink/heatshrink_common.h create mode 100644 lib/heatshrink/heatshrink_config.h create mode 100644 lib/heatshrink/heatshrink_decoder.c create mode 100644 lib/heatshrink/heatshrink_decoder.h create mode 100644 lib/heatshrink/heatshrink_encoder.c create mode 100644 lib/heatshrink/heatshrink_encoder.h create mode 100644 lib/include/auth.h create mode 100644 lib/include/cgiwebsocket.h create mode 100644 lib/include/httpd-platform.h create mode 100644 lib/include/httpd-utils.h create mode 100644 lib/include/httpd.h create mode 100644 lib/include/httpdespfs.h create mode 100755 lib/include/logging.h create mode 100644 lib/src/cgiwebsocket.c create mode 100644 lib/src/httpd-loop.c create mode 100644 lib/src/httpd.c create mode 100644 lib/src/httpdespfs.c create mode 100644 lib/src/port/httpd-freertos.c create mode 100644 lib/src/port/httpd-posix.c create mode 100644 lib/src/utils/base64.c create mode 100644 lib/src/utils/base64.h create mode 100644 lib/src/utils/sha1.c create mode 100644 lib/src/utils/sha1.h create mode 100644 lib/todo/esphttpclient/LICENSE create mode 100644 lib/todo/esphttpclient/README.md create mode 100644 lib/todo/esphttpclient/httpclient.c create mode 100644 lib/todo/httpclient.h create mode 100755 lib/todo/uptime.c create mode 100755 lib/todo/uptime.h create mode 100644 lib/todo/webpages-espfs.h create mode 100644 lib/todo/webpages.espfs.ld create mode 100644 main.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3639f04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.o +.idea/ +*.bin +*.elf +*.map +*.d +*~ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a6377b0 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +PORT_SOURCES = lib/src/port/httpd-posix.c + +LIB_SOURCES = ${PORT_SOURCES} \ + lib/src/utils/base64.c \ + lib/src/utils/sha1.c \ + lib/espfs/espfs.c \ + lib/src/httpdespfs.c \ + lib/src/httpd.c \ + lib/src/httpd-loop.c \ + lib/src/cgiwebsocket.c \ + lib/heatshrink/heatshrink_decoder.c + +LIB_INCLUDES = -Ilib/include -Ilib/heatshrink -Ilib/espfs + +DEMO_SOURCES = main.c + + +all: demo + +demo: ${LIB_SOURCES} ${DEMO_SOURCES} + cc -g $^ -o $@ ${LIB_INCLUDES} diff --git a/README.md b/README.md new file mode 100644 index 0000000..f57978d --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# sprite-httpd + +This project is a continuation of Sprite-TM's ESP8266 / ESP32 http server (https://github.com/Spritetm/esphttpd) with ESP-Term improvements +(https://github.com/MightyPork/libesphttpd), ported to plain FreeRTOS with Berkeley sockets. The posix port allows it to run on Linux as well +- ideal for debugging. + +The server implements some of the more basic parts of HTTP/1.1 with Websockets and includes a templating system and a compressed ROM filesystem +called `espfs`. diff --git a/espfsbuilder/.gitignore b/espfsbuilder/.gitignore new file mode 100644 index 0000000..bc10363 --- /dev/null +++ b/espfsbuilder/.gitignore @@ -0,0 +1,4 @@ +mkespfsimage +*.o +.idea/ +*.bin diff --git a/espfsbuilder/Makefile b/espfsbuilder/Makefile new file mode 100644 index 0000000..eafa4c6 --- /dev/null +++ b/espfsbuilder/Makefile @@ -0,0 +1,12 @@ +TARGET=mkespfsimage + +SOURCES = main.c ../lib/heatshrink/heatshrink_encoder.c ../lib/heatshrink/heatshrink_decoder.c ../lib/espfs/espfs.c +CFLAGS = -I. -I../lib/heatshrink/ -I../lib/espfs/ + +all: $(TARGET) + +$(TARGET): ${SOURCES} + cc -O3 -lz $^ -o $@ ${CFLAGS} + +clean: + rm $(TARGET) diff --git a/espfsbuilder/logging.h b/espfsbuilder/logging.h new file mode 100755 index 0000000..89f8086 --- /dev/null +++ b/espfsbuilder/logging.h @@ -0,0 +1,124 @@ +#pragma once + +#include + +#ifndef VERBOSE_LOGGING +#define VERBOSE_LOGGING 1 +#endif + +#ifndef LOG_EOL +#define LOG_EOL "\n" +#endif + +/** + * Print a startup banner message (printf syntax) + * Uses bright green color + */ +#define banner(fmt, ...) \ + do { \ + fprintf(stderr, LOG_EOL "\x1b[32;1m[i] " fmt "\x1b[0m" LOG_EOL, ##__VA_ARGS__); \ + } while(0) + +/** + * Same as 'info()', but enabled even if verbose logging is disabled. + * This can be used to print version etc under the banner. + */ +#define banner_info(fmt, ...) \ + do { \ + fprintf(stderr, "\x1b[32m[i] " fmt "\x1b[0m"LOG_EOL, ##__VA_ARGS__); \ + } while(0) + +/** + * Empty line in the headers + */ +#define banner_gap() \ + do { \ + fprintf(stderr, LOG_EOL); \ + } while(0) + +#if VERBOSE_LOGGING + /** + * Print a debug log message (printf format) + */ + #define dbg(fmt, ...) \ + do { \ + fprintf(stderr, "[ ] " fmt LOG_EOL, ##__VA_ARGS__); \ + } while(0) + + /** + * Print a info log message (printf format) + * Uses bright green color + */ + #define info(fmt, ...) \ + do { \ + fprintf(stderr, "[i] " fmt "\x1b[0m"LOG_EOL, ##__VA_ARGS__); \ + } while(0) +#else + #define dbg(fmt, ...) + #define info(fmt, ...) +#endif + +/** + * Print a error log message (printf format) + * Uses bright red color + */ +#define error(fmt, ...) \ + do { \ + fprintf(stderr, "[E] " fmt "\x1b[0m"LOG_EOL, ##__VA_ARGS__); \ + } while(0) + +/** + * Print a warning log message (printf format) + * Uses bright yellow color + */ +#define warn(fmt, ...) \ + do { \ + fprintf(stderr, "[W] " fmt "\x1b[0m"LOG_EOL, ##__VA_ARGS__); \ + } while(0) + +// --------------- logging categories -------------------- + +#ifndef DEBUG_ESPFS +#define DEBUG_ESPFS 1 +#endif + +#ifndef DEBUG_HEATSHRINK +#define DEBUG_HEATSHRINK 1 +#endif + +#ifndef DEBUG_MALLOC +#define DEBUG_MALLOC 0 +#endif + +// filesystem +#if DEBUG_ESPFS +#define espfs_warn(...) warn(__VA_ARGS__) +#define espfs_dbg(...) dbg(__VA_ARGS__) +#define espfs_error(...) error(__VA_ARGS__) +#define espfs_info(...) info(__VA_ARGS__) +#else +#define espfs_dbg(...) +#define espfs_warn(...) +#define espfs_error(...) +#define espfs_info(...) +#endif + +// captive portal +#if DEBUG_HEATSHRINK +#define heatshrink_warn(...) warn(__VA_ARGS__) +#define heatshrink_dbg(...) dbg(__VA_ARGS__) +#define heatshrink_error(...) error(__VA_ARGS__) +#define heatshrink_info(...) info(__VA_ARGS__) +#else +#define heatshrink_dbg(...) +#define heatshrink_warn(...) +#define heatshrink_error(...) +#define heatshrink_info(...) +#endif + +// all malloc usage +#if DEBUG_MALLOC +#define mem_dbg(...) dbg(__VA_ARGS__) +#else +#define mem_dbg(...) +#endif diff --git a/espfsbuilder/main.c b/espfsbuilder/main.c new file mode 100644 index 0000000..07269db --- /dev/null +++ b/espfsbuilder/main.c @@ -0,0 +1,560 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "espfsformat.h" +#include "heatshrink_encoder.h" +#include "espfs.h" + +#define DEFAULT_GZIP_EXTS "html,css,js,svg,png,jpg,gif" + +// static variables +static int s_outFd = 1; +struct InputFileLinkedListEntry; + +struct InputFileLinkedListEntry { + char *name; + struct InputFileLinkedListEntry *next; +}; + +static struct InputFileLinkedListEntry *s_inputFiles = NULL; +static struct InputFileLinkedListEntry *s_lastInputFile = NULL; + +static char **s_gzipExtensions = NULL; +static bool s_gzipAll = false; + +// impls to satisfy defs in the config header +void *httpdPlatMalloc(size_t len) +{ + return malloc(len); +} + +void httpdPlatFree(void *ptr) +{ + free(ptr); +} + +size_t compressHeatshrink(uint8_t *in, size_t insize, uint8_t *out, size_t outcap, int level) +{ + uint8_t *inp = in; + uint8_t *outp = out; + size_t len; + int ws[] = {5, 6, 8, 11, 13}; + int ls[] = {3, 3, 4, 4, 4}; + HSE_poll_res pres; + HSE_sink_res sres; + size_t r; + if (level == -1) { level = 8; } + level = (level - 1) / 2; //level is now 0, 1, 2, 3, 4 + heatshrink_encoder *enc = heatshrink_encoder_alloc(ws[level], ls[level]); + if (enc == NULL) { + perror("allocating mem for heatshrink"); + exit(1); + } + //Save encoder parms as first byte + *outp = (ws[level] << 4) | ls[level]; + outp++; + outcap--; + + r = 1; + do { + if (insize > 0) { + sres = heatshrink_encoder_sink(enc, inp, insize, &len); + if (sres != HSER_SINK_OK) { break; } + inp += len; + insize -= len; + if (insize == 0) { heatshrink_encoder_finish(enc); } + } + do { + pres = heatshrink_encoder_poll(enc, outp, outcap, &len); + if (pres != HSER_POLL_MORE && pres != HSER_POLL_EMPTY) { break; } + outp += len; + outcap -= len; + r += len; + } while (pres == HSER_POLL_MORE); + } while (insize != 0); + + if (insize != 0) { + fprintf(stderr, "Heatshrink: Bug? insize is still %d. sres=%d pres=%d\n", (int)insize, sres, pres); + exit(1); + } + + heatshrink_encoder_free(enc); + return r; +} + +size_t compressGzip(uint8_t *in, size_t insize, uint8_t *out, size_t outsize, int level) +{ + z_stream stream; + int zresult; + + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.next_in = in; + stream.avail_in = insize; + stream.next_out = out; + stream.avail_out = outsize; + // 31 -> 15 window bits + 16 for gzip + zresult = deflateInit2(&stream, level, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY); + if (zresult != Z_OK) { + fprintf(stderr, "DeflateInit2 failed with code %d\n", zresult); + exit(1); + } + + zresult = deflate(&stream, Z_FINISH); + if (zresult != Z_STREAM_END) { + fprintf(stderr, "Deflate failed with code %d\n", zresult); + exit(1); + } + + zresult = deflateEnd(&stream); + if (zresult != Z_OK) { + fprintf(stderr, "DeflateEnd failed with code %d\n", zresult); + exit(1); + } + + return stream.total_out; +} + +bool shouldCompressGzip(const char *name) +{ + if (!s_gzipExtensions) { return false; } + if (s_gzipAll) { return true; } + + const char *ext = name + strlen(name); + while (*ext != '.') { + ext--; + if (ext < name) { + // no dot in file name -> no extension -> nothing to match against + return false; + } + } + ext++; + + int i = 0; + while (s_gzipExtensions[i] != NULL) { + if (strcasecmp(ext, s_gzipExtensions[i]) == 0) { + return true; + } + i++; + } + + return false; +} + +int parseGzipExtensions(char *input) +{ + char *token; + char *extList = input; + int count = 2; // one for first element, second for terminator + + // count elements + while (*extList != 0) { + if (*extList == ',') { count++; } + extList++; + } + + // split string + extList = input; + s_gzipExtensions = malloc(count * sizeof(char *)); + count = 0; + token = strtok(extList, ","); + while (token) { + s_gzipExtensions[count++] = token; + token = strtok(NULL, ","); + } + // terminate list + s_gzipExtensions[count] = NULL; + + return 1; +} + +/** + * Process a file. + * + * @param fd - filedes + * @param name - filename to embed in the archive + * @param compression_mode - compression mode + * @param level - compression level for heatshrink, 1-9 + * @param[out] compName - the used compression is output here (for debug print) + * @return + */ +int handleFile(int fd, const char *name, int compression_mode, int level, const char **compName) +{ + uint8_t *fdat = NULL, *cdat = NULL, *cdatbuf = NULL; + uint32_t size, csize; + EspFsHeader h; + uint16_t realNameLen; + uint8_t flags = 0; + size = lseek(fd, 0, SEEK_END); + fdat = malloc(size); + lseek(fd, 0, SEEK_SET); + read(fd, fdat, size); + + if (shouldCompressGzip(name)) { + csize = size * 3; + if (csize < 100) { // gzip has some headers that do not fit when trying to compress small files + csize = 100; + } // enlarge buffer if this is the case + cdat = cdatbuf = malloc(csize); + csize = compressGzip(fdat, size, cdat, csize, level); + compression_mode = COMPRESS_NONE; // don't use heatshrink if gzip was already used - it would only make it bigger + flags = FLAG_GZIP; + } else if (compression_mode == COMPRESS_NONE) { + csize = size; + cdat = fdat; + } else if (compression_mode == COMPRESS_HEATSHRINK) { + cdat = cdatbuf = malloc(size * 2); + csize = compressHeatshrink(fdat, size, cdat, size * 2, level); + } else { + fprintf(stderr, "Unknown compression - %d\n", compression_mode); + exit(1); + } + + if (csize > size) { + fprintf(stderr, "! Compression enbiggened %s, embed as plain\n", name); + //Compressing enbiggened this file. Revert to uncompressed store. + compression_mode = COMPRESS_NONE; + csize = size; + cdat = fdat; + flags = 0; + } + + //Fill header data + h.magic = htole32(ESPFS_MAGIC); // ('E' << 0) + ('S' << 8) + ('f' << 16) + ('s' << 24); + h.flags = flags; + h.compression = (int8_t) compression_mode; + h.nameLen = realNameLen = strlen(name) + 1; // zero terminator + uint32_t padbytes = 0; + if (h.nameLen & 3) { + //Round to next 32bit boundary + padbytes = 4 - (h.nameLen & 3); + h.nameLen += padbytes; // include the bytes in "name" to make parsing easier - these will be zeroed out, so the c-string remains the same. + } + h.nameLen = htole16(h.nameLen); + h.fileLenComp = htole32(csize); + h.fileLenDecomp = htole32(size); + + write(s_outFd, &h, sizeof(EspFsHeader)); + write(s_outFd, name, realNameLen); + if (padbytes) { + write(s_outFd, "\0\0\0", padbytes); // these zeros are included in h.nameLen + } + write(s_outFd, cdat, csize); + //Pad out to 32bit boundary - the parser does this automatically when walking over the archive. + if (csize & 3) { + padbytes = 4 - (csize & 3); + write(s_outFd, "\0\0\0", padbytes); + csize += padbytes; + } + + free(fdat); + if (cdatbuf) { + // free the buffer allocated for compression output + free(cdatbuf); + } + + // debug outputs ... + + if (compName != NULL) { + if (h.compression == COMPRESS_HEATSHRINK) { + *compName = "heatshrink"; + } else if (h.compression == COMPRESS_NONE) { + if (h.flags & FLAG_GZIP) { + *compName = "gzip"; + } else { + *compName = "none"; + } + } else { + *compName = "unknown"; + } + } + + // get compression % (lower is better) + return size ? (int) ((csize * 100) / size) : 100; +} + +//Write final dummy header with FLAG_LASTFILE set. +void finishArchive() +{ + EspFsHeader h; + h.magic = htole32(ESPFS_MAGIC); // ('E' << 0) + ('S' << 8) + ('f' << 16) + ('s' << 24); + h.flags = FLAG_LASTFILE; + h.compression = COMPRESS_NONE; + h.nameLen = 0; + h.fileLenComp = 0; + h.fileLenDecomp = 0; + write(s_outFd, &h, sizeof(EspFsHeader)); +} + +static size_t espfs_parse_filesize = -1; +static int espfs_parse_fd = -1; + +void parseEspfsFileAndShowItsContents(const char *filename) +{ + int rv; + fprintf(stderr, "Parsing: %s\n", filename); + + FILE *f = fopen(filename, "r"); + if (!f) { + perror(filename); + exit(1); + } + int fd = fileno(f); + + espfs_parse_filesize = lseek(fd, 0, SEEK_END); + lseek(fd, 0, SEEK_SET); + + espfs_parse_fd = fd; + + rv = espFsInit(); + if (rv != 0) { + fprintf(stderr, "Fail to init FS\n"); + exit(1); + } + + EspFsWalk walk; + espFsWalkInit(&walk); + + EspFsHeader header; + uint32_t offset; + char namebuf[1024]; + + while (espFsWalkNext(&walk, &header, namebuf, 1024, &offset)) { + fprintf(stderr, "at %04x: \"%s\", flags: %02x, comp: %s, compLen: %d, plainLen: %d\n", offset, namebuf, header.flags, + header.compression == 1 ? "HS" : "None", header.fileLenComp, header.fileLenDecomp); + } + + fclose(f); +} + + +int httpdPlatEspfsRead(void *dest, uint32_t offset, size_t len) +{ + fprintf(stderr, "FS read @ %d, len %d\n", offset, len); + if (offset + len > espfs_parse_filesize) { + fprintf(stderr, "Read out fo range!\n"); + return -1; + } + lseek(espfs_parse_fd, offset, SEEK_SET); + read(espfs_parse_fd, dest, len); + return 0; +} + + +void queueInputFile(char *name) +{ + fprintf(stderr, "INFILE: %s\n", name); + + struct InputFileLinkedListEntry *tmp = malloc(sizeof(struct InputFileLinkedListEntry)); + tmp->name = strdup(name); + tmp->next = NULL; + + if (s_lastInputFile == NULL) { + s_inputFiles = tmp; + s_lastInputFile = tmp; + } else { + s_lastInputFile->next = tmp; + s_lastInputFile = tmp; + } +} + +int main(int argc, char **argv) +{ + int f; + char inputFileName[1024]; + char *realName; + struct stat statBuf; + int serr; + int rate; + int err = 0; + int compType; //default compression type - heatshrink + int compLvl = -1; + bool use_gzip = false; + + compType = COMPRESS_HEATSHRINK; + + int c; + char *outfile = NULL; + char *parseFile = NULL; + + while (1) { + int option_index = 0; + static struct option long_options[] = { + {"parse", required_argument, 0, 'p'}, + {"compress", required_argument, 0, 'c'}, + {"gzip", no_argument, 0, 'z'}, + {"gzip-all", no_argument, 0, 'G'}, + {"level", required_argument, 0, 'l'}, + {"gzip-exts", required_argument, 0, 'g'}, + {"input", required_argument, 0, 'i'}, + {"output", required_argument, 0, 'o'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + + c = getopt_long(argc, argv, "c:l:g:zGhp:i:o:0123456789", + long_options, &option_index); + if (c == -1) { + break; + } + + switch (c) { + case 'h': + goto show_help; + + case 'z': + use_gzip = true; + break; + + case '0' ... '9': + compLvl = c - '0'; + break; + + case 'p': + parseFile = strdup(optarg); + break; + + case 'c': + compType = atoi(optarg); + break; + + case 'G': + use_gzip = true; + s_gzipAll = true; + break; + + case 'g': + use_gzip = true; + if (!parseGzipExtensions(optarg)) { + fprintf(stderr, "Bad gzip extension list: %s\n", optarg); + err = 1; + goto show_help; + } + break; + + case 'l': + compLvl = atoi(optarg); + if (compLvl < 1 || compLvl > 9) { + fprintf(stderr, "Bad compression level: %d\n", compLvl); + err = 1; + goto show_help; + } + break; + + case 'i': + queueInputFile(optarg); + break; + + case 'o': + outfile = strdup(optarg); + break; + + case '?': + goto show_help; + + default: + fprintf(stderr, "Unknown option: %c\n", c); + err = 1; + goto show_help; + } + } + + if (parseFile) { + parseEspfsFileAndShowItsContents(parseFile); + exit(0); + } + + + if (s_gzipExtensions == NULL && use_gzip) { + parseGzipExtensions(strdup(DEFAULT_GZIP_EXTS)); + } + + if (optind < argc) { + while (optind < argc) { + queueInputFile(argv[optind++]); + } + } + + if (!s_inputFiles) { + fprintf(stderr, "Reading input file names from stdin\n"); + while (fgets(inputFileName, sizeof(inputFileName), stdin)) { + //Kill off '\n' at the end + inputFileName[strlen(inputFileName) - 1] = 0; + queueInputFile(inputFileName); + } + } + + FILE *outfp = NULL; + if (outfile) { + fprintf(stderr, "Writing to %s\n", outfile); + outfp = fopen(outfile, "w+"); + if (!outfp) { + perror(outfile); + return 1; + } + s_outFd = fileno(outfp); + ftruncate(s_outFd, 0); + } else { + fprintf(stderr, "Writing to stdout\n\n"); + } + + struct InputFileLinkedListEntry *entry = s_inputFiles; + while (entry) { + char *name = entry->name; + //Only include files + serr = stat(name, &statBuf); + if ((serr == 0) && S_ISREG(statBuf.st_mode)) { + //Strip off './' or '/' madness. + realName = name; + if (name[0] == '.' && name[1] == '/') { realName += 2; } + if (realName[0] == '/') { realName++; } + f = open(name, O_RDONLY); + if (f > 0) { + const char *compName = "unknown"; + rate = handleFile(f, realName, compType, compLvl, &compName); + fprintf(stderr, "%s (%d%%, %s)\n", realName, rate, compName); + close(f); + } else { + perror(name); + } + } else if (serr != 0) { + perror(name); + } + + entry = entry->next; + } + + finishArchive(); + fsync(s_outFd); + + if (outfp) { + fclose(outfp); + } + + return 0; + + + show_help: + fprintf(stderr, "%s - Program to create espfs images\n", argv[0]); + fprintf(stderr, "Options:\n"); + fprintf(stderr, "[-p|--parse FILE]\n Parse an espfs file and show a list of its contents. No other options apply in this mode.\n"); + fprintf(stderr, "[-c|--compress COMPRESSOR]\n 0 - None, 1 - Heatshrink (default)\n"); + fprintf(stderr, "[-l|--level LEVEL] or [-0 through -9]\n compression level 1-9, higher is better but uses more RAM\n"); + fprintf(stderr, "[-z|--gzip]\n use gzip for files with extensions matching "DEFAULT_GZIP_EXTS"\n"); + fprintf(stderr, "[-Z|--gzip-all]\n use gzip for all files\n"); + fprintf(stderr, "[-g|--gzip-exts GZIPPED_EXTENSIONS]\n use gzip for files with custom extensions, comma-separated\n"); + fprintf(stderr, "[-i|--input FILE]\n Input file, can be multiple. Files can also be passed at the end without -i, or as lines on stdin if not specified by args\n"); + fprintf(stderr, "[-o|--output FILE]\n Output file name; if not specified, outputs to stdout\n"); + fprintf(stderr, "[-h|--help\n Show help.\n\n"); + exit(err); +} + diff --git a/lib/espfs/espfs.c b/lib/espfs/espfs.c new file mode 100644 index 0000000..60f9157 --- /dev/null +++ b/lib/espfs/espfs.c @@ -0,0 +1,319 @@ +/* +This is a simple read-only implementation of a file system. It uses a block of data coming from the +mkespfsimg tool, and can use that block to do abstracted operations on the files that are in there. +It's written for use with httpd, but doesn't need to be used as such. +*/ + +/* + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * Jeroen Domburg wrote this file. As long as you retain + * this notice you can do whatever you want with this stuff. If we meet some day, + * and you think this stuff is worth it, you can buy me a beer in return. + * ---------------------------------------------------------------------------- + */ + + +#include +#include +#include + +#include "espfsformat.h" +#include "espfs.h" + +#include "heatshrink_decoder.h" +#include "logging.h" + +// forward declaration for use in the stand-alone espfs tool +int httpdPlatEspfsRead(void *dest, uint32_t offset, size_t len); + +struct EspFsFile { + uint32_t header; + char decompressor; + uint32_t posDecomp; + uint32_t posStart; + uint32_t posComp; + heatshrink_decoder *decompData; +}; + + +EspFsInitResult espFsInit() +{ + // check if there is valid header at address + EspFsHeader testHeader; + int rv; + rv = httpdPlatEspfsRead(&testHeader, 0, sizeof(EspFsHeader)); + if (rv != 0 || testHeader.magic != ESPFS_MAGIC) { + espfs_error("[EspFS] Invalid magic on first file header"); + return ESPFS_INIT_RESULT_NO_IMAGE; + } + return ESPFS_INIT_RESULT_OK; +} + +// Returns flags of opened file. +int espFsFlags(EspFsFile *fh) +{ + if (fh == NULL) { + espfs_error("[EspFS] File handle not ready"); + return -1; + } + + int8_t flags; + httpdPlatEspfsRead(&flags, fh->header + offsetof(EspFsHeader, flags), 1); + return (int) flags; +} + +void espFsWalkInit(EspFsWalk *walk) +{ + if (!walk) { return; } + walk->hpos = 0; +} + +bool espFsWalkNext(EspFsWalk *walk, EspFsHeader *header, char *namebuf, size_t namebuf_cap, uint32_t *filepos) +{ + int rv; + + if (!header || !namebuf) { + espfs_error("[EspFS] espFsWalkNext NULL header or namebuf arg"); + return false; + } + + uint32_t p = walk->hpos; + + //Grab the next file header. + rv = httpdPlatEspfsRead(header, p, sizeof(EspFsHeader)); + if (rv != 0) { + return false; + } + + if (header->magic != ESPFS_MAGIC) { + espfs_error("[EspFS] Magic mismatch. EspFS image broken."); + return false; + } + + if (header->flags & FLAG_LASTFILE) { + espfs_dbg("[EspFS] End of image."); + return false; + } + + if (header->nameLen > namebuf_cap) { + espfs_dbg("[EspFS] Name too long for buffer"); + return false; + } + + //Grab the name of the file. + p += sizeof(EspFsHeader); + httpdPlatEspfsRead(namebuf, p, header->nameLen); + namebuf[header->nameLen] = 0; // ensure it's terminated + + if (filepos) { + *filepos = walk->hpos; + } + + walk->hpos += sizeof(EspFsHeader) + header->nameLen + header->fileLenComp; + // Align + while(walk->hpos & 3) { + walk->hpos++; + } + + return true; +} + +//Open a file and return a pointer to the file desc struct. +EspFsFile *espFsOpenFromHeader(EspFsHeader *h, uint32_t hpos) +{ + int rv; + if (h->magic != ESPFS_MAGIC) { + espfs_error("[EspFS] Magic mismatch. EspFS image broken."); + return NULL; + } + if (h->flags & FLAG_LASTFILE) { + espfs_dbg("[EspFS] End of image."); + return NULL; + } + + hpos += sizeof(EspFsHeader); + hpos += h->nameLen; // Skip to content + + EspFsFile *r = (EspFsFile *) httpdPlatMalloc(sizeof(EspFsFile)); //Alloc file desc mem + if (r == NULL) { return NULL; } + r->header = hpos; + r->decompressor = h->compression; + r->posComp = hpos; + r->posStart = hpos; + r->posDecomp = 0; + + espfs_dbg("[EspFS] Found file @ hpos %d", hpos); + + if (h->compression == COMPRESS_NONE) { + r->decompData = NULL; + } else if (h->compression == COMPRESS_HEATSHRINK) { + //File is compressed with Heatshrink. + char parm; + //Decoder params are stored in 1st byte. + rv = httpdPlatEspfsRead(&parm, r->posComp, 1); + if (rv != 0) { + return NULL; + } + r->posComp++; + espfs_dbg("[EspFS] Heatshrink compressed file; decode parms = %x", parm); + heatshrink_decoder *dec = heatshrink_decoder_alloc(16, (parm >> 4) & 0xf, parm & 0xf); + r->decompData = dec; + return r; + } else { + espfs_error("[EspFS] Invalid compression: %d", h->compression); + httpdPlatFree(r); + return NULL; + } +} + +//Open a file and return a pointer to the file desc struct. +EspFsFile *espFsOpenAt(uint32_t hpos) +{ + EspFsHeader h; + int rv = httpdPlatEspfsRead(&h, hpos, sizeof(EspFsHeader)); + if (rv != 0) { + return NULL; + } + return espFsOpenFromHeader(&h, hpos); +} + + +//Open a file and return a pointer to the file desc struct. +EspFsFile *espFsOpen(const char *fileName) +{ + int rv; + uint32_t p = 0; + uint32_t hpos; + char namebuf[256]; + EspFsHeader h; + EspFsFile *r; + //Strip initial slashes + while (fileName[0] == '/') { fileName++; } + + espfs_dbg("[EspFS] Open file: %s", fileName); + + //Go find that file! + while (1) { + hpos = p; + //Grab the next file header. + rv = httpdPlatEspfsRead(&h, p, sizeof(EspFsHeader)); + if (rv != 0) { + return NULL; + } + + if (h.magic != ESPFS_MAGIC) { + espfs_error("[EspFS] Magic mismatch. EspFS image broken."); + return NULL; + } + if (h.flags & FLAG_LASTFILE) { + espfs_dbg("[EspFS] End of image."); + return NULL; + } + + //Grab the name of the file. + p += sizeof(EspFsHeader); + rv = httpdPlatEspfsRead(&namebuf, p, h.nameLen); + if (rv != 0) { + return NULL; + } + namebuf[h.nameLen] = 0; // ensure it's terminated + + if (strcmp(namebuf, fileName) == 0) { + //Yay, this is the file we need! + return espFsOpenFromHeader(&h, hpos); + } + //We don't need this file. Skip name and file + p += h.nameLen + h.fileLenComp; + while(p & 3) { + p++; + } + } +} + +//Read len bytes from the given file into buff. Returns the actual amount of bytes read. +int espFsRead(EspFsFile *fh, char *buff, size_t len) +{ + int rv; + int flen; + int fdlen; + if (fh == NULL) { return 0; } + + rv = httpdPlatEspfsRead(&flen, fh->header + offsetof(EspFsHeader, fileLenComp), 4); + if (rv != 0) { + return 0; + } + + //Cache file length. + //Do stuff depending on the way the file is compressed. + if (fh->decompressor == COMPRESS_NONE) { + int toRead = flen - (int) (fh->posComp - fh->posStart); + if (toRead < 0) { toRead = 0; } + if (len > toRead) { len = toRead; } + rv = httpdPlatEspfsRead(buff, fh->posComp, len); + if (rv != 0) { + return 0; + } + + fh->posDecomp += len; + fh->posComp += len; + return (int) len; + } else if (fh->decompressor == COMPRESS_HEATSHRINK) { + rv = httpdPlatEspfsRead(&fdlen, fh->header + offsetof(EspFsHeader, fileLenDecomp), 4); + if (rv != 0) { + return 0; + } + + size_t decoded = 0; + size_t elen, rlen; + char ebuff[16]; + heatshrink_decoder *dec = fh->decompData; + if (fh->posDecomp == fdlen) { + return 0; + } + + // We must ensure that whole file is decompressed and written to output buffer. + // This means even when there is no input data (elen==0) try to poll decoder until + // posDecomp equals decompressed file length + + while (decoded < len) { + //Feed data into the decompressor + //ToDo: Check ret val of heatshrink fns for errors + elen = flen - (fh->posComp - fh->posStart); + if (elen > 0) { + rv = httpdPlatEspfsRead(ebuff, fh->posComp, 16); + if (rv != 0) { + return 0; + } + + heatshrink_decoder_sink(dec, (uint8_t *) ebuff, (elen > 16) ? 16 : elen, &rlen); + fh->posComp += rlen; + } + //Grab decompressed data and put into buff + heatshrink_decoder_poll(dec, (uint8_t *) buff, len - decoded, &rlen); + fh->posDecomp += rlen; + buff += rlen; + decoded += rlen; + + if (elen == 0) { + if (fh->posDecomp == fdlen) { + heatshrink_decoder_finish(dec); + } + return (int) decoded; + } + } + return (int) len; + } + return 0; +} + +//Close the file. +void espFsClose(EspFsFile *fh) +{ + if (fh == NULL) { return; } + if (fh->decompressor == COMPRESS_HEATSHRINK) { + heatshrink_decoder *dec = (heatshrink_decoder *) fh->decompData; + heatshrink_decoder_free(dec); + } + httpdPlatFree(fh); +} diff --git a/lib/espfs/espfs.h b/lib/espfs/espfs.h new file mode 100644 index 0000000..27ada64 --- /dev/null +++ b/lib/espfs/espfs.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include "espfsformat.h" + +typedef enum { + ESPFS_INIT_RESULT_OK, + ESPFS_INIT_RESULT_NO_IMAGE, + ESPFS_INIT_RESULT_BAD_ALIGN, +} EspFsInitResult; + +typedef struct EspFsFile EspFsFile; + +struct EspFsWalk { + uint32_t hpos; +}; +typedef struct EspFsWalk EspFsWalk; + +/** Init filesystem walk */ +void espFsWalkInit(EspFsWalk *walk); +/** + * Advance in the filesystem walk + * + * header - the next file's header is read here + * namebuf - the name is read here + * filepos - the file header's pos is copied here, if not NULL + */ +bool espFsWalkNext(EspFsWalk *walk, EspFsHeader *header, char *namebuf, size_t namebuf_cap, uint32_t *filepos); +/** Open a file with header starting at the given position */ +EspFsFile *espFsOpenAt(uint32_t hpos); +/** + * Open a file using an already read header and it's offset. + * This is the same function as espFsOpenAt, but avoids reading the header again if already read. + */ +EspFsFile *espFsOpenFromHeader(EspFsHeader *h, uint32_t hpos); + +EspFsInitResult espFsInit(); +EspFsFile *espFsOpen(const char *fileName); +int espFsFlags(EspFsFile *fh); +int espFsRead(EspFsFile *fh, char *buff, size_t len); +void espFsClose(EspFsFile *fh); diff --git a/lib/espfs/espfsformat.h b/lib/espfs/espfsformat.h new file mode 100644 index 0000000..a3cc0a2 --- /dev/null +++ b/lib/espfs/espfsformat.h @@ -0,0 +1,25 @@ +#pragma once + +/* +The idea 'borrows' from cpio: it's basically a concatenation of {header, filename, file} data. +Header, filename and file data is 32-bit aligned. The last file is indicated by data-less header +with the FLAG_LASTFILE flag set. +*/ + +#include + +#define FLAG_LASTFILE (1<<0) +#define FLAG_GZIP (1<<1) +#define COMPRESS_NONE 0 +#define COMPRESS_HEATSHRINK 1 +#define ESPFS_MAGIC 0x73665345 /* ASCII ESfs - when read as little endian */ + +/* 16 bytes long for alignment */ +typedef struct { + uint32_t magic; + uint8_t flags; + uint8_t compression; + uint16_t nameLen; + uint32_t fileLenComp; + uint32_t fileLenDecomp; +} __attribute__((packed)) EspFsHeader; diff --git a/lib/heatshrink/heatshrink_common.h b/lib/heatshrink/heatshrink_common.h new file mode 100644 index 0000000..b02788c --- /dev/null +++ b/lib/heatshrink/heatshrink_common.h @@ -0,0 +1,17 @@ +#pragma once + +#define HEATSHRINK_AUTHOR "Scott Vokes " +#define HEATSHRINK_URL "https://github.com/atomicobject/heatshrink" + +/* Version 0.4.1 */ +#define HEATSHRINK_VERSION_MAJOR 0 +#define HEATSHRINK_VERSION_MINOR 4 +#define HEATSHRINK_VERSION_PATCH 1 + +#define HEATSHRINK_MIN_WINDOW_BITS 4 +#define HEATSHRINK_MAX_WINDOW_BITS 15 + +#define HEATSHRINK_MIN_LOOKAHEAD_BITS 3 + +#define HEATSHRINK_LITERAL_MARKER 0x01 +#define HEATSHRINK_BACKREF_MARKER 0x00 diff --git a/lib/heatshrink/heatshrink_config.h b/lib/heatshrink/heatshrink_config.h new file mode 100644 index 0000000..38d00b2 --- /dev/null +++ b/lib/heatshrink/heatshrink_config.h @@ -0,0 +1,40 @@ +#pragma once + +/* Should functionality assuming dynamic allocation be used? */ +#ifndef HEATSHRINK_DYNAMIC_ALLOC +#define HEATSHRINK_DYNAMIC_ALLOC 1 +#endif + +#if HEATSHRINK_DYNAMIC_ALLOC + + // forward declare - needed when building the heatshrink compressor + void *httpdPlatMalloc(size_t len); + void httpdPlatFree(void *ptr); + + /* Optional replacement of malloc/free */ + #define HEATSHRINK_MALLOC(SZ) httpdPlatMalloc(SZ) + #define HEATSHRINK_FREE(P, SZ) httpdPlatFree(P) +#else + /* Required parameters for static configuration */ + #ifndef HEATSHRINK_STATIC_INPUT_BUFFER_SIZE + #define HEATSHRINK_STATIC_INPUT_BUFFER_SIZE 32 + #endif + + #ifndef HEATSHRINK_STATIC_WINDOW_BITS + #define HEATSHRINK_STATIC_WINDOW_BITS 8 + #endif + + #ifndef HEATSHRINK_STATIC_LOOKAHEAD_BITS + #define HEATSHRINK_STATIC_LOOKAHEAD_BITS 4 + #endif +#endif + +/* Turn on logging for debugging. */ +#ifndef HEATSHRINK_DEBUGGING_LOGS +#define HEATSHRINK_DEBUGGING_LOGS 0 +#endif + +/* Use indexing for faster compression. (This requires additional space.) */ +#ifndef HEATSHRINK_USE_INDEX +#define HEATSHRINK_USE_INDEX 1 +#endif diff --git a/lib/heatshrink/heatshrink_decoder.c b/lib/heatshrink/heatshrink_decoder.c new file mode 100644 index 0000000..9acc27c --- /dev/null +++ b/lib/heatshrink/heatshrink_decoder.c @@ -0,0 +1,374 @@ +#include +#include + +#include "heatshrink_decoder.h" + +/* States for the polling state machine. */ +typedef enum { + HSDS_TAG_BIT, /* tag bit */ + HSDS_YIELD_LITERAL, /* ready to yield literal byte */ + HSDS_BACKREF_INDEX_MSB, /* most significant byte of index */ + HSDS_BACKREF_INDEX_LSB, /* least significant byte of index */ + HSDS_BACKREF_COUNT_MSB, /* most significant byte of count */ + HSDS_BACKREF_COUNT_LSB, /* least significant byte of count */ + HSDS_YIELD_BACKREF, /* ready to yield back-reference */ +} HSD_state; + +#if HEATSHRINK_DEBUGGING_LOGS +#include +#include +#include +#define LOG(...) fprintf(stderr, __VA_ARGS__) +#define ASSERT(X) assert(X) +static const char *state_names[] = { + "tag_bit", + "yield_literal", + "backref_index_msb", + "backref_index_lsb", + "backref_count_msb", + "backref_count_lsb", + "yield_backref", +}; +#else +#define LOG(...) /* no-op */ +#define ASSERT(X) /* no-op */ +#endif + +typedef struct { + uint8_t *buf; /* output buffer */ + size_t buf_size; /* buffer size */ + size_t *output_size; /* bytes pushed to buffer, so far */ +} output_info; + +#define NO_BITS ((uint16_t)-1) + +/* Forward references. */ +static uint16_t get_bits(heatshrink_decoder *hsd, uint8_t count); +static void push_byte(heatshrink_decoder *hsd, output_info *oi, uint8_t byte); + +#if HEATSHRINK_DYNAMIC_ALLOC +heatshrink_decoder *heatshrink_decoder_alloc(uint16_t input_buffer_size, + uint8_t window_sz2, + uint8_t lookahead_sz2) { + if ((window_sz2 < HEATSHRINK_MIN_WINDOW_BITS) || + (window_sz2 > HEATSHRINK_MAX_WINDOW_BITS) || + (input_buffer_size == 0) || + (lookahead_sz2 < HEATSHRINK_MIN_LOOKAHEAD_BITS) || + (lookahead_sz2 >= window_sz2)) { + return NULL; + } + size_t buffers_sz = (1 << window_sz2) + input_buffer_size; + size_t sz = sizeof(heatshrink_decoder) + buffers_sz; + heatshrink_decoder *hsd = HEATSHRINK_MALLOC(sz); + if (hsd == NULL) { return NULL; } + hsd->input_buffer_size = input_buffer_size; + hsd->window_sz2 = window_sz2; + hsd->lookahead_sz2 = lookahead_sz2; + heatshrink_decoder_reset(hsd); + LOG("-- allocated decoder with buffer size of %zu (%zu + %u + %u)\n", + sz, sizeof(heatshrink_decoder), (1 << window_sz2), input_buffer_size); + return hsd; +} + +void heatshrink_decoder_free(heatshrink_decoder *hsd) { + size_t buffers_sz = (1 << hsd->window_sz2) + hsd->input_buffer_size; + size_t sz = sizeof(heatshrink_decoder) + buffers_sz; + HEATSHRINK_FREE(hsd, sz); + (void)sz; /* may not be used by free */ +} +#endif + +void heatshrink_decoder_reset(heatshrink_decoder *hsd) { + size_t buf_sz = 1 << HEATSHRINK_DECODER_WINDOW_BITS(hsd); + size_t input_sz = HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(hsd); + memset(hsd->buffers, 0, buf_sz + input_sz); + hsd->state = HSDS_TAG_BIT; + hsd->input_size = 0; + hsd->input_index = 0; + hsd->bit_index = 0x00; + hsd->current_byte = 0x00; + hsd->output_count = 0; + hsd->output_index = 0; + hsd->head_index = 0; +} + +/* Copy SIZE bytes into the decoder's input buffer, if it will fit. */ +HSD_sink_res heatshrink_decoder_sink(heatshrink_decoder *hsd, + uint8_t *in_buf, size_t size, size_t *input_size) { + if ((hsd == NULL) || (in_buf == NULL) || (input_size == NULL)) { + return HSDR_SINK_ERROR_NULL; + } + + size_t rem = HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(hsd) - hsd->input_size; + if (rem == 0) { + *input_size = 0; + return HSDR_SINK_FULL; + } + + size = rem < size ? rem : size; + LOG("-- sinking %zd bytes\n", size); + /* copy into input buffer (at head of buffers) */ + memcpy(&hsd->buffers[hsd->input_size], in_buf, size); + hsd->input_size += (uint16_t)size; + *input_size = size; + return HSDR_SINK_OK; +} + + +/***************** + * Decompression * + *****************/ + +#define BACKREF_COUNT_BITS(HSD) (HEATSHRINK_DECODER_LOOKAHEAD_BITS(HSD)) +#define BACKREF_INDEX_BITS(HSD) (HEATSHRINK_DECODER_WINDOW_BITS(HSD)) + +// States +static HSD_state st_tag_bit(heatshrink_decoder *hsd); +static HSD_state st_yield_literal(heatshrink_decoder *hsd, + output_info *oi); +static HSD_state st_backref_index_msb(heatshrink_decoder *hsd); +static HSD_state st_backref_index_lsb(heatshrink_decoder *hsd); +static HSD_state st_backref_count_msb(heatshrink_decoder *hsd); +static HSD_state st_backref_count_lsb(heatshrink_decoder *hsd); +static HSD_state st_yield_backref(heatshrink_decoder *hsd, + output_info *oi); + +HSD_poll_res heatshrink_decoder_poll(heatshrink_decoder *hsd, + uint8_t *out_buf, size_t out_buf_size, size_t *output_size) { + if ((hsd == NULL) || (out_buf == NULL) || (output_size == NULL)) { + return HSDR_POLL_ERROR_NULL; + } + *output_size = 0; + + output_info oi; + oi.buf = out_buf; + oi.buf_size = out_buf_size; + oi.output_size = output_size; + + while (1) { + LOG("-- poll, state is %d (%s), input_size %d\n", + hsd->state, state_names[hsd->state], hsd->input_size); + uint8_t in_state = hsd->state; + HSD_state next_state; + switch (in_state) { + case HSDS_TAG_BIT: + next_state = st_tag_bit(hsd); + break; + case HSDS_YIELD_LITERAL: + next_state = st_yield_literal(hsd, &oi); + break; + case HSDS_BACKREF_INDEX_MSB: + next_state = st_backref_index_msb(hsd); + break; + case HSDS_BACKREF_INDEX_LSB: + next_state = st_backref_index_lsb(hsd); + break; + case HSDS_BACKREF_COUNT_MSB: + next_state = st_backref_count_msb(hsd); + break; + case HSDS_BACKREF_COUNT_LSB: + next_state = st_backref_count_lsb(hsd); + break; + case HSDS_YIELD_BACKREF: + next_state = st_yield_backref(hsd, &oi); + break; + default: + return HSDR_POLL_ERROR_UNKNOWN; + } + hsd->state = (uint8_t)next_state; + + /* If the current state cannot advance, check if input or output + * buffer are exhausted. */ + if (hsd->state == in_state) { + if (*output_size == out_buf_size) { return HSDR_POLL_MORE; } + return HSDR_POLL_EMPTY; + } + } +} + +static HSD_state st_tag_bit(heatshrink_decoder *hsd) { + const uint32_t bits = get_bits(hsd, 1); // get tag bit + if (bits == NO_BITS) { + return HSDS_TAG_BIT; + } else if (bits > 0) { + return HSDS_YIELD_LITERAL; +/* This suppresses a warning for unreachable code in non-dynamic + * builds where the window bits is always <= 8. */ +#if HEATSHRINK_DYNAMIC_ALLOC || HEATSHRINK_STATIC_WINDOW_BITS > 8 + } else if (HEATSHRINK_DECODER_WINDOW_BITS(hsd) > 8) { + return HSDS_BACKREF_INDEX_MSB; +#endif + } else { + hsd->output_index = 0; + return HSDS_BACKREF_INDEX_LSB; + } +} + +static HSD_state st_yield_literal(heatshrink_decoder *hsd, + output_info *oi) { + /* Emit a repeated section from the window buffer, and add it (again) + * to the window buffer. (Note that the repetition can include + * itself.)*/ + if (*oi->output_size < oi->buf_size) { + uint16_t byte = get_bits(hsd, 8); + if (byte == NO_BITS) { return HSDS_YIELD_LITERAL; } /* out of input */ + uint8_t *buf = &hsd->buffers[HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(hsd)]; + uint16_t mask = (uint16_t)(1 << HEATSHRINK_DECODER_WINDOW_BITS(hsd)) - 1; + uint8_t c = byte & 0xFF; + LOG("-- emitting literal byte 0x%02x ('%c')\n", c, isprint(c) ? c : '.'); + buf[hsd->head_index++ & mask] = c; + push_byte(hsd, oi, c); + return HSDS_TAG_BIT; + } else { + return HSDS_YIELD_LITERAL; + } +} + +static HSD_state st_backref_index_msb(heatshrink_decoder *hsd) { + uint8_t bit_ct = BACKREF_INDEX_BITS(hsd); + ASSERT(bit_ct > 8); + uint16_t bits = get_bits(hsd, bit_ct - 8); + LOG("-- backref index (msb), got 0x%04x (+1)\n", bits); + if (bits == NO_BITS) { return HSDS_BACKREF_INDEX_MSB; } + hsd->output_index = (uint16_t)(bits << 8); + return HSDS_BACKREF_INDEX_LSB; +} + +static HSD_state st_backref_index_lsb(heatshrink_decoder *hsd) { + uint8_t bit_ct = BACKREF_INDEX_BITS(hsd); + uint16_t bits = get_bits(hsd, bit_ct < 8 ? bit_ct : 8); + LOG("-- backref index (lsb), got 0x%04x (+1)\n", bits); + if (bits == NO_BITS) { return HSDS_BACKREF_INDEX_LSB; } + hsd->output_index |= bits; + hsd->output_index++; + uint8_t br_bit_ct = BACKREF_COUNT_BITS(hsd); + hsd->output_count = 0; + return (br_bit_ct > 8) ? HSDS_BACKREF_COUNT_MSB : HSDS_BACKREF_COUNT_LSB; +} + +static HSD_state st_backref_count_msb(heatshrink_decoder *hsd) { + uint8_t br_bit_ct = BACKREF_COUNT_BITS(hsd); + ASSERT(br_bit_ct > 8); + uint16_t bits = get_bits(hsd, br_bit_ct - 8); + LOG("-- backref count (msb), got 0x%04x (+1)\n", bits); + if (bits == NO_BITS) { return HSDS_BACKREF_COUNT_MSB; } + hsd->output_count = (uint16_t)(bits << 8); + return HSDS_BACKREF_COUNT_LSB; +} + +static HSD_state st_backref_count_lsb(heatshrink_decoder *hsd) { + uint8_t br_bit_ct = BACKREF_COUNT_BITS(hsd); + uint16_t bits = get_bits(hsd, br_bit_ct < 8 ? br_bit_ct : 8); + LOG("-- backref count (lsb), got 0x%04x (+1)\n", bits); + if (bits == NO_BITS) { return HSDS_BACKREF_COUNT_LSB; } + hsd->output_count |= bits; + hsd->output_count++; + return HSDS_YIELD_BACKREF; +} + +static HSD_state st_yield_backref(heatshrink_decoder *hsd, + output_info *oi) { + size_t count = oi->buf_size - *oi->output_size; + if (count > 0) { + size_t i = 0; + if (hsd->output_count < count) count = hsd->output_count; + uint8_t *buf = &hsd->buffers[HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(hsd)]; + uint16_t mask = (uint16_t)((1 << HEATSHRINK_DECODER_WINDOW_BITS(hsd)) - 1); + uint16_t neg_offset = hsd->output_index; + LOG("-- emitting %zu bytes from -%u bytes back\n", count, neg_offset); + ASSERT(neg_offset <= mask + 1); + ASSERT(count <= (size_t)(1 << BACKREF_COUNT_BITS(hsd))); + + for (i=0; ihead_index - neg_offset) & mask]; + push_byte(hsd, oi, c); + buf[hsd->head_index & mask] = c; + hsd->head_index++; + LOG(" -- ++ 0x%02x\n", c); + } + hsd->output_count -= (uint16_t)count; + if (hsd->output_count == 0) { return HSDS_TAG_BIT; } + } + return HSDS_YIELD_BACKREF; +} + +/* Get the next COUNT bits from the input buffer, saving incremental progress. + * Returns NO_BITS on end of input, or if more than 15 bits are requested. */ +static uint16_t get_bits(heatshrink_decoder *hsd, uint8_t count) { + uint16_t accumulator = 0; + int i = 0; + if (count > 15) { return NO_BITS; } + LOG("-- popping %u bit(s)\n", count); + + /* If we aren't able to get COUNT bits, suspend immediately, because we + * don't track how many bits of COUNT we've accumulated before suspend. */ + if (hsd->input_size == 0) { + if (hsd->bit_index < (1 << (count - 1))) { return NO_BITS; } + } + + for (i = 0; i < count; i++) { + if (hsd->bit_index == 0x00) { + if (hsd->input_size == 0) { + LOG(" -- out of bits, suspending w/ accumulator of %u (0x%02x)\n", + accumulator, accumulator); + return NO_BITS; + } + hsd->current_byte = hsd->buffers[hsd->input_index++]; + LOG(" -- pulled byte 0x%02x\n", hsd->current_byte); + if (hsd->input_index == hsd->input_size) { + hsd->input_index = 0; /* input is exhausted */ + hsd->input_size = 0; + } + hsd->bit_index = 0x80; + } + accumulator <<= 1; + if (hsd->current_byte & hsd->bit_index) { + accumulator |= 0x01; + if (0) { + LOG(" -- got 1, accumulator 0x%04x, bit_index 0x%02x\n", + accumulator, hsd->bit_index); + } + } else { + if (0) { + LOG(" -- got 0, accumulator 0x%04x, bit_index 0x%02x\n", + accumulator, hsd->bit_index); + } + } + hsd->bit_index >>= 1; + } + + if (count > 1) { LOG(" -- accumulated %08x\n", accumulator); } + return accumulator; +} + +HSD_finish_res heatshrink_decoder_finish(heatshrink_decoder *hsd) { + if (hsd == NULL) { return HSDR_FINISH_ERROR_NULL; } + switch (hsd->state) { + case HSDS_TAG_BIT: + return hsd->input_size == 0 ? HSDR_FINISH_DONE : HSDR_FINISH_MORE; + + /* If we want to finish with no input, but are in these states, it's + * because the 0-bit padding to the last byte looks like a backref + * marker bit followed by all 0s for index and count bits. */ + case HSDS_BACKREF_INDEX_LSB: + case HSDS_BACKREF_INDEX_MSB: + case HSDS_BACKREF_COUNT_LSB: + case HSDS_BACKREF_COUNT_MSB: + return hsd->input_size == 0 ? HSDR_FINISH_DONE : HSDR_FINISH_MORE; + + /* If the output stream is padded with 0xFFs (possibly due to being in + * flash memory), also explicitly check the input size rather than + * uselessly returning MORE but yielding 0 bytes when polling. */ + case HSDS_YIELD_LITERAL: + return hsd->input_size == 0 ? HSDR_FINISH_DONE : HSDR_FINISH_MORE; + + default: + return HSDR_FINISH_MORE; + } +} + +static void push_byte(heatshrink_decoder *hsd, output_info *oi, uint8_t byte) { + LOG(" -- pushing byte: 0x%02x ('%c')\n", byte, isprint(byte) ? byte : '.'); + oi->buf[(*oi->output_size)++] = byte; + (void)hsd; +} diff --git a/lib/heatshrink/heatshrink_decoder.h b/lib/heatshrink/heatshrink_decoder.h new file mode 100644 index 0000000..36caa02 --- /dev/null +++ b/lib/heatshrink/heatshrink_decoder.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include "heatshrink_common.h" +#include "heatshrink_config.h" + +typedef enum { + HSDR_SINK_OK, /* data sunk, ready to poll */ + HSDR_SINK_FULL, /* out of space in internal buffer */ + HSDR_SINK_ERROR_NULL=-1, /* NULL argument */ +} HSD_sink_res; + +typedef enum { + HSDR_POLL_EMPTY, /* input exhausted */ + HSDR_POLL_MORE, /* more data remaining, call again w/ fresh output buffer */ + HSDR_POLL_ERROR_NULL=-1, /* NULL arguments */ + HSDR_POLL_ERROR_UNKNOWN=-2, +} HSD_poll_res; + +typedef enum { + HSDR_FINISH_DONE, /* output is done */ + HSDR_FINISH_MORE, /* more output remains */ + HSDR_FINISH_ERROR_NULL=-1, /* NULL arguments */ +} HSD_finish_res; + +#if HEATSHRINK_DYNAMIC_ALLOC +#define HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(BUF) \ + ((BUF)->input_buffer_size) +#define HEATSHRINK_DECODER_WINDOW_BITS(BUF) \ + ((BUF)->window_sz2) +#define HEATSHRINK_DECODER_LOOKAHEAD_BITS(BUF) \ + ((BUF)->lookahead_sz2) +#else +#define HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(_) \ + HEATSHRINK_STATIC_INPUT_BUFFER_SIZE +#define HEATSHRINK_DECODER_WINDOW_BITS(_) \ + (HEATSHRINK_STATIC_WINDOW_BITS) +#define HEATSHRINK_DECODER_LOOKAHEAD_BITS(BUF) \ + (HEATSHRINK_STATIC_LOOKAHEAD_BITS) +#endif + +typedef struct { + uint16_t input_size; /* bytes in input buffer */ + uint16_t input_index; /* offset to next unprocessed input byte */ + uint16_t output_count; /* how many bytes to output */ + uint16_t output_index; /* index for bytes to output */ + uint16_t head_index; /* head of window buffer */ + uint8_t state; /* current state machine node */ + uint8_t current_byte; /* current byte of input */ + uint8_t bit_index; /* current bit index */ + +#if HEATSHRINK_DYNAMIC_ALLOC + /* Fields that are only used if dynamically allocated. */ + uint8_t window_sz2; /* window buffer bits */ + uint8_t lookahead_sz2; /* lookahead bits */ + uint16_t input_buffer_size; /* input buffer size */ + + /* Input buffer, then expansion window buffer */ + uint8_t buffers[]; +#else + /* Input buffer, then expansion window buffer */ + uint8_t buffers[(1 << HEATSHRINK_DECODER_WINDOW_BITS(_)) + + HEATSHRINK_DECODER_INPUT_BUFFER_SIZE(_)]; +#endif +} heatshrink_decoder; + +#if HEATSHRINK_DYNAMIC_ALLOC +/* Allocate a decoder with an input buffer of INPUT_BUFFER_SIZE bytes, + * an expansion buffer size of 2^WINDOW_SZ2, and a lookahead + * size of 2^lookahead_sz2. (The window buffer and lookahead sizes + * must match the settings used when the data was compressed.) + * Returns NULL on error. */ +heatshrink_decoder *heatshrink_decoder_alloc(uint16_t input_buffer_size, + uint8_t expansion_buffer_sz2, uint8_t lookahead_sz2); + +/* Free a decoder. */ +void heatshrink_decoder_free(heatshrink_decoder *hsd); +#endif + +/* Reset a decoder. */ +void heatshrink_decoder_reset(heatshrink_decoder *hsd); + +/* Sink at most SIZE bytes from IN_BUF into the decoder. *INPUT_SIZE is set to + * indicate how many bytes were actually sunk (in case a buffer was filled). */ +HSD_sink_res heatshrink_decoder_sink(heatshrink_decoder *hsd, + uint8_t *in_buf, size_t size, size_t *input_size); + +/* Poll for output from the decoder, copying at most OUT_BUF_SIZE bytes into + * OUT_BUF (setting *OUTPUT_SIZE to the actual amount copied). */ +HSD_poll_res heatshrink_decoder_poll(heatshrink_decoder *hsd, + uint8_t *out_buf, size_t out_buf_size, size_t *output_size); + +/* Notify the decoder that the input stream is finished. + * If the return value is HSDR_FINISH_MORE, there is still more output, so + * call heatshrink_decoder_poll and repeat. */ +HSD_finish_res heatshrink_decoder_finish(heatshrink_decoder *hsd); diff --git a/lib/heatshrink/heatshrink_encoder.c b/lib/heatshrink/heatshrink_encoder.c new file mode 100644 index 0000000..21b52e7 --- /dev/null +++ b/lib/heatshrink/heatshrink_encoder.c @@ -0,0 +1,607 @@ +#include +#include +#include +#include "heatshrink_encoder.h" + +typedef enum { + HSES_NOT_FULL, /* input buffer not full enough */ + HSES_FILLED, /* buffer is full */ + HSES_SEARCH, /* searching for patterns */ + HSES_YIELD_TAG_BIT, /* yield tag bit */ + HSES_YIELD_LITERAL, /* emit literal byte */ + HSES_YIELD_BR_INDEX, /* yielding backref index */ + HSES_YIELD_BR_LENGTH, /* yielding backref length */ + HSES_SAVE_BACKLOG, /* copying buffer to backlog */ + HSES_FLUSH_BITS, /* flush bit buffer */ + HSES_DONE, /* done */ +} HSE_state; + +#if HEATSHRINK_DEBUGGING_LOGS +#include +#include +#include +#define LOG(...) fprintf(stderr, __VA_ARGS__) +#define ASSERT(X) assert(X) +static const char *state_names[] = { + "not_full", + "filled", + "search", + "yield_tag_bit", + "yield_literal", + "yield_br_index", + "yield_br_length", + "save_backlog", + "flush_bits", + "done", +}; +#else +#define LOG(...) /* no-op */ +#define ASSERT(X) /* no-op */ +#endif + +// Encoder flags +enum { + FLAG_IS_FINISHING = 0x01, +}; + +typedef struct { + uint8_t *buf; /* output buffer */ + size_t buf_size; /* buffer size */ + size_t *output_size; /* bytes pushed to buffer, so far */ +} output_info; + +#define MATCH_NOT_FOUND ((uint16_t)-1) + +static uint16_t get_input_offset(heatshrink_encoder *hse); +static uint16_t get_input_buffer_size(heatshrink_encoder *hse); +static uint16_t get_lookahead_size(heatshrink_encoder *hse); +static void add_tag_bit(heatshrink_encoder *hse, output_info *oi, uint8_t tag); +static int can_take_byte(output_info *oi); +static int is_finishing(heatshrink_encoder *hse); +static void save_backlog(heatshrink_encoder *hse); + +/* Push COUNT (max 8) bits to the output buffer, which has room. */ +static void push_bits(heatshrink_encoder *hse, uint8_t count, uint8_t bits, + output_info *oi); +static uint8_t push_outgoing_bits(heatshrink_encoder *hse, output_info *oi); +static void push_literal_byte(heatshrink_encoder *hse, output_info *oi); + +#if HEATSHRINK_DYNAMIC_ALLOC +heatshrink_encoder *heatshrink_encoder_alloc(uint8_t window_sz2, + uint8_t lookahead_sz2) { + if ((window_sz2 < HEATSHRINK_MIN_WINDOW_BITS) || + (window_sz2 > HEATSHRINK_MAX_WINDOW_BITS) || + (lookahead_sz2 < HEATSHRINK_MIN_LOOKAHEAD_BITS) || + (lookahead_sz2 >= window_sz2)) { + return NULL; + } + + /* Note: 2 * the window size is used because the buffer needs to fit + * (1 << window_sz2) bytes for the current input, and an additional + * (1 << window_sz2) bytes for the previous buffer of input, which + * will be scanned for useful backreferences. */ + size_t buf_sz = (2U << window_sz2); + + heatshrink_encoder *hse = HEATSHRINK_MALLOC(sizeof(*hse) + buf_sz); + if (hse == NULL) { return NULL; } + hse->window_sz2 = window_sz2; + hse->lookahead_sz2 = lookahead_sz2; + heatshrink_encoder_reset(hse); + +#if HEATSHRINK_USE_INDEX + size_t index_sz = buf_sz*sizeof(uint16_t); + hse->search_index = HEATSHRINK_MALLOC(index_sz + sizeof(struct hs_index)); + if (hse->search_index == NULL) { + HEATSHRINK_FREE(hse, sizeof(*hse) + buf_sz); + return NULL; + } + hse->search_index->size = (uint16_t)index_sz; +#endif + + LOG("-- allocated encoder with buffer size of %zu (%u byte input size)\n", + buf_sz, get_input_buffer_size(hse)); + return hse; +} + +void heatshrink_encoder_free(heatshrink_encoder *hse) { + size_t buf_sz = (2U << HEATSHRINK_ENCODER_WINDOW_BITS(hse)); +#if HEATSHRINK_USE_INDEX + size_t index_sz = sizeof(struct hs_index) + hse->search_index->size; + HEATSHRINK_FREE(hse->search_index, index_sz); + (void)index_sz; +#endif + HEATSHRINK_FREE(hse, sizeof(heatshrink_encoder) + buf_sz); + (void)buf_sz; +} +#endif + +void heatshrink_encoder_reset(heatshrink_encoder *hse) { + size_t buf_sz = (2U << HEATSHRINK_ENCODER_WINDOW_BITS(hse)); + memset(hse->buffer, 0, buf_sz); + hse->input_size = 0; + hse->state = HSES_NOT_FULL; + hse->match_scan_index = 0; + hse->flags = 0; + hse->bit_index = 0x80; + hse->current_byte = 0x00; + hse->match_length = 0; + + hse->outgoing_bits = 0x0000; + hse->outgoing_bits_count = 0; + + #ifdef LOOP_DETECT + hse->loop_detect = (uint32_t)-1; + #endif +} + +HSE_sink_res heatshrink_encoder_sink(heatshrink_encoder *hse, + uint8_t *in_buf, size_t size, size_t *input_size) { + if ((hse == NULL) || (in_buf == NULL) || (input_size == NULL)) { + return HSER_SINK_ERROR_NULL; + } + + /* Sinking more content after saying the content is done, tsk tsk */ + if (is_finishing(hse)) { return HSER_SINK_ERROR_MISUSE; } + + /* Sinking more content before processing is done */ + if (hse->state != HSES_NOT_FULL) { return HSER_SINK_ERROR_MISUSE; } + + uint16_t write_offset = get_input_offset(hse) + hse->input_size; + uint16_t ibs = get_input_buffer_size(hse); + uint16_t rem = ibs - hse->input_size; + uint16_t cp_sz = rem < size ? rem : (uint16_t)size; + + memcpy(&hse->buffer[write_offset], in_buf, cp_sz); + *input_size = cp_sz; + hse->input_size += cp_sz; + + LOG("-- sunk %u bytes (of %zu) into encoder at %d, input buffer now has %u\n", + cp_sz, size, write_offset, hse->input_size); + if (cp_sz == rem) { + LOG("-- internal buffer is now full\n"); + hse->state = HSES_FILLED; + } + + return HSER_SINK_OK; +} + + +/*************** + * Compression * + ***************/ + +static uint16_t find_longest_match(heatshrink_encoder *hse, uint16_t start, + uint16_t end, const uint16_t maxlen, uint16_t *match_length); +static void do_indexing(heatshrink_encoder *hse); + +static HSE_state st_step_search(heatshrink_encoder *hse); +static HSE_state st_yield_tag_bit(heatshrink_encoder *hse, + output_info *oi); +static HSE_state st_yield_literal(heatshrink_encoder *hse, + output_info *oi); +static HSE_state st_yield_br_index(heatshrink_encoder *hse, + output_info *oi); +static HSE_state st_yield_br_length(heatshrink_encoder *hse, + output_info *oi); +static HSE_state st_save_backlog(heatshrink_encoder *hse); +static HSE_state st_flush_bit_buffer(heatshrink_encoder *hse, + output_info *oi); + +HSE_poll_res heatshrink_encoder_poll(heatshrink_encoder *hse, + uint8_t *out_buf, size_t out_buf_size, size_t *output_size) { + if ((hse == NULL) || (out_buf == NULL) || (output_size == NULL)) { + return HSER_POLL_ERROR_NULL; + } + if (out_buf_size == 0) { + LOG("-- MISUSE: output buffer size is 0\n"); + return HSER_POLL_ERROR_MISUSE; + } + *output_size = 0; + + output_info oi; + oi.buf = out_buf; + oi.buf_size = out_buf_size; + oi.output_size = output_size; + + while (1) { + LOG("-- polling, state %u (%s), flags 0x%02x\n", + hse->state, state_names[hse->state], hse->flags); + + const uint8_t in_state = hse->state; + HSE_state next_state; + + switch (in_state) { + case HSES_NOT_FULL: + return HSER_POLL_EMPTY; + case HSES_FILLED: + do_indexing(hse); + next_state = HSES_SEARCH; + break; + case HSES_SEARCH: + next_state = st_step_search(hse); + break; + case HSES_YIELD_TAG_BIT: + next_state = st_yield_tag_bit(hse, &oi); + break; + case HSES_YIELD_LITERAL: + next_state = st_yield_literal(hse, &oi); + break; + case HSES_YIELD_BR_INDEX: + next_state = st_yield_br_index(hse, &oi); + break; + case HSES_YIELD_BR_LENGTH: + next_state = st_yield_br_length(hse, &oi); + break; + case HSES_SAVE_BACKLOG: + next_state = st_save_backlog(hse); + break; + case HSES_FLUSH_BITS: + hse->state = (uint8_t)st_flush_bit_buffer(hse, &oi); + return HSER_POLL_EMPTY; + case HSES_DONE: + return HSER_POLL_EMPTY; + default: + LOG("-- bad state %s\n", state_names[hse->state]); + return HSER_POLL_ERROR_MISUSE; + } + hse->state = (uint8_t)next_state; + + if (hse->state == in_state) { + /* Check if output buffer is exhausted. */ + if (*output_size == out_buf_size) return HSER_POLL_MORE; + } + } +} + +HSE_finish_res heatshrink_encoder_finish(heatshrink_encoder *hse) { + if (hse == NULL) { return HSER_FINISH_ERROR_NULL; } + LOG("-- setting is_finishing flag\n"); + hse->flags |= FLAG_IS_FINISHING; + if (hse->state == HSES_NOT_FULL) { hse->state = HSES_FILLED; } + return hse->state == HSES_DONE ? HSER_FINISH_DONE : HSER_FINISH_MORE; +} + +static HSE_state st_step_search(heatshrink_encoder *hse) { + uint16_t window_length = get_input_buffer_size(hse); + uint16_t lookahead_sz = get_lookahead_size(hse); + uint16_t msi = hse->match_scan_index; + LOG("## step_search, scan @ +%d (%d/%d), input size %d\n", + msi, hse->input_size + msi, 2*window_length, hse->input_size); + + bool fin = is_finishing(hse); + if (msi > hse->input_size - (fin ? 1 : lookahead_sz)) { + /* Current search buffer is exhausted, copy it into the + * backlog and await more input. */ + LOG("-- end of search @ %d\n", msi); + return fin ? HSES_FLUSH_BITS : HSES_SAVE_BACKLOG; + } + + uint16_t input_offset = get_input_offset(hse); + uint16_t end = input_offset + msi; + uint16_t start = end - window_length; + + uint16_t max_possible = lookahead_sz; + if (hse->input_size - msi < lookahead_sz) { + max_possible = hse->input_size - msi; + } + + uint16_t match_length = 0; + uint16_t match_pos = find_longest_match(hse, + start, end, max_possible, &match_length); + + if (match_pos == MATCH_NOT_FOUND) { + LOG("ss Match not found\n"); + hse->match_scan_index++; + hse->match_length = 0; + return HSES_YIELD_TAG_BIT; + } else { + LOG("ss Found match of %d bytes at %d\n", match_length, match_pos); + hse->match_pos = match_pos; + hse->match_length = match_length; + ASSERT(match_pos <= 1 << HEATSHRINK_ENCODER_WINDOW_BITS(hse) /*window_length*/); + + return HSES_YIELD_TAG_BIT; + } +} + +static HSE_state st_yield_tag_bit(heatshrink_encoder *hse, + output_info *oi) { + if (can_take_byte(oi)) { + if (hse->match_length == 0) { + add_tag_bit(hse, oi, HEATSHRINK_LITERAL_MARKER); + return HSES_YIELD_LITERAL; + } else { + add_tag_bit(hse, oi, HEATSHRINK_BACKREF_MARKER); + hse->outgoing_bits = hse->match_pos - 1; + hse->outgoing_bits_count = HEATSHRINK_ENCODER_WINDOW_BITS(hse); + return HSES_YIELD_BR_INDEX; + } + } else { + return HSES_YIELD_TAG_BIT; /* output is full, continue */ + } +} + +static HSE_state st_yield_literal(heatshrink_encoder *hse, + output_info *oi) { + if (can_take_byte(oi)) { + push_literal_byte(hse, oi); + return HSES_SEARCH; + } else { + return HSES_YIELD_LITERAL; + } +} + +static HSE_state st_yield_br_index(heatshrink_encoder *hse, + output_info *oi) { + if (can_take_byte(oi)) { + LOG("-- yielding backref index %u\n", hse->match_pos); + if (push_outgoing_bits(hse, oi) > 0) { + return HSES_YIELD_BR_INDEX; /* continue */ + } else { + hse->outgoing_bits = hse->match_length - 1; + hse->outgoing_bits_count = HEATSHRINK_ENCODER_LOOKAHEAD_BITS(hse); + return HSES_YIELD_BR_LENGTH; /* done */ + } + } else { + return HSES_YIELD_BR_INDEX; /* continue */ + } +} + +static HSE_state st_yield_br_length(heatshrink_encoder *hse, + output_info *oi) { + if (can_take_byte(oi)) { + LOG("-- yielding backref length %u\n", hse->match_length); + if (push_outgoing_bits(hse, oi) > 0) { + return HSES_YIELD_BR_LENGTH; + } else { + hse->match_scan_index += hse->match_length; + hse->match_length = 0; + return HSES_SEARCH; + } + } else { + return HSES_YIELD_BR_LENGTH; + } +} + +static HSE_state st_save_backlog(heatshrink_encoder *hse) { + LOG("-- saving backlog\n"); + save_backlog(hse); + return HSES_NOT_FULL; +} + +static HSE_state st_flush_bit_buffer(heatshrink_encoder *hse, + output_info *oi) { + if (hse->bit_index == 0x80) { + LOG("-- done!\n"); + return HSES_DONE; + } else if (can_take_byte(oi)) { + LOG("-- flushing remaining byte (bit_index == 0x%02x)\n", hse->bit_index); + oi->buf[(*oi->output_size)++] = hse->current_byte; + LOG("-- done!\n"); + return HSES_DONE; + } else { + return HSES_FLUSH_BITS; + } +} + +static void add_tag_bit(heatshrink_encoder *hse, output_info *oi, uint8_t tag) { + LOG("-- adding tag bit: %d\n", tag); + push_bits(hse, 1, tag, oi); +} + +static uint16_t get_input_offset(heatshrink_encoder *hse) { + return get_input_buffer_size(hse); +} + +static uint16_t get_input_buffer_size(heatshrink_encoder *hse) { + (void)hse; + return (uint16_t)(1U << HEATSHRINK_ENCODER_WINDOW_BITS(hse)); +} + +static uint16_t get_lookahead_size(heatshrink_encoder *hse) { + (void)hse; + return (uint16_t)(1U << HEATSHRINK_ENCODER_LOOKAHEAD_BITS(hse)); +} + +static void do_indexing(heatshrink_encoder *hse) { +#if HEATSHRINK_USE_INDEX + /* Build an index array I that contains flattened linked lists + * for the previous instances of every byte in the buffer. + * + * For example, if buf[200] == 'x', then index[200] will either + * be an offset i such that buf[i] == 'x', or a negative offset + * to indicate end-of-list. This significantly speeds up matching, + * while only using sizeof(uint16_t)*sizeof(buffer) bytes of RAM. + * + * Future optimization options: + * 1. Since any negative value represents end-of-list, the other + * 15 bits could be used to improve the index dynamically. + * + * 2. Likewise, the last lookahead_sz bytes of the index will + * not be usable, so temporary data could be stored there to + * dynamically improve the index. + * */ + struct hs_index *hsi = HEATSHRINK_ENCODER_INDEX(hse); + int16_t last[256]; + memset(last, 0xFF, sizeof(last)); + + uint8_t * const data = hse->buffer; + int16_t * const index = hsi->index; + + const uint16_t input_offset = get_input_offset(hse); + const uint16_t end = input_offset + hse->input_size; + + for (int16_t i = 0; i < end; i++) { + uint8_t v = data[i]; + int16_t lv = last[v]; + index[i] = lv; + last[v] = i; + } +#else + (void)hse; +#endif +} + +static int is_finishing(heatshrink_encoder *hse) { + return hse->flags & FLAG_IS_FINISHING; +} + +static int can_take_byte(output_info *oi) { + return *oi->output_size < oi->buf_size; +} + +/* Return the longest match for the bytes at buf[end:end+maxlen] between + * buf[start] and buf[end-1]. If no match is found, return -1. */ +static uint16_t find_longest_match(heatshrink_encoder *hse, uint16_t start, + uint16_t end, const uint16_t maxlen, uint16_t *match_length) { + LOG("-- scanning for match of buf[%u:%u] between buf[%u:%u] (max %u bytes)\n", + end, end + maxlen, start, end + maxlen - 1, maxlen); + uint8_t *buf = hse->buffer; + + uint16_t match_maxlen = 0; + uint16_t match_index = MATCH_NOT_FOUND; + + uint16_t len = 0; + uint8_t * const needlepoint = &buf[end]; +#if HEATSHRINK_USE_INDEX + struct hs_index *hsi = HEATSHRINK_ENCODER_INDEX(hse); + int16_t pos = hsi->index[end]; + + while (pos - (int16_t)start >= 0) { + uint8_t * const pospoint = &buf[pos]; + + /* Only check matches that will potentially beat the current maxlen. + * This is redundant with the index if match_maxlen is 0, but the + * added branch overhead to check if it == 0 seems to be worse. */ + if (pospoint[match_maxlen] != needlepoint[match_maxlen]) { + pos = hsi->index[pos]; + continue; + } + + for (len = 1; len < maxlen; len++) { + if (pospoint[len] != needlepoint[len]) break; + } + + if (len > match_maxlen) { + match_maxlen = len; + match_index = (uint16_t)pos; + if (len == maxlen) { break; } /* won't find better */ + } + pos = hsi->index[pos]; + } +#else + for (int16_t pos=end - 1; pos - (int16_t)start >= 0; pos--) { + uint8_t * const pospoint = &buf[pos]; + if ((pospoint[match_maxlen] == needlepoint[match_maxlen]) + && (*pospoint == *needlepoint)) { + for (len=1; len cmp buf[%d] == 0x%02x against %02x (start %u)\n", + pos + len, pospoint[len], needlepoint[len], start); + } + if (pospoint[len] != needlepoint[len]) { break; } + } + if (len > match_maxlen) { + match_maxlen = len; + match_index = pos; + if (len == maxlen) { break; } /* don't keep searching */ + } + } + } +#endif + + const size_t break_even_point = + (1 + HEATSHRINK_ENCODER_WINDOW_BITS(hse) + + HEATSHRINK_ENCODER_LOOKAHEAD_BITS(hse)); + + /* Instead of comparing break_even_point against 8*match_maxlen, + * compare match_maxlen against break_even_point/8 to avoid + * overflow. Since MIN_WINDOW_BITS and MIN_LOOKAHEAD_BITS are 4 and + * 3, respectively, break_even_point/8 will always be at least 1. */ + if (match_maxlen > (break_even_point / 8)) { + LOG("-- best match: %u bytes at -%u\n", + match_maxlen, end - match_index); + *match_length = match_maxlen; + return end - match_index; + } + LOG("-- none found\n"); + return MATCH_NOT_FOUND; +} + +static uint8_t push_outgoing_bits(heatshrink_encoder *hse, output_info *oi) { + uint8_t count = 0; + uint8_t bits = 0; + if (hse->outgoing_bits_count > 8) { + count = 8; + bits = (uint8_t)(hse->outgoing_bits >> (hse->outgoing_bits_count - 8)); + } else { + count = hse->outgoing_bits_count; + bits = (uint8_t)hse->outgoing_bits; + } + + if (count > 0) { + LOG("-- pushing %d outgoing bits: 0x%02x\n", count, bits); + push_bits(hse, count, bits, oi); + hse->outgoing_bits_count -= count; + } + return count; +} + +/* Push COUNT (max 8) bits to the output buffer, which has room. + * Bytes are set from the lowest bits, up. */ +static void push_bits(heatshrink_encoder *hse, uint8_t count, uint8_t bits, + output_info *oi) { + ASSERT(count <= 8); + LOG("++ push_bits: %d bits, input of 0x%02x\n", count, bits); + + /* If adding a whole byte and at the start of a new output byte, + * just push it through whole and skip the bit IO loop. */ + if (count == 8 && hse->bit_index == 0x80) { + oi->buf[(*oi->output_size)++] = bits; + } else { + for (int i=count - 1; i>=0; i--) { + bool bit = bits & (1 << i); + if (bit) { hse->current_byte |= hse->bit_index; } + if (0) { + LOG(" -- setting bit %d at bit index 0x%02x, byte => 0x%02x\n", + bit ? 1 : 0, hse->bit_index, hse->current_byte); + } + hse->bit_index >>= 1; + if (hse->bit_index == 0x00) { + hse->bit_index = 0x80; + LOG(" > pushing byte 0x%02x\n", hse->current_byte); + oi->buf[(*oi->output_size)++] = hse->current_byte; + hse->current_byte = 0x00; + } + } + } +} + +static void push_literal_byte(heatshrink_encoder *hse, output_info *oi) { + uint16_t processed_offset = hse->match_scan_index - 1; + uint16_t input_offset = get_input_offset(hse) + processed_offset; + uint8_t c = hse->buffer[input_offset]; + LOG("-- yielded literal byte 0x%02x ('%c') from +%d\n", + c, isprint(c) ? c : '.', input_offset); + push_bits(hse, 8, c, oi); +} + +static void save_backlog(heatshrink_encoder *hse) { + size_t input_buf_sz = get_input_buffer_size(hse); + + uint16_t msi = hse->match_scan_index; + + /* Copy processed data to beginning of buffer, so it can be + * used for future matches. Don't bother checking whether the + * input is less than the maximum size, because if it isn't, + * we're done anyway. */ + uint16_t rem = (uint16_t)input_buf_sz - msi; // unprocessed bytes + uint16_t shift_sz = (uint16_t)input_buf_sz + rem; + + memmove(&hse->buffer[0], + &hse->buffer[input_buf_sz - rem], + shift_sz); + + hse->match_scan_index = 0; + hse->input_size -= (uint16_t)input_buf_sz - rem; +} diff --git a/lib/heatshrink/heatshrink_encoder.h b/lib/heatshrink/heatshrink_encoder.h new file mode 100644 index 0000000..1d9bb4f --- /dev/null +++ b/lib/heatshrink/heatshrink_encoder.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include "heatshrink_common.h" +#include "heatshrink_config.h" + +typedef enum { + HSER_SINK_OK, /* data sunk into input buffer */ + HSER_SINK_ERROR_NULL=-1, /* NULL argument */ + HSER_SINK_ERROR_MISUSE=-2, /* API misuse */ +} HSE_sink_res; + +typedef enum { + HSER_POLL_EMPTY, /* input exhausted */ + HSER_POLL_MORE, /* poll again for more output */ + HSER_POLL_ERROR_NULL=-1, /* NULL argument */ + HSER_POLL_ERROR_MISUSE=-2, /* API misuse */ +} HSE_poll_res; + +typedef enum { + HSER_FINISH_DONE, /* encoding is complete */ + HSER_FINISH_MORE, /* more output remaining; use poll */ + HSER_FINISH_ERROR_NULL=-1, /* NULL argument */ +} HSE_finish_res; + +#if HEATSHRINK_DYNAMIC_ALLOC +#define HEATSHRINK_ENCODER_WINDOW_BITS(HSE) \ + ((HSE)->window_sz2) +#define HEATSHRINK_ENCODER_LOOKAHEAD_BITS(HSE) \ + ((HSE)->lookahead_sz2) +#define HEATSHRINK_ENCODER_INDEX(HSE) \ + ((HSE)->search_index) +struct hs_index { + uint16_t size; + int16_t index[]; +}; +#else +#define HEATSHRINK_ENCODER_WINDOW_BITS(_) \ + (HEATSHRINK_STATIC_WINDOW_BITS) +#define HEATSHRINK_ENCODER_LOOKAHEAD_BITS(_) \ + (HEATSHRINK_STATIC_LOOKAHEAD_BITS) +#define HEATSHRINK_ENCODER_INDEX(HSE) \ + (&(HSE)->search_index) +struct hs_index { + uint16_t size; + int16_t index[2 << HEATSHRINK_STATIC_WINDOW_BITS]; +}; +#endif + +typedef struct { + uint16_t input_size; /* bytes in input buffer */ + uint16_t match_scan_index; + uint16_t match_length; + uint16_t match_pos; + uint16_t outgoing_bits; /* enqueued outgoing bits */ + uint8_t outgoing_bits_count; + uint8_t flags; + uint8_t state; /* current state machine node */ + uint8_t current_byte; /* current byte of output */ + uint8_t bit_index; /* current bit index */ +#if HEATSHRINK_DYNAMIC_ALLOC + uint8_t window_sz2; /* 2^n size of window */ + uint8_t lookahead_sz2; /* 2^n size of lookahead */ +#if HEATSHRINK_USE_INDEX + struct hs_index *search_index; +#endif + /* input buffer and / sliding window for expansion */ + uint8_t buffer[]; +#else + #if HEATSHRINK_USE_INDEX + struct hs_index search_index; + #endif + /* input buffer and / sliding window for expansion */ + uint8_t buffer[2 << HEATSHRINK_ENCODER_WINDOW_BITS(_)]; +#endif +} heatshrink_encoder; + +#if HEATSHRINK_DYNAMIC_ALLOC +/* Allocate a new encoder struct and its buffers. + * Returns NULL on error. */ +heatshrink_encoder *heatshrink_encoder_alloc(uint8_t window_sz2, + uint8_t lookahead_sz2); + +/* Free an encoder. */ +void heatshrink_encoder_free(heatshrink_encoder *hse); +#endif + +/* Reset an encoder. */ +void heatshrink_encoder_reset(heatshrink_encoder *hse); + +/* Sink up to SIZE bytes from IN_BUF into the encoder. + * INPUT_SIZE is set to the number of bytes actually sunk (in case a + * buffer was filled.). */ +HSE_sink_res heatshrink_encoder_sink(heatshrink_encoder *hse, + uint8_t *in_buf, size_t size, size_t *input_size); + +/* Poll for output from the encoder, copying at most OUT_BUF_SIZE bytes into + * OUT_BUF (setting *OUTPUT_SIZE to the actual amount copied). */ +HSE_poll_res heatshrink_encoder_poll(heatshrink_encoder *hse, + uint8_t *out_buf, size_t out_buf_size, size_t *output_size); + +/* Notify the encoder that the input stream is finished. + * If the return value is HSER_FINISH_MORE, there is still more output, so + * call heatshrink_encoder_poll and repeat. */ +HSE_finish_res heatshrink_encoder_finish(heatshrink_encoder *hse); diff --git a/lib/include/auth.h b/lib/include/auth.h new file mode 100644 index 0000000..76e13ad --- /dev/null +++ b/lib/include/auth.h @@ -0,0 +1,19 @@ +#pragma once + +#include "httpd.h" + +#ifndef HTTP_AUTH_REALM +#define HTTP_AUTH_REALM "Protected" +#endif + +#define HTTPD_AUTH_SINGLE 0 +#define HTTPD_AUTH_CALLBACK 1 + +#define AUTH_MAX_USER_LEN 32 +#define AUTH_MAX_PASS_LEN 32 + +//Parameter given to authWhatever functions. This callback returns the usernames/passwords the device +//has. +typedef int (* AuthGetUserPw)(HttpdConnData *connData, int no, char *user, int userLen, char *pass, int passLen); + +httpd_cgi_state authBasic(HttpdConnData *connData); diff --git a/lib/include/cgiwebsocket.h b/lib/include/cgiwebsocket.h new file mode 100644 index 0000000..5226886 --- /dev/null +++ b/lib/include/cgiwebsocket.h @@ -0,0 +1,33 @@ +#pragma once + +#include "httpd.h" + +#define WEBSOCK_FLAG_NONE 0 +#define WEBSOCK_FLAG_MORE (1<<0) //Set if the data is not the final data in the message; more follows +#define WEBSOCK_FLAG_BIN (1<<1) //Set if the data is binary instead of text +#define WEBSOCK_FLAG_CONT (1<<2) // set if this is a continuation frame (after WEBSOCK_FLAG_MORE) + +typedef struct Websock Websock; +typedef struct WebsockPriv WebsockPriv; + +typedef void(*WsConnectedCb)(Websock *ws); +typedef void(*WsRecvCb)(Websock *ws, char *data, int len, int flags); +typedef void(*WsSentCb)(Websock *ws); +typedef void(*WsCloseCb)(Websock *ws); + +struct Websock { + void *userData; + HttpdConnData *conn; + uint8_t status; + WsRecvCb recvCb; + WsSentCb sentCb; + WsCloseCb closeCb; + WebsockPriv *priv; +}; + +httpd_cgi_state cgiWebsocket(HttpdConnData *connData); +int cgiWebsocketSend(Websock *ws, const char *data, int len, int flags); +void cgiWebsocketClose(Websock *ws, int reason); +httpd_cgi_state cgiWebSocketRecv(HttpdConnData *connData, char *data, int len); +int cgiWebsockBroadcast(const char *resource, const char *data, int len, int flags); +void cgiWebsockMeasureBacklog(const char *resource, int *total, int *max); diff --git a/lib/include/httpd-platform.h b/lib/include/httpd-platform.h new file mode 100644 index 0000000..6ecf225 --- /dev/null +++ b/lib/include/httpd-platform.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +#include "httpd-utils.h" + +// opaque conn type struct +struct HttpdConnType; +typedef struct HttpdConnType HttpdConnType; +typedef HttpdConnType* ConnTypePtr; + +struct httpd_thread_handle; +typedef struct httpd_thread_handle httpd_thread_handle_t; + +struct httpd_options; + +#define httpd_printf(fmt, ...) printf(fmt, ##__VA_ARGS__) + +// Prototypes for porting + +int httpdConnSendData(ConnTypePtr conn, char *buff, int len); +void httpdConnDisconnect(ConnTypePtr conn); +void httpdPlatDisableTimeout(ConnTypePtr conn); +void httpdPlatInit(); +httpd_thread_handle_t* httpdPlatStart(struct httpd_options *opts); +void httpdPlatJoin(httpd_thread_handle_t * handle); +void httpdPlatLock(); +void httpdPlatUnlock(); +void* httpdPlatMalloc(size_t len); +void httpdPlatFree(void *ptr); +char* httpdPlatStrdup(const char *s); +void httpdPlatDelayMs(uint32_t ms); +void httpdPlatTaskEnd(); +int httpdPlatEspfsRead(void *dest, uint32_t offset, size_t len); + +void platHttpServerTask(void *pvParameters); +void* platHttpServerTaskPosix(void *pvParameters); diff --git a/lib/include/httpd-utils.h b/lib/include/httpd-utils.h new file mode 100644 index 0000000..67bb1f2 --- /dev/null +++ b/lib/include/httpd-utils.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +// Custom helpers +#define streq(a, b) (strcmp((const char*)(a), (const char*)(b)) == 0) +#define strneq(a, b, n) (strncmp((const char*)(a), (const char*)(b), (n)) == 0) +#define strstarts(a, b) strneq((a), (b), (int)strlen((b))) +#define last_char_n(str, n) ((str))[strlen((str)) - (n)] +#define last_char(str) last_char_n((str), 1) diff --git a/lib/include/httpd.h b/lib/include/httpd.h new file mode 100644 index 0000000..bec929d --- /dev/null +++ b/lib/include/httpd.h @@ -0,0 +1,204 @@ +#pragma once + +#include +#include +#include "httpd-platform.h" + +#ifndef GIT_HASH +#define GIT_HASH "unknown" +#endif + +// we must not use this macro outside the library, as the git hash is not defined there +#define HTTPDVER "0.4+MightyPork/libesphttpd#" GIT_HASH + +// default servername +#ifndef HTTPD_SERVERNAME +#define HTTPD_SERVERNAME "esp8266-httpd " HTTPDVER +#endif + +//Max length of request head. This is statically allocated for each connection. +#ifndef HTTPD_MAX_HEAD_LEN +#define HTTPD_MAX_HEAD_LEN 1024 +#endif + +//Max post buffer len. This is dynamically malloc'ed if needed. +#ifndef HTTPD_MAX_POST_LEN +#define HTTPD_MAX_POST_LEN 2048 +#endif + +//Max send buffer len. This is allocated on the stack. +#ifndef HTTPD_MAX_SENDBUFF_LEN +#define HTTPD_MAX_SENDBUFF_LEN 2048 +#endif + +//If some data can't be sent because the underlaying socket doesn't accept the data (like the nonos +//layer is prone to do), we put it in a backlog that is dynamically malloc'ed. This defines the max +//size of the backlog. +#ifndef HTTPD_MAX_BACKLOG_SIZE +#define HTTPD_MAX_BACKLOG_SIZE (4*1024) +#endif + +//Max len of CORS token. This is allocated in each connection +#ifndef HTTPD_MAX_CORS_TOKEN_LEN +#define HTTPD_MAX_CORS_TOKEN_LEN 256 +#endif + +#ifndef HTTPD_MAX_CONNECTIONS +#define HTTPD_MAX_CONNECTIONS 4 +#endif + +/** + * CGI handler state / return value + */ +typedef enum { + HTTPD_CGI_MORE = 0, + HTTPD_CGI_DONE = 1, + HTTPD_CGI_NOTFOUND = 2, + HTTPD_CGI_AUTHENTICATED = 3, +} httpd_cgi_state; + +/** + * HTTP method (verb) used for the request + */ +typedef enum { + HTTPD_METHOD_GET = 1, + HTTPD_METHOD_POST = 2, + HTTPD_METHOD_OPTIONS = 3, + HTTPD_METHOD_PUT = 4, + HTTPD_METHOD_DELETE = 5, + HTTPD_METHOD_PATCH = 6, + HTTPD_METHOD_HEAD = 7, +} httpd_method; + +/** + * Transfer mode + */ +typedef enum { + HTTPD_TRANSFER_CLOSE = 0, + HTTPD_TRANSFER_CHUNKED = 1, + HTTPD_TRANSFER_NONE = 2, +} httpd_transfer_opt; + +typedef struct HttpdPriv HttpdPriv; +typedef struct HttpdConnData HttpdConnData; +typedef struct HttpdPostData HttpdPostData; + +// Private static connection pool +extern HttpdConnData *s_connData[HTTPD_MAX_CONNECTIONS]; + +typedef httpd_cgi_state (* cgiSendCallback)(HttpdConnData *connData); +typedef httpd_cgi_state (* cgiRecvHandler)(HttpdConnData *connData, char *data, int len); + +struct httpd_options { + uint16_t port; +}; + +//A struct describing a http connection. This gets passed to cgi functions. +struct HttpdConnData { + ConnTypePtr conn; // The TCP connection. Exact type depends on the platform. + httpd_method requestType; // One of the HTTPD_METHOD_* values + char *url; // The URL requested, without hostname or GET arguments + char *getArgs; // The GET arguments for this request, if any. + const void *cgiArg; // Argument to the CGI function, as stated as the 3rd argument of + // the builtInUrls entry that referred to the CGI function. + const void *cgiArg2; // 4th argument of the builtInUrls entries, used to pass template file to the tpl handler. + void *cgiData; // Opaque data pointer for the CGI function + char *hostName; // Host name field of request + HttpdPriv *priv; // Opaque pointer to data for internal httpd housekeeping + cgiSendCallback cgi; // CGI function pointer + cgiRecvHandler recvHdl; // Handler for data received after headers, if any + HttpdPostData *post; // POST data structure + int remote_port; // Remote TCP port + uint8_t remote_ip[4]; // IP address of client + uint8_t slot; // Slot ID +}; + +//A struct describing the POST data sent inside the http connection. This is used by the CGI functions +struct HttpdPostData { + int len; // POST Content-Length + int buffSize; // The maximum length of the post buffer + int buffLen; // The amount of bytes in the current post buffer + int received; // The total amount of bytes received so far + char *buff; // Actual POST data buffer + char *multipartBoundary; //Text of the multipart boundary, if any +}; + +//A struct describing an url. This is the main struct that's used to send different URL requests to +//different routines. +typedef struct { + const char *url; + cgiSendCallback cgiCb; + const void *cgiArg; + const void *cgiArg2; +} HttpdBuiltInUrl; + +// macros for defining HttpdBuiltInUrl's + +/** Route with a CGI handler and two arguments */ +#define ROUTE_CGI_ARG2(path, handler, arg1, arg2) {(path), (handler), (void *)(arg1), (void *)(arg2)} + +/** Route with a CGI handler and one arguments */ +#define ROUTE_CGI_ARG(path, handler, arg1) ROUTE_CGI_ARG2((path), (handler), (arg1), NULL) + +/** Route with an argument-less CGI handler */ +#define ROUTE_CGI(path, handler) ROUTE_CGI_ARG2((path), (handler), NULL, NULL) + +/** Static file route (file loaded from espfs) */ +#define ROUTE_FILE(path, filepath) ROUTE_CGI_ARG((path), cgiEspFsHook, (const char*)(filepath)) + +/** Static file as a template with a replacer function */ +#define ROUTE_TPL(path, replacer) ROUTE_CGI_ARG((path), cgiEspFsTemplate, (TplCallback)(replacer)) + +/** Static file as a template with a replacer function, taking additional argument connData->cgiArg2 */ +#define ROUTE_TPL_FILE(path, replacer, filepath) ROUTE_CGI_ARG2((path), cgiEspFsTemplate, (TplCallback)(replacer), (filepath)) + +/** Redirect to some URL */ +#define ROUTE_REDIRECT(path, target) ROUTE_CGI_ARG((path), cgiRedirect, (const char*)(target)) + +/** Following routes are basic-auth protected */ +#define ROUTE_AUTH(path, passwdFunc) ROUTE_CGI_ARG((path), authBasic, (AuthGetUserPw)(passwdFunc)) + +/** Websocket endpoint */ +#define ROUTE_WS(path, callback) ROUTE_CGI_ARG((path), cgiWebsocket, (WsConnectedCb)(callback)) + +/** Catch-all filesystem route */ +#define ROUTE_FILESYSTEM() ROUTE_CGI("*", cgiEspFsHook) + +#define ROUTE_END() {NULL, NULL, NULL, NULL} + +const char *httpdGetVersion(void); + +httpd_cgi_state cgiRedirect(HttpdConnData *connData); +httpd_cgi_state cgiRedirectToHostname(HttpdConnData *connData); +httpd_cgi_state cgiRedirectApClientToHostname(HttpdConnData *connData); + +void httpdRedirect(HttpdConnData *conn, const char *newUrl); +int httpdUrlDecode(const char *val, int valLen, char *ret, int retLen); +int httpdFindArg(const char *line, const char *arg, char *buff, int buffLen); +httpd_thread_handle_t *httpdInit(const HttpdBuiltInUrl *fixedUrls, struct httpd_options *options); +void httpdJoin(httpd_thread_handle_t *handle); +const char *httpdGetMimetype(const char *url); +const char *httpdMethodName(httpd_method m); +void httdSetTransferMode(HttpdConnData *conn, int mode); +void httpdStartResponse(HttpdConnData *conn, int code); +void httpdHeader(HttpdConnData *conn, const char *field, const char *val); +void httpdEndHeaders(HttpdConnData *conn); +int httpdGetHeader(HttpdConnData *conn, const char *header, char *ret, int retLen); +int httpdSend(HttpdConnData *conn, const char *data, int len); +int httpdSend_js(HttpdConnData *conn, const char *data, int len); +int httpdSend_html(HttpdConnData *conn, const char *data, int len); +bool httpdFlushSendBuffer(HttpdConnData *conn); +void httpdContinue(HttpdConnData *conn); +void httpdConnSendStart(HttpdConnData *conn); +void httpdConnSendFinish(HttpdConnData *conn); +void httpdAddCacheHeaders(HttpdConnData *connData, const char *mime); + +int httpGetBacklogSize(const HttpdConnData *connData); +void httdResponseOptions(HttpdConnData *conn, int cors); + +//Platform dependent code should call these. +void httpdSentCb(ConnTypePtr conn, const char *remIp, int remPort); +void httpdRecvCb(ConnTypePtr conn, const char *remIp, int remPort, char *data, unsigned short len); +void httpdDisconCb(ConnTypePtr conn, const char *remIp, int remPort); +int httpdConnectCb(ConnTypePtr conn, const char *remIp, int remPort); +void httpdSetName(const char *name); diff --git a/lib/include/httpdespfs.h b/lib/include/httpdespfs.h new file mode 100644 index 0000000..da403e2 --- /dev/null +++ b/lib/include/httpdespfs.h @@ -0,0 +1,13 @@ +#pragma once + +#include "httpd.h" + +/** + * The template substitution callback. + * Returns CGI_MORE if more should be sent within the token, CGI_DONE otherwise. + */ +typedef httpd_cgi_state (* TplCallback)(HttpdConnData *connData, char *token, void **arg); + +httpd_cgi_state cgiEspFsHook(HttpdConnData *connData); +httpd_cgi_state cgiEspFsTemplate(HttpdConnData *connData); +int tplSend(HttpdConnData *conn, const char *str, int len); diff --git a/lib/include/logging.h b/lib/include/logging.h new file mode 100755 index 0000000..a5b1a18 --- /dev/null +++ b/lib/include/logging.h @@ -0,0 +1,193 @@ +#pragma once + +#include "httpd-platform.h" +#include + +#ifndef VERBOSE_LOGGING +#define VERBOSE_LOGGING 1 +#endif + +#ifndef LOG_EOL +#define LOG_EOL "\n" +#endif + +/** + * Print a startup banner message (printf syntax) + * Uses bright green color + */ +#define banner(fmt, ...) \ + do { \ + httpd_printf(LOG_EOL "\x1b[32;1m[i] " fmt "\x1b[0m" LOG_EOL, ##__VA_ARGS__); \ + } while(0) + +/** + * Same as 'info()', but enabled even if verbose logging is disabled. + * This can be used to print version etc under the banner. + */ +#define banner_info(fmt, ...) \ + do { \ + httpd_printf("\x1b[32m[i] " fmt "\x1b[0m"LOG_EOL, ##__VA_ARGS__); \ + } while(0) + +/** + * Empty line in the headers + */ +#define banner_gap() \ + do { \ + httpd_printf(LOG_EOL); \ + } while(0) + +#if VERBOSE_LOGGING + /** + * Print a debug log message (printf format) + */ + #define dbg(fmt, ...) \ + do { \ + httpd_printf("[ ] " fmt LOG_EOL, ##__VA_ARGS__); \ + } while(0) + + /** + * Print a info log message (printf format) + * Uses bright green color + */ + #define info(fmt, ...) \ + do { \ + httpd_printf("[i] " fmt "\x1b[0m"LOG_EOL, ##__VA_ARGS__); \ + } while(0) +#else + #define dbg(fmt, ...) + #define info(fmt, ...) +#endif + +/** + * Print a error log message (printf format) + * Uses bright red color + */ +#define error(fmt, ...) \ + do { \ + httpd_printf("[E] " fmt "\x1b[0m"LOG_EOL, ##__VA_ARGS__); \ + } while(0) + +/** + * Print a warning log message (printf format) + * Uses bright yellow color + */ +#define warn(fmt, ...) \ + do { \ + httpd_printf("[W] " fmt "\x1b[0m"LOG_EOL, ##__VA_ARGS__); \ + } while(0) + +// --------------- logging categories -------------------- + +#ifndef DEBUG_ROUTER +#define DEBUG_ROUTER 1 +#endif + +#ifndef DEBUG_ESPFS +#define DEBUG_ESPFS 1 +#endif + +#ifndef DEBUG_WS +#define DEBUG_WS 1 +#endif + +#ifndef DEBUG_HTTP +#define DEBUG_HTTP 1 +#endif + +#ifndef DEBUG_HTTPC +#define DEBUG_HTTPC 1 +#endif + +#ifndef DEBUG_HEATSHRINK +#define DEBUG_HEATSHRINK 1 +#endif + +#ifndef DEBUG_MALLOC +#define DEBUG_MALLOC 0 +#endif + +// router (resolving urls to serve) +#if DEBUG_ROUTER +#define router_warn(...) warn(__VA_ARGS__) +#define router_dbg(...) dbg(__VA_ARGS__) +#define router_error(...) error(__VA_ARGS__) +#define router_info(...) info(__VA_ARGS__) +#else +#define router_dbg(...) +#define router_warn(...) +#define router_error(...) +#define router_info(...) +#endif + +// filesystem +#if DEBUG_ESPFS +#define espfs_warn(...) warn(__VA_ARGS__) +#define espfs_dbg(...) dbg(__VA_ARGS__) +#define espfs_error(...) error(__VA_ARGS__) +#define espfs_info(...) info(__VA_ARGS__) +#else +#define espfs_dbg(...) +#define espfs_warn(...) +#define espfs_error(...) +#define espfs_info(...) +#endif + +// websocket +#if DEBUG_WS +#define ws_warn(...) warn(__VA_ARGS__) +#define ws_dbg(...) dbg(__VA_ARGS__) +#define ws_error(...) error(__VA_ARGS__) +#define ws_info(...) info(__VA_ARGS__) +#else +#define ws_dbg(...) +#define ws_warn(...) +#define ws_error(...) +#define ws_info(...) +#endif + +// server +#if DEBUG_HTTP +#define http_warn(...) warn(__VA_ARGS__) +#define http_dbg(...) dbg(__VA_ARGS__) +#define http_error(...) error(__VA_ARGS__) +#define http_info(...) info(__VA_ARGS__) +#else +#define http_dbg(...) +#define http_warn(...) +#define http_error(...) +#define http_info(...) +#endif + +// client +#if DEBUG_HTTPC +#define httpc_warn(...) warn(__VA_ARGS__) +#define httpc_dbg(...) dbg(__VA_ARGS__) +#define httpc_error(...) error(__VA_ARGS__) +#define httpc_info(...) info(__VA_ARGS__) +#else +#define httpc_dbg(...) +#define httpc_warn(...) +#define httpc_error(...) +#define httpc_info(...) +#endif + +// captive portal +#if DEBUG_HEATSHRINK +#define heatshrink_warn(...) warn(__VA_ARGS__) +#define heatshrink_dbg(...) dbg(__VA_ARGS__) +#define heatshrink_error(...) error(__VA_ARGS__) +#define heatshrink_info(...) info(__VA_ARGS__) +#else +#define heatshrink_dbg(...) +#define heatshrink_warn(...) +#define heatshrink_error(...) +#define heatshrink_info(...) +#endif + +// all malloc usage +#if DEBUG_MALLOC +#define mem_dbg(...) dbg(__VA_ARGS__) +#else +#define mem_dbg(...) +#endif diff --git a/lib/src/cgiwebsocket.c b/lib/src/cgiwebsocket.c new file mode 100644 index 0000000..f6c906b --- /dev/null +++ b/lib/src/cgiwebsocket.c @@ -0,0 +1,415 @@ +/* +Websocket support for esphttpd. Inspired by https://github.com/dangrie158/ESP-8266-WebSocket +*/ + +/* + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * Jeroen Domburg wrote this file. As long as you retain + * this notice you can do whatever you want with this stuff. If we meet some day, + * and you think this stuff is worth it, you can buy me a beer in return. + * ---------------------------------------------------------------------------- + */ + +#include +#include "httpd.h" +#include "httpd-platform.h" + +#include "utils/sha1.h" +#include "utils/base64.h" +#include "cgiwebsocket.h" +#include "logging.h" + +#define WS_KEY_IDENTIFIER "Sec-WebSocket-Key: " +#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +/* from IEEE RFC6455 sec 5.2 + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-------+-+-------------+-------------------------------+ + |F|R|R|R| opcode|M| Payload len | Extended payload length | + |I|S|S|S| (4) |A| (7) | (16/64) | + |N|V|V|V| |S| | (if payload len==126/127) | + | |1|2|3| |K| | | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + | Extended payload length continued, if payload len == 127 | + + - - - - - - - - - - - - - - - +-------------------------------+ + | |Masking-key, if MASK set to 1 | + +-------------------------------+-------------------------------+ + | Masking-key (continued) | Payload Data | + +-------------------------------- - - - - - - - - - - - - - - - + + : Payload Data continued ... : + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + | Payload Data continued ... | + +---------------------------------------------------------------+ +*/ + +#define FLAG_FIN (1 << 7) + +#define OPCODE_CONTINUE 0x0 +#define OPCODE_TEXT 0x1 +#define OPCODE_BINARY 0x2 +#define OPCODE_CLOSE 0x8 +#define OPCODE_PING 0x9 +#define OPCODE_PONG 0xA + +#define FLAGS_MASK ((uint8_t)0xF0) +#define OPCODE_MASK ((uint8_t)0x0F) +#define IS_MASKED ((uint8_t)(1<<7)) +#define PAYLOAD_MASK ((uint8_t)0x7F) + +typedef struct WebsockFrame WebsockFrame; + +#define ST_FLAGS 0 +#define ST_LEN0 1 +#define ST_LEN1 2 +#define ST_LEN2 3 +//... +#define ST_LEN8 9 +#define ST_MASK1 10 +#define ST_MASK4 13 +#define ST_PAYLOAD 14 + +struct WebsockFrame { + uint8_t flags; + uint8_t len8; + uint64_t len; + uint8_t mask[4]; +}; + +struct WebsockPriv { + struct WebsockFrame fr; + uint8_t maskCtr; + uint8_t frameCont; + uint8_t closedHere; + int wsStatus; + Websock *next; //in linked list +}; + +static Websock *llStart = NULL; + +static int sendFrameHead(Websock *ws, int opcode, int len) +{ + uint8_t buf[14]; + int i = 0; + buf[i++] = opcode; + if (len > 65535) { + buf[i++] = 127; + buf[i++] = 0; + buf[i++] = 0; + buf[i++] = 0; + buf[i++] = 0; + buf[i++] = len >> 24; + buf[i++] = len >> 16; + buf[i++] = len >> 8; + buf[i++] = len; + } else if (len > 125) { + buf[i++] = 126; + buf[i++] = len >> 8; + buf[i++] = len; + } else { + buf[i++] = len; + } +// ws_dbg("WS: Sent frame head for payload of %d bytes.", len); + return httpdSend(ws->conn, (char *) buf, i); +} + +int cgiWebsocketSend(Websock *ws, const char *data, int len, int flags) +{ + int r = 0; + int fl = 0; + // Continuation frame has opcode 0 + if (!(flags & WEBSOCK_FLAG_CONT)) { + if (flags & WEBSOCK_FLAG_BIN) { + fl = OPCODE_BINARY; + } else { + fl = OPCODE_TEXT; + } + } + // add FIN to last frame + if (!(flags & WEBSOCK_FLAG_MORE)) { fl |= FLAG_FIN; } + sendFrameHead(ws, fl, len); + if (len != 0) { r = httpdSend(ws->conn, data, len); } + r &= httpdFlushSendBuffer(ws->conn); + return r; +} + +//Broadcast data to all websockets at a specific url. Returns the amount of connections sent to. +int cgiWebsockBroadcast(const char *resource, const char *data, int len, int flags) +{ + Websock *lw = llStart; + int ret = 0; + while (lw != NULL) { + if (strcmp(lw->conn->url, resource) == 0) { + httpdConnSendStart(lw->conn); + if (!cgiWebsocketSend(lw, data, len, flags)) { + // send failed, do not try further (assume memory is clogged due to max backlog size) + + // HACK: If conn->conn is not NULL, {httpdConnSendFinish} would try to flush again + // (cgiWebsocketSend already tried to flush, and that's where the error came from, possibly) + // we remove it temporarily to bypass that + ConnTypePtr oldpriv = lw->conn->conn; + lw->conn->conn = NULL; + httpdConnSendFinish(lw->conn); + // -> and now we put it back for later + lw->conn->conn = oldpriv; + + // Abort + return -1; + } + httpdConnSendFinish(lw->conn); + ret++; + } + lw = lw->priv->next; + } + return ret; +} + +/** this is used for estimation how full the ram is */ +void cgiWebsockMeasureBacklog(const char *resource, int *total, int *max) +{ + Websock *lw = llStart; + int bMax = 0; + int bTotal = 0; + while (lw != NULL) { + if (strcmp(lw->conn->url, resource) == 0) { + //lw->conn + int bs = httpGetBacklogSize(lw->conn); + bTotal += bs; + if (bs > bMax) { bMax = bs; } + } + lw = lw->priv->next; + } + *total = bTotal; + *max = bMax; +} + +void cgiWebsocketClose(Websock *ws, int reason) +{ + char rs[2] = {reason >> 8, reason & 0xff}; + sendFrameHead(ws, FLAG_FIN | OPCODE_CLOSE, 2); + httpdSend(ws->conn, rs, 2); + ws->priv->closedHere = 1; + httpdFlushSendBuffer(ws->conn); +} + + +static void websockFree(Websock *ws) +{ + ws_dbg("Ws: Free"); + if (ws->closeCb) { ws->closeCb(ws); } + //Clean up linked list + if (llStart == ws) { + llStart = ws->priv->next; + } else if (llStart) { + Websock *lws = llStart; + //Find ws that links to this one. + while (lws != NULL && lws->priv->next != ws) { lws = lws->priv->next; } + if (lws != NULL) { lws->priv->next = ws->priv->next; } + } + if (ws->priv) { httpdPlatFree(ws->priv); } +} + +httpd_cgi_state cgiWebSocketRecv(HttpdConnData *connData, char *data, int len) +{ + int i, j, sl; + httpd_cgi_state r = HTTPD_CGI_MORE; + int wasHeaderByte; + Websock *ws = (Websock *) connData->cgiData; + for (i = 0; i < len; i++) { +// httpd_printf("Ws: State %d byte 0x%02X\n", ws->priv->wsStatus, data[i]); + wasHeaderByte = 1; + if (ws->priv->wsStatus == ST_FLAGS) { + ws->priv->maskCtr = 0; + ws->priv->frameCont = 0; + ws->priv->fr.flags = (uint8_t) data[i]; + ws->priv->wsStatus = ST_LEN0; + } else if (ws->priv->wsStatus == ST_LEN0) { + ws->priv->fr.len8 = (uint8_t) data[i]; + if ((ws->priv->fr.len8 & 127) >= 126) { + ws->priv->fr.len = 0; + ws->priv->wsStatus = ST_LEN1; + } else { + ws->priv->fr.len = ws->priv->fr.len8 & 127; + ws->priv->wsStatus = (ws->priv->fr.len8 & IS_MASKED) ? ST_MASK1 : ST_PAYLOAD; + } + } else if (ws->priv->wsStatus <= ST_LEN8) { + ws->priv->fr.len = (ws->priv->fr.len << 8) | data[i]; + if (((ws->priv->fr.len8 & 127) == 126 && ws->priv->wsStatus == ST_LEN2) || ws->priv->wsStatus == ST_LEN8) { + ws->priv->wsStatus = (ws->priv->fr.len8 & IS_MASKED) ? ST_MASK1 : ST_PAYLOAD; + } else { + ws->priv->wsStatus++; + } + } else if (ws->priv->wsStatus <= ST_MASK4) { + ws->priv->fr.mask[ws->priv->wsStatus - ST_MASK1] = data[i]; + ws->priv->wsStatus++; + } else { + //Was a payload byte. + wasHeaderByte = 0; + } + + if (ws->priv->wsStatus == ST_PAYLOAD && wasHeaderByte) { + //We finished parsing the header, but i still is on the last header byte. Move one forward so + //the payload code works as usual. + i++; + } + //Also finish parsing frame if we haven't received any payload bytes yet, but the length of the frame + //is zero. + if (ws->priv->wsStatus == ST_PAYLOAD) { + //Okay, header is in; this is a data byte. We're going to process all the data bytes we have + //received here at the same time; no more byte iterations till the end of this frame. + //First, unmask the data + sl = len - i; +// ws_dbg("Ws: Frame payload. wasHeaderByte %d fr.len %d sl %d cmd 0x%x", wasHeaderByte, (int)ws->priv->fr.len, (int)sl, ws->priv->fr.flags); + if (sl > ws->priv->fr.len) { sl = ws->priv->fr.len; } + for (j = 0; j < sl; j++) { data[i + j] ^= (ws->priv->fr.mask[(ws->priv->maskCtr++) & 3]); } + +// if (DEBUG_WS) { +// ws_dbg("Unmasked: "); +// for (j = 0; j < sl; j++) httpd_printf("%02X ", data[i + j] & 0xff); +// ws_dbg("\n"); +// } + + //Inspect the header to see what we need to do. + if ((ws->priv->fr.flags & OPCODE_MASK) == OPCODE_PING) { + if (ws->priv->fr.len > 125) { + if (!ws->priv->frameCont) { cgiWebsocketClose(ws, 1002); } + r = HTTPD_CGI_DONE; + break; + } else { + if (!ws->priv->frameCont) { sendFrameHead(ws, OPCODE_PONG | FLAG_FIN, ws->priv->fr.len); } + if (sl > 0) { httpdSend(ws->conn, data + i, sl); } + } + } else if ((ws->priv->fr.flags & OPCODE_MASK) == OPCODE_TEXT || + (ws->priv->fr.flags & OPCODE_MASK) == OPCODE_BINARY || + (ws->priv->fr.flags & OPCODE_MASK) == OPCODE_CONTINUE) { + if (sl > ws->priv->fr.len) { sl = ws->priv->fr.len; } + if (!(ws->priv->fr.len8 & IS_MASKED)) { + //We're a server; client should send us masked packets. + cgiWebsocketClose(ws, 1002); + r = HTTPD_CGI_DONE; + break; + } else { + int flags = 0; + if ((ws->priv->fr.flags & OPCODE_MASK) == OPCODE_BINARY) { flags |= WEBSOCK_FLAG_BIN; } + if ((ws->priv->fr.flags & FLAG_FIN) == 0) { flags |= WEBSOCK_FLAG_MORE; } + if (ws->recvCb) { ws->recvCb(ws, data + i, sl, flags); } + } + } else if ((ws->priv->fr.flags & OPCODE_MASK) == OPCODE_CLOSE) { +// ws_dbg("WS: Got close frame"); + if (!ws->priv->closedHere) { +// ws_dbg("WS: Sending response close frame, %x %x (i=%d, len=%d)", data[i], data[i+1], i, len); + int cause = 1000; + if (i <= len - 2) { + cause = ((data[i] << 8) & 0xff00) + (data[i + 1] & 0xff); + } + cgiWebsocketClose(ws, cause); + } + r = HTTPD_CGI_DONE; + break; + } else { + if (!ws->priv->frameCont) ws_error("WS: Unknown opcode 0x%X", ws->priv->fr.flags & OPCODE_MASK); + } + i += sl - 1; + ws->priv->fr.len -= sl; + if (ws->priv->fr.len == 0) { + ws->priv->wsStatus = ST_FLAGS; //go receive next frame + } else { + ws->priv->frameCont = 1; //next payload is continuation of this frame. + } + } + } + if (r == HTTPD_CGI_DONE) { + //We're going to tell the main webserver we're done. The webserver expects us to clean up by ourselves + //we're chosing to be done. Do so. + websockFree(ws); + httpdPlatFree(connData->cgiData); + connData->cgiData = NULL; + } + return r; +} + +//Websocket 'cgi' implementation +httpd_cgi_state cgiWebsocket(HttpdConnData *connData) +{ + char buff[256]; + int i; + sha1nfo s; + if (connData->conn == NULL) { + //Connection aborted. Clean up. + ws_dbg("WS: Cleanup"); + if (connData->cgiData) { + Websock *ws = (Websock *) connData->cgiData; + websockFree(ws); + httpdPlatFree(connData->cgiData); + connData->cgiData = NULL; + } + return HTTPD_CGI_DONE; + } + + if (connData->cgiData == NULL) { +// httpd_printf("WS: First call\n"); + //First call here. Check if client headers are OK, send server header. + i = httpdGetHeader(connData, "Upgrade", buff, sizeof(buff) - 1); + ws_dbg("WS: Upgrade: %s", buff); + if (i && strcasecmp(buff, "websocket") == 0) { + i = httpdGetHeader(connData, "Sec-WebSocket-Key", buff, sizeof(buff) - 1); + if (i) { +// httpd_printf("WS: Key: %s\n", buff); + //Seems like a WebSocket connection. + // Alloc structs + connData->cgiData = httpdPlatMalloc(sizeof(Websock)); + if (connData->cgiData == NULL) { + ws_error("Can't allocate mem for websocket"); + return HTTPD_CGI_DONE; + } + memset(connData->cgiData, 0, sizeof(Websock)); + Websock *ws = (Websock *) connData->cgiData; + ws->priv = httpdPlatMalloc(sizeof(WebsockPriv)); + if (ws->priv == NULL) { + ws_error("Can't allocate mem for websocket priv"); + httpdPlatFree(connData->cgiData); + connData->cgiData = NULL; + return HTTPD_CGI_DONE; + } + memset(ws->priv, 0, sizeof(WebsockPriv)); + ws->conn = connData; + //Reply with the right headers. + strcat(buff, WS_GUID); + sha1_init(&s); + sha1_write(&s, buff, strlen(buff)); + httdSetTransferMode(connData, HTTPD_TRANSFER_NONE); + httpdStartResponse(connData, 101); + httpdHeader(connData, "Upgrade", "websocket"); + httpdHeader(connData, "Connection", "upgrade"); + base64_encode(20, sha1_result(&s), sizeof(buff), buff); + httpdHeader(connData, "Sec-WebSocket-Accept", buff); + httpdEndHeaders(connData); + //Set data receive handler + connData->recvHdl = cgiWebSocketRecv; + //Inform CGI function we have a connection + WsConnectedCb connCb = connData->cgiArg; + connCb(ws); + //Insert ws into linked list + if (llStart == NULL) { + llStart = ws; + } else { + Websock *lw = llStart; + while (lw->priv->next) { lw = lw->priv->next; } + lw->priv->next = ws; + } + return HTTPD_CGI_MORE; + } + } + //No valid websocket connection + httpdStartResponse(connData, 500); + httpdEndHeaders(connData); + return HTTPD_CGI_DONE; + } + + //Sending is done. Call the sent callback if we have one. + Websock *ws = (Websock *) connData->cgiData; + if (ws && ws->sentCb) { ws->sentCb(ws); } + + return HTTPD_CGI_MORE; +} diff --git a/lib/src/httpd-loop.c b/lib/src/httpd-loop.c new file mode 100644 index 0000000..857846d --- /dev/null +++ b/lib/src/httpd-loop.c @@ -0,0 +1,235 @@ +/* +ESP8266 web server - platform-dependent routines, FreeRTOS version +Thanks to my collague at Espressif for writing the foundations of this code. +*/ + +#include "httpd.h" +#include "platform.h" +#include "httpd-platform.h" +#include +#include +#include +#include +#include +#include +#include "logging.h" + +static int httpPort; +static int httpMaxConnCt; + +struct HttpdConnType { + int fd; + int needWriteDoneNotif; + int needsClose; + int port; + char ip[4]; +}; + +static HttpdConnType s_rconn[HTTPD_MAX_CONNECTIONS]; + +int httpdConnSendData(ConnTypePtr conn, char *buff, int len) +{ + conn->needWriteDoneNotif = 1; + return (write(conn->fd, buff, len) >= 0); +} + +void httpdConnDisconnect(ConnTypePtr conn) +{ + conn->needsClose = 1; + conn->needWriteDoneNotif = 1; //because the real close is done in the writable select code +} + +#define RECV_BUF_SIZE 2048 + +void platHttpServerTask(void *pvParameters) +{ + int32_t listenfd; + int32_t remotefd; + int32_t len; + int32_t ret; + int x; + int maxfdp = 0; + char *precvbuf; + fd_set readset, writeset; + struct sockaddr name; + //struct timeval timeout; + struct sockaddr_in server_addr; + struct sockaddr_in remote_addr; + + struct httpd_options *options = pvParameters; + if (options == NULL) { + httpPort = 80; + } else { + httpPort = options->port; + } + + for (x = 0; x < HTTPD_MAX_CONNECTIONS; x++) { + s_rconn[x].fd = -1; + } + + /* Construct local address structure */ + memset(&server_addr, 0, sizeof(server_addr)); /* Zero out structure */ + server_addr.sin_family = AF_INET; /* Internet address family */ + server_addr.sin_addr.s_addr = INADDR_ANY; /* Any incoming interface */ + //server_addr.sin_len = sizeof(server_addr); + server_addr.sin_port = htons(httpPort); /* Local port */ + + /* Create socket for incoming connections */ + do { + listenfd = socket(AF_INET, SOCK_STREAM, 0); + if (listenfd == -1) { + error("platHttpServerTask: failed to create sock!"); + httpdPlatDelayMs(1000); + } + } while (listenfd == -1); + + /* Bind to the local port */ + do { + ret = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr)); + if (ret != 0) { + error("platHttpServerTask: failed to bind!"); + httpdPlatDelayMs(1000); + } + } while (ret != 0); + + do { + /* Listen to the local connection */ + ret = listen(listenfd, HTTPD_MAX_CONNECTIONS); + if (ret != 0) { + error("platHttpServerTask: failed to listen!"); + httpdPlatDelayMs(1000); + } + + } while (ret != 0); + + info("esphttpd: active and listening to connections."); + while (1) { + // clear fdset, and set the select function wait time + int socketsFull = 1; + maxfdp = 0; + FD_ZERO(&readset); + FD_ZERO(&writeset); + //timeout.tv_sec = 2; + //timeout.tv_usec = 0; + + for (x = 0; x < HTTPD_MAX_CONNECTIONS; x++) { + if (s_rconn[x].fd != -1) { + FD_SET(s_rconn[x].fd, &readset); + if (s_rconn[x].needWriteDoneNotif) FD_SET(s_rconn[x].fd, &writeset); + if (s_rconn[x].fd > maxfdp) { maxfdp = s_rconn[x].fd; } + } else { + socketsFull = 0; + } + } + + if (!socketsFull) { + FD_SET(listenfd, &readset); + if (listenfd > maxfdp) { maxfdp = listenfd; } + } + + //polling all exist client handle,wait until readable/writable + ret = select(maxfdp + 1, &readset, &writeset, NULL, NULL);//&timeout + if (ret > 0) { + //See if we need to accept a new connection + if (FD_ISSET(listenfd, &readset)) { + len = sizeof(struct sockaddr_in); + remotefd = accept(listenfd, (struct sockaddr *) &remote_addr, (socklen_t *) &len); + if (remotefd < 0) { + warn("platHttpServerTask: Huh? Accept failed."); + continue; + } + for (x = 0; x < HTTPD_MAX_CONNECTIONS; x++) { if (s_rconn[x].fd == -1) { break; }} + if (x == HTTPD_MAX_CONNECTIONS) { + warn("platHttpServerTask: Huh? Got accept with all slots full."); + continue; + } + int keepAlive = 1; //enable keepalive + int keepIdle = 60; //60s + int keepInterval = 5; //5s + int keepCount = 3; //retry times + + setsockopt(remotefd, SOL_SOCKET, SO_KEEPALIVE, (void *) &keepAlive, sizeof(keepAlive)); + setsockopt(remotefd, IPPROTO_TCP, TCP_KEEPIDLE, (void *) &keepIdle, sizeof(keepIdle)); + setsockopt(remotefd, IPPROTO_TCP, TCP_KEEPINTVL, (void *) &keepInterval, sizeof(keepInterval)); + setsockopt(remotefd, IPPROTO_TCP, TCP_KEEPCNT, (void *) &keepCount, sizeof(keepCount)); + + s_rconn[x].fd = remotefd; + s_rconn[x].needWriteDoneNotif = 0; + s_rconn[x].needsClose = 0; + + len = sizeof(name); + getpeername(remotefd, &name, (socklen_t *) &len); + struct sockaddr_in *piname = (struct sockaddr_in *) &name; + + s_rconn[x].port = piname->sin_port; + memcpy(&s_rconn[x].ip, &piname->sin_addr.s_addr, sizeof(s_rconn[x].ip)); + + httpdConnectCb(&s_rconn[x], s_rconn[x].ip, s_rconn[x].port); + //os_timer_disarm(&connData[x].conn->stop_watch); + //os_timer_setfn(&connData[x].conn->stop_watch, (os_timer_func_t *)httpserver_conn_watcher, connData[x].conn); + //os_timer_arm(&connData[x].conn->stop_watch, STOP_TIMER, 0); +// dbg("httpserver acpt index %d sockfd %d!", x, remotefd); + } + + //See if anything happened on the existing connections. + for (x = 0; x < HTTPD_MAX_CONNECTIONS; x++) { + //Skip empty slots + if (s_rconn[x].fd == -1) { continue; } + + //Check for write availability first: the read routines may write needWriteDoneNotif while + //the select didn't check for that. + if (s_rconn[x].needWriteDoneNotif && FD_ISSET(s_rconn[x].fd, &writeset)) { + s_rconn[x].needWriteDoneNotif = 0; //Do this first, httpdSentCb may write something making this 1 again. + if (s_rconn[x].needsClose) { + //Do callback and close fd. + httpdDisconCb(&s_rconn[x], s_rconn[x].ip, s_rconn[x].port); + close(s_rconn[x].fd); + s_rconn[x].fd = -1; + } else { + httpdSentCb(&s_rconn[x], s_rconn[x].ip, s_rconn[x].port); + } + } + + if (FD_ISSET(s_rconn[x].fd, &readset)) { + precvbuf = (char *) malloc(RECV_BUF_SIZE); + if (precvbuf == NULL) { + error("platHttpServerTask: memory exhausted!"); + httpdDisconCb(&s_rconn[x], s_rconn[x].ip, s_rconn[x].port); + close(s_rconn[x].fd); + s_rconn[x].fd = -1; + } + ret = (int) recv(s_rconn[x].fd, precvbuf, RECV_BUF_SIZE, 0); + if (ret > 0) { + //Data received. Pass to httpd. + httpdRecvCb(&s_rconn[x], s_rconn[x].ip, s_rconn[x].port, precvbuf, ret); + } else { + //recv error,connection close + httpdDisconCb(&s_rconn[x], s_rconn[x].ip, s_rconn[x].port); + close(s_rconn[x].fd); + s_rconn[x].fd = -1; + } + if (precvbuf) { free(precvbuf); } + } + } + } + } + +//Deinit code, not used here. + /*release data connection*/ + for (x = 0; x < HTTPD_MAX_CONNECTIONS; x++) { + //find all valid handle + if (s_connData[x]->conn == NULL) { continue; } + if (s_connData[x]->conn->fd >= 0) { + //os_timer_disarm((os_timer_t *)&connData[x].conn->stop_watch); // ??? + + close(s_connData[x]->conn->fd); + s_connData[x]->conn->fd = -1; + s_connData[x]->conn = NULL; + if (s_connData[x]->cgi != NULL) { s_connData[x]->cgi(s_connData[x]); } //flush cgi data + } + } + /*release listen socket*/ + close(listenfd); + + httpdPlatTaskEnd(); +} diff --git a/lib/src/httpd.c b/lib/src/httpd.c new file mode 100644 index 0000000..51970c3 --- /dev/null +++ b/lib/src/httpd.c @@ -0,0 +1,1090 @@ +/* +Esp8266 http server - core routines +*/ + +/* + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * Jeroen Domburg wrote this file. As long as you retain + * this notice you can do whatever you want with this stuff. If we meet some day, + * and you think this stuff is worth it, you can buy me a beer in return. + * ---------------------------------------------------------------------------- + */ + +#include +#include +#include "httpd.h" +#include "httpd-platform.h" +#include "logging.h" + +//This gets set at init time. +static const HttpdBuiltInUrl *builtInUrls; +static const char *serverName = HTTPD_SERVERNAME; + +typedef struct HttpSendBacklogItem HttpSendBacklogItem; + +struct HttpSendBacklogItem { + int len; + HttpSendBacklogItem *next; + char data[]; +}; + +//Flags +#define HFL_HTTP11 (1<<0) +#define HFL_CHUNKED (1<<1) +#define HFL_SENDINGBODY (1<<2) +#define HFL_DISCONAFTERSENT (1<<3) +#define HFL_NOCONNECTIONSTR (1<<4) +#define HFL_NOCORS (1<<5) + +//Private data for http connection +struct HttpdPriv { + char head[HTTPD_MAX_HEAD_LEN]; + char corsToken[HTTPD_MAX_CORS_TOKEN_LEN]; + int headPos; + char *sendBuff; + int sendBuffLen; + char *chunkHdr; + HttpSendBacklogItem *sendBacklog; + int sendBacklogSize; + int flags; +}; + + +//Connection pool +HttpdConnData *s_connData[HTTPD_MAX_CONNECTIONS]; + +//Struct to keep extension->mime data in +typedef struct { + const char *ext; + const char *mimetype; +} MimeMap; + + +//#define RSTR(a) ((const char)(a)) + +//The mappings from file extensions to mime types. If you need an extra mime type, +//add it here. +static const MimeMap mimeTypes[] = { + {"htm", "text/html"}, + {"html", "text/html"}, + {"css", "text/css"}, + {"js", "text/javascript"}, + {"txt", "text/plain"}, + {"csv", "text/csv"}, + {"ico", "image/x-icon"}, + {"jpg", "image/jpeg"}, + {"jpeg", "image/jpeg"}, + {"png", "image/png"}, + {"gif", "image/gif"}, + {"bmp", "image/bmp"}, + {"svg", "image/svg+xml"}, + {"xml", "text/xml"}, + {"json", "application/json"}, + {NULL, "text/html"}, //default value +}; + +//Returns a static char* to a mime type for a given url to a file. +const char *httpdGetMimetype(const char *url) +{ + int i = 0; + //Go find the extension + const char *ext = url + (strlen(url) - 1); + while (ext != url && *ext != '.') { ext--; } + if (*ext == '.') { ext++; } + + while (mimeTypes[i].ext != NULL && strcasecmp(ext, mimeTypes[i].ext) != 0) { i++; } + + return mimeTypes[i].mimetype; +} + +const char *httpdMethodName(httpd_method m) +{ + switch (m) { + default: + case HTTPD_METHOD_GET: + return "GET"; + case HTTPD_METHOD_POST: + return "POST"; + case HTTPD_METHOD_OPTIONS: + return "OPTIONS"; + case HTTPD_METHOD_PUT: + return "PUT"; + case HTTPD_METHOD_DELETE: + return "DELETE"; + case HTTPD_METHOD_PATCH: + return "PATCH"; + case HTTPD_METHOD_HEAD: + return "HEAD"; + } +} + +const char *code2str(int code) +{ + switch (code) { + case 200: + return "OK"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 403: + return "Forbidden"; + case 400: + return "Bad Request"; + case 404: + return "Not Found"; + default: + if (code >= 500) { return "Server Error"; } + if (code >= 400) { return "Client Error"; } + return "OK"; + } +} + +/** + * Add sensible cache control headers to avoid needless asset reloading + * + * @param connData + * @param mime - mime type string + */ +void httpdAddCacheHeaders(HttpdConnData *connData, const char *mime) +{ + if (streq(mime, "text/html")) { return; } + if (streq(mime, "text/plain")) { return; } + if (streq(mime, "text/csv")) { return; } + if (streq(mime, "application/json")) { return; } + + httpdHeader(connData, "Cache-Control", "max-age=7200, public, must-revalidate"); +} + +const char *httpdGetVersion(void) +{ + return HTTPDVER; +} + +int httpGetBacklogSize(const HttpdConnData *conn) +{ + HttpSendBacklogItem *bl = conn->priv->sendBacklog; + if (!bl) { return 0; } + int bytes = 0; + while (bl != NULL) { + bytes += bl->len; + bl = bl->next; + } + return bytes; +} + +//Looks up the connData info for a specific connection +static HttpdConnData *httpdFindConnData(ConnTypePtr conn, const char *remIp, int remPort) +{ + for (int i = 0; i < HTTPD_MAX_CONNECTIONS; i++) { + if (s_connData[i] && s_connData[i]->remote_port == remPort && + memcmp(s_connData[i]->remote_ip, remIp, 4) == 0) { + s_connData[i]->conn = conn; + return s_connData[i]; + } + } + //Shouldn't happen. + http_error("*** Unknown connection %d.%d.%d.%d:%d", remIp[0] & 0xff, remIp[1] & 0xff, remIp[2] & 0xff, remIp[3] & 0xff, remPort); + httpdConnDisconnect(conn); + return NULL; +} + +//Retires a connection for re-use +static void httpdRetireConn(HttpdConnData *conn) +{ + if (conn->priv->sendBacklog != NULL) { + HttpSendBacklogItem *i, *j; + i = conn->priv->sendBacklog; + do { + j = i; + i = i->next; + httpdPlatFree(j); + } while (i != NULL); + } + if (conn->post->buff != NULL) { httpdPlatFree(conn->post->buff); } + if (conn->post != NULL) { httpdPlatFree(conn->post); } + if (conn->priv != NULL) { httpdPlatFree(conn->priv); } + if (conn) { httpdPlatFree(conn); } + for (int i = 0; i < HTTPD_MAX_CONNECTIONS; i++) { + if (s_connData[i] == conn) { s_connData[i] = NULL; } + } +} + +//Stupid li'l helper function that returns the value of a hex char. +static int httpdHexVal(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 0; +} + +//Decode a percent-encoded value. +//Takes the valLen bytes stored in val, and converts it into at most retLen bytes that +//are stored in the ret buffer. Returns the actual amount of bytes used in ret. Also +//zero-terminates the ret buffer. +int httpdUrlDecode(const char *val, int valLen, char *ret, int retLen) +{ + int s = 0, d = 0; + int esced = 0, escVal = 0; + while (s < valLen && d < retLen) { + if (esced == 1) { + escVal = httpdHexVal(val[s]) << 4; + esced = 2; + } else if (esced == 2) { + escVal += httpdHexVal(val[s]); + ret[d++] = escVal; + esced = 0; + } else if (val[s] == '%') { + esced = 1; + } else if (val[s] == '+') { + ret[d++] = ' '; + } else { + ret[d++] = val[s]; + } + s++; + } + if (d < retLen) { ret[d] = 0; } + return d; +} + +//Find a specific arg in a string of get- or post-data. +//Line is the string of post/get-data, arg is the name of the value to find. The +//zero-terminated result is written in buff, with at most buffLen bytes used. The +//function returns the length of the result, or -1 if the value wasn't found. The +//returned string will be urldecoded already. +int httpdFindArg(const char *line, const char *arg, char *buff, int buffLen) +{ + const char *p, *e; + if (line == NULL) { return -1; } + const int arglen = (int) strlen(arg); + p = line; + while (p != NULL && *p != '\n' && *p != '\r' && *p != 0) { + router_dbg("findArg: %s", p); + if (strstarts(p, arg) && p[arglen] == '=') { + p += arglen + 1; //move p to start of value + e = strstr(p, "&"); + if (e == NULL) { e = p + strlen(p); } + router_dbg("findArg: val %s len %d", p, (e - p)); + return httpdUrlDecode(p, (e - p), buff, buffLen); + } + p = strstr(p, "&"); + if (p != NULL) { p += 1; } + } + router_error("Finding arg %s in %s: Not found :/", arg, line); + return -1; //not found +} + +//Get the value of a certain header in the HTTP client head +//Returns true when found, false when not found. +int httpdGetHeader(HttpdConnData *conn, const char *header, char *ret, int retLen) +{ + char *p = conn->priv->head; + p = p + strlen(p) + 1; //skip GET/POST part + p = p + strlen(p) + 1; //skip HTTP part + while (p < (conn->priv->head + conn->priv->headPos)) { + while (*p <= 32 && *p != 0) { p++; } //skip crap at start + //See if this is the header + if (strstarts(p, header) && p[strlen(header)] == ':') { + //Skip 'key:' bit of header line + p = p + strlen(header) + 1; + //Skip past spaces after the colon + while (*p == ' ') { p++; } + //Copy from p to end + while (*p != 0 && *p != '\r' && *p != '\n' && retLen > 1) { + *ret++ = *p++; + retLen--; + } + //Zero-terminate string + *ret = 0; + //All done :) + return 1; + } + p += strlen(p) + 1; //Skip past end of string and \0 terminator + } + return 0; +} + +void httdSetTransferMode(HttpdConnData *conn, int mode) +{ + if (mode == HTTPD_TRANSFER_CLOSE) { + conn->priv->flags &= ~HFL_CHUNKED; + conn->priv->flags &= ~HFL_NOCONNECTIONSTR; + } else if (mode == HTTPD_TRANSFER_CHUNKED) { + conn->priv->flags |= HFL_CHUNKED; + conn->priv->flags &= ~HFL_NOCONNECTIONSTR; + } else if (mode == HTTPD_TRANSFER_NONE) { + conn->priv->flags &= ~HFL_CHUNKED; + conn->priv->flags |= HFL_NOCONNECTIONSTR; + } +} + +void httdResponseOptions(HttpdConnData *conn, int cors) +{ + if (cors == 0) { conn->priv->flags |= HFL_NOCORS; } +} + +//Start the response headers. +void httpdStartResponse(HttpdConnData *conn, int code) +{ + char buff[256]; + int l; + const char *connStr = "Connection: close\r\n"; + if (conn->priv->flags & HFL_CHUNKED) { connStr = "Transfer-Encoding: chunked\r\n"; } + if (conn->priv->flags & HFL_NOCONNECTIONSTR) { connStr = ""; } + l = sprintf(buff, "HTTP/1.%d %d %s\r\nServer: %s\r\n%s", + (conn->priv->flags & HFL_HTTP11) ? 1 : 0, + code, + code2str(code), + serverName, + connStr); + httpdSend(conn, buff, l); + + if (0 == (conn->priv->flags & HFL_NOCORS)) { + // CORS headers + httpdSend(conn, "Access-Control-Allow-Origin: *\r\n", -1); + httpdSend(conn, "Access-Control-Allow-Methods: GET,POST,OPTIONS\r\n", -1); + } +} + +//Send a http header. +void httpdHeader(HttpdConnData *conn, const char *field, const char *val) +{ + httpdSend(conn, field, -1); + httpdSend(conn, ": ", -1); + httpdSend(conn, val, -1); + httpdSend(conn, "\r\n", -1); +} + +//Finish the headers. +void httpdEndHeaders(HttpdConnData *conn) +{ + httpdSend(conn, "\r\n", -1); + conn->priv->flags |= HFL_SENDINGBODY; +} + +//Redirect to the given URL. +void httpdRedirect(HttpdConnData *conn, const char *newUrl) +{ + http_dbg("Redirecting to %s", newUrl); + httpdStartResponse(conn, 302); + httpdHeader(conn, "Location", newUrl); + httpdEndHeaders(conn); + httpdSend(conn, "Moved to ", -1); + httpdSend(conn, newUrl, -1); +} + +//Use this as a cgi function to redirect one url to another. +httpd_cgi_state cgiRedirect(HttpdConnData *connData) +{ + if (connData->conn == NULL) { + //Connection aborted. Clean up. + return HTTPD_CGI_DONE; + } + httpdRedirect(connData, (char *) connData->cgiArg); + return HTTPD_CGI_DONE; +} + +//Used to spit out a 404 error +static httpd_cgi_state cgiNotFound(HttpdConnData *connData) +{ + if (connData->conn == NULL) { return HTTPD_CGI_DONE; } + httpdStartResponse(connData, 404); + httpdEndHeaders(connData); + httpdSend(connData, "404 File not found.", -1); + return HTTPD_CGI_DONE; +} + +//This CGI function redirects to a fixed url of http://[hostname]/ if hostname field of request isn't +//already that hostname. Use this in combination with a DNS server that redirects everything to the +//ESP in order to load a HTML page as soon as a phone, tablet etc connects to the ESP. Watch out: +//this will also redirect connections when the ESP is in STA mode, potentially to a hostname that is not +//in the 'official' DNS and so will fail. +httpd_cgi_state cgiRedirectToHostname(HttpdConnData *connData) +{ + static const char hostFmt[] = "http://%s/"; + char *buff; + int isIP = 0; + int x; + if (connData->conn == NULL) { + //Connection aborted. Clean up. + return HTTPD_CGI_DONE; + } + if (connData->hostName == NULL) { + http_warn("Huh? No hostname."); + return HTTPD_CGI_NOTFOUND; + } + + //Quick and dirty code to see if host is an IP + if (strlen(connData->hostName) > 8) { + isIP = 1; + for (x = 0; x < strlen(connData->hostName); x++) { + if (connData->hostName[x] != '.' && (connData->hostName[x] < '0' || connData->hostName[x] > '9')) { isIP = 0; } + } + } + if (isIP) { return HTTPD_CGI_NOTFOUND; } + //Check hostname; pass on if the same + if (strcmp(connData->hostName, (char *) connData->cgiArg) == 0) { return HTTPD_CGI_NOTFOUND; } + //Not the same. Redirect to real hostname. + buff = httpdPlatMalloc(strlen((char *) connData->cgiArg) + sizeof(hostFmt)); + if (buff == NULL) { + //Bail out + return HTTPD_CGI_DONE; + } + sprintf(buff, hostFmt, (char *) connData->cgiArg); + http_info("Redirecting to hostname url %s", buff); + httpdRedirect(connData, buff); + httpdPlatFree(buff); + return HTTPD_CGI_DONE; +} + + +//Add data to the send buffer. len is the length of the data. If len is -1 +//the data is seen as a C-string. +//Returns 1 for success, 0 for out-of-memory. +int httpdSend(HttpdConnData *conn, const char *data, int len) +{ + if (conn->conn == NULL) { return 0; } + if (len < 0) { len = strlen(data); } + if (len == 0) { return 0; } + if (conn->priv->flags & HFL_CHUNKED && conn->priv->flags & HFL_SENDINGBODY && conn->priv->chunkHdr == NULL) { + if (conn->priv->sendBuffLen + len + 6 > HTTPD_MAX_SENDBUFF_LEN) { return 0; } + //Establish start of chunk + conn->priv->chunkHdr = &conn->priv->sendBuff[conn->priv->sendBuffLen]; + strcpy(conn->priv->chunkHdr, "0000\r\n"); + conn->priv->sendBuffLen += 6; + } + if (conn->priv->sendBuffLen + len > HTTPD_MAX_SENDBUFF_LEN) { return 0; } + memcpy(conn->priv->sendBuff + conn->priv->sendBuffLen, data, len); + conn->priv->sendBuffLen += len; + return 1; +} + +static char httpdHexNibble(int val) +{ + val &= 0xf; + if (val < 10) { return '0' + val; } + return 'A' + (val - 10); +} + +#define httpdSend_orDie(conn, data, len) do { if (!httpdSend((conn), (data), (len))) return false; } while (0) + +/* encode for HTML. returns 0 or 1 - 1 = success */ +int httpdSend_html(HttpdConnData *conn, const char *data, int len) +{ + int start = 0, end = 0; + char c; + if (conn->conn == NULL) { return 0; } + if (len < 0) { len = (int) strlen(data); } + if (len == 0) { return 0; } + + for (end = 0; end < len; end++) { + c = data[end]; + if (c == 0) { + // we found EOS + break; + } + + if (c == '"' || c == '\'' || c == '<' || c == '>') { + if (start < end) httpdSend_orDie(conn, data + start, end - start); + start = end + 1; + } + + if (c == '"') httpdSend_orDie(conn, """, 5); + else if (c == '\'') httpdSend_orDie(conn, "'", 5); + else if (c == '<') httpdSend_orDie(conn, "<", 4); + else if (c == '>') httpdSend_orDie(conn, ">", 4); + } + + if (start < end) httpdSend_orDie(conn, data + start, end - start); + return 1; +} + +/* encode for JS. returns 0 or 1 - 1 = success */ +int httpdSend_js(HttpdConnData *conn, const char *data, int len) +{ + int start = 0, end = 0; + char c; + if (conn->conn == NULL) { return 0; } + if (len < 0) { len = (int) strlen(data); } + if (len == 0) { return 0; } + + for (end = 0; end < len; end++) { + c = data[end]; + if (c == 0) { + // we found EOS + break; + } + + if (c == '"' || c == '\\' || c == '\'' || c == '<' || c == '>' || c == '\n' || c == '\r') { + if (start < end) httpdSend_orDie(conn, data + start, end - start); + start = end + 1; + } + + if (c == '"') httpdSend_orDie(conn, "\\\"", 2); + else if (c == '\'') httpdSend_orDie(conn, "\\'", 2); + else if (c == '\\') httpdSend_orDie(conn, "\\\\", 2); + else if (c == '<') httpdSend_orDie(conn, "\\u003C", 6); + else if (c == '>') httpdSend_orDie(conn, "\\u003E", 6); + else if (c == '\n') httpdSend_orDie(conn, "\\n", 2); + else if (c == '\r') httpdSend_orDie(conn, "\\r", 2); + } + + if (start < end) httpdSend_orDie(conn, data + start, end - start); + return 1; +} + +//Function to send any data in conn->priv->sendBuff. Do not use in CGIs unless you know what you +//are doing! Also, if you do set conn->cgi to NULL to indicate the connection is closed, do it BEFORE +//calling this. +//Returns false if data could not be sent nor put in backlog. +bool httpdFlushSendBuffer(HttpdConnData *conn) +{ + int r, len; + if (conn->conn == NULL) { return false; } + if (conn->priv->chunkHdr != NULL) { + //We're sending chunked data, and the chunk needs fixing up. + //Finish chunk with cr/lf + httpdSend(conn, "\r\n", 2); + //Calculate length of chunk + len = ((&conn->priv->sendBuff[conn->priv->sendBuffLen]) - conn->priv->chunkHdr) - 8; + //Fix up chunk header to correct value + conn->priv->chunkHdr[0] = httpdHexNibble(len >> 12); + conn->priv->chunkHdr[1] = httpdHexNibble(len >> 8); + conn->priv->chunkHdr[2] = httpdHexNibble(len >> 4); + conn->priv->chunkHdr[3] = httpdHexNibble(len >> 0); + //Reset chunk hdr for next call + conn->priv->chunkHdr = NULL; + } + if (conn->priv->flags & HFL_CHUNKED && conn->priv->flags & HFL_SENDINGBODY && conn->cgi == NULL) { + //Connection finished sending whatever needs to be sent. Add NULL chunk to indicate this. + strcpy(&conn->priv->sendBuff[conn->priv->sendBuffLen], "0\r\n\r\n"); + conn->priv->sendBuffLen += 5; + } + if (conn->priv->sendBuffLen != 0) { + r = httpdConnSendData(conn->conn, conn->priv->sendBuff, conn->priv->sendBuffLen); + if (!r) { + //Can't send this for some reason. Dump packet in backlog, we can send it later. + if (conn->priv->sendBacklogSize + conn->priv->sendBuffLen > HTTPD_MAX_BACKLOG_SIZE) { + http_error("Httpd: Backlog overrun, dropped %dB", conn->priv->sendBuffLen); + conn->priv->sendBuffLen = 0; + return false; + } + HttpSendBacklogItem *i = httpdPlatMalloc(sizeof(HttpSendBacklogItem) + conn->priv->sendBuffLen); + if (i == NULL) { + http_error("Httpd: Backlog: malloc failed, out of memory!"); + return false; + } + memcpy(i->data, conn->priv->sendBuff, conn->priv->sendBuffLen); + i->len = conn->priv->sendBuffLen; + i->next = NULL; + if (conn->priv->sendBacklog == NULL) { + conn->priv->sendBacklog = i; + } else { + HttpSendBacklogItem *e = conn->priv->sendBacklog; + while (e->next != NULL) { e = e->next; } + e->next = i; + } + conn->priv->sendBacklogSize += conn->priv->sendBuffLen; + } + conn->priv->sendBuffLen = 0; + } + return true; +} + +void httpdCgiIsDone(HttpdConnData *conn) +{ + conn->cgi = NULL; //no need to call this anymore + if (conn->priv->flags & HFL_CHUNKED) { + http_dbg("Pool slot %d is done. Cleaning up for next req", conn->slot); + httpdFlushSendBuffer(conn); + //Note: Do not clean up sendBacklog, it may still contain data at this point. + conn->priv->headPos = 0; + conn->post->len = -1; + conn->priv->flags = 0; + if (conn->post->buff) { httpdPlatFree(conn->post->buff); } + conn->post->buff = NULL; + conn->post->buffLen = 0; + conn->post->received = 0; + conn->hostName = NULL; + } else { + //Cannot re-use this connection. Mark to get it killed after all data is sent. + conn->priv->flags |= HFL_DISCONAFTERSENT; + } +} + +//Callback called when the data on a socket has been successfully +//sent. +void httpdSentCb(ConnTypePtr rconn, const char *remIp, int remPort) +{ + HttpdConnData *conn = httpdFindConnData(rconn, remIp, remPort); + httpdContinue(conn); +} + +//Can be called after a CGI function has returned HTTPD_CGI_MORE to +//resume handling an open connection asynchronously +void httpdContinue(HttpdConnData *conn) +{ + int r; + httpdPlatLock(); + + char *sendBuff; + + if (conn == NULL) { return; } + + if (conn->priv->sendBacklog != NULL) { + //We have some backlog to send first. + HttpSendBacklogItem *next = conn->priv->sendBacklog->next; + httpdConnSendData(conn->conn, conn->priv->sendBacklog->data, conn->priv->sendBacklog->len); + conn->priv->sendBacklogSize -= conn->priv->sendBacklog->len; + httpdPlatFree(conn->priv->sendBacklog); + conn->priv->sendBacklog = next; + httpdPlatUnlock(); + return; + } + + if (conn->priv->flags & HFL_DISCONAFTERSENT) { //Marked for destruction? + http_dbg("Pool slot %d is done. Closing.", conn->slot); + httpdConnDisconnect(conn->conn); + httpdPlatUnlock(); + return; //No need to call httpdFlushSendBuffer. + } + + //If we don't have a CGI function, there's nothing to do but wait for something from the client. + if (conn->cgi == NULL) { + httpdPlatUnlock(); + return; + } + + sendBuff = httpdPlatMalloc(HTTPD_MAX_SENDBUFF_LEN); + if (sendBuff == NULL) { + http_error("Malloc of sendBuff failed!"); + httpdPlatUnlock(); + return; + } + conn->priv->sendBuff = sendBuff; + conn->priv->sendBuffLen = 0; + r = conn->cgi(conn); //Execute cgi fn. + if (r == HTTPD_CGI_DONE) { + httpdCgiIsDone(conn); + } + if (r == HTTPD_CGI_NOTFOUND || r == HTTPD_CGI_AUTHENTICATED) { + http_error("ERROR! CGI fn returns code %d after sending data! Bad CGI!", r); + httpdCgiIsDone(conn); + } + httpdFlushSendBuffer(conn); + httpdPlatFree(sendBuff); + httpdPlatUnlock(); +} + +//This is called when the headers have been received and the connection is ready to send +//the result headers and data. +//We need to find the CGI function to call, call it, and dependent on what it returns either +//find the next cgi function, wait till the cgi data is sent or close up the connection. +static void httpdProcessRequest(HttpdConnData *conn) +{ + int r; + int i = 0; + if (conn->url == NULL) { + router_warn("WtF? url = NULL"); + return; //Shouldn't happen + } + + // CORS preflight, allow the token we received before + if (conn->requestType == HTTPD_METHOD_OPTIONS) { + httpdStartResponse(conn, 200); + httpdHeader(conn, "Access-Control-Allow-Headers", conn->priv->corsToken); + httpdEndHeaders(conn); + httpdCgiIsDone(conn); + + router_dbg("CORS preflight resp sent."); + return; + } + + + //See if we can find a CGI that's happy to handle the request. + while (1) { + //Look up URL in the built-in URL table. + while (builtInUrls[i].url != NULL) { + int match = 0; + const char const *route = builtInUrls[i].url; + //See if there's a literal match + if (streq(route, conn->url)) { match = 1; } + //See if there's a wildcard match (*) + if (last_char(route) == '*' && + strneq(route, conn->url, strlen(route) - 1)) { + match = 1; + } + // Optional slash (/?) + if (last_char(route) == '?' && last_char_n(route, 2) == '/' && + strneq(route, conn->url, strlen(route) - 2) && + strlen(conn->url) <= strlen(route) - 1) { + match = 1; + } + if (match) { + router_dbg("Matched route #%d, url=%s", i, route); + conn->cgiData = NULL; + conn->cgi = builtInUrls[i].cgiCb; + conn->cgiArg = builtInUrls[i].cgiArg; + conn->cgiArg2 = builtInUrls[i].cgiArg2; + break; + } + i++; + } + if (builtInUrls[i].url == NULL) { + //Drat, we're at the end of the URL table. This usually shouldn't happen. Well, just + //generate a built-in 404 to handle this. + router_warn("%s not found. 404!", conn->url); + conn->cgi = cgiNotFound; + } + + //Okay, we have a CGI function that matches the URL. See if it wants to handle the + //particular URL we're supposed to handle. + r = conn->cgi(conn); + if (r == HTTPD_CGI_MORE) { + //Yep, it's happy to do so and has more data to send. + if (conn->recvHdl) { + //Seems the CGI is planning to do some long-term communications with the socket. + //Disable the timeout on it, so we won't run into that. + httpdPlatDisableTimeout(conn->conn); + } + httpdFlushSendBuffer(conn); + return; + } else if (r == HTTPD_CGI_DONE) { + //Yep, it's happy to do so and already is done sending data. + httpdCgiIsDone(conn); + return; + } else if (r == HTTPD_CGI_NOTFOUND || r == HTTPD_CGI_AUTHENTICATED) { + //URL doesn't want to handle the request: either the data isn't found or there's no + //need to generate a login screen. + i++; //look at next url the next iteration of the loop. + } + } +} + +//Parse a line of header data and modify the connection data accordingly. +static void httpdParseHeader(char *h, HttpdConnData *conn) +{ + int i; + char firstLine = 0; + + if (strstarts(h, "GET ")) { + conn->requestType = HTTPD_METHOD_GET; + firstLine = 1; + } else if (strstarts(h, "Host:")) { + i = 5; + while (h[i] == ' ') { i++; } + conn->hostName = &h[i]; + } else if (strstarts(h, "POST ")) { + conn->requestType = HTTPD_METHOD_POST; + firstLine = 1; + } else if (strstarts(h, "PUT ")) { + conn->requestType = HTTPD_METHOD_PUT; + firstLine = 1; + } else if (strstarts(h, "PATCH ")) { + conn->requestType = HTTPD_METHOD_PATCH; + firstLine = 1; + } else if (strstarts(h, "OPTIONS ")) { + conn->requestType = HTTPD_METHOD_OPTIONS; + firstLine = 1; + } else if (strstarts(h, "DELETE ")) { + conn->requestType = HTTPD_METHOD_DELETE; + firstLine = 1; + } + + if (firstLine) { + char *e; + + //Skip past the space after POST/GET + i = 0; + while (h[i] != ' ') { i++; } + conn->url = h + i + 1; + + //Figure out end of url. + e = strstr(conn->url, " "); + if (e == NULL) { return; } //wtf? + *e = 0; //terminate url part + e++; //Skip to protocol indicator + while (*e == ' ') { e++; } //Skip spaces. + //If HTTP/1.1, note that and set chunked encoding + if (strcasecmp(e, "HTTP/1.1") == 0) { + conn->priv->flags |= HFL_HTTP11 | HFL_CHUNKED; + } + + http_info("URL = %s", conn->url); + //Parse out the URL part before the GET parameters. + conn->getArgs = strstr(conn->url, "?"); + if (conn->getArgs != 0) { + *conn->getArgs = 0; + conn->getArgs++; + http_dbg("GET args = %s", conn->getArgs); + } else { + conn->getArgs = NULL; + } + } else if (strstarts(h, "Connection:")) { + i = 11; + //Skip trailing spaces + while (h[i] == ' ') { i++; } + if (strstarts(&h[i], "close")) { conn->priv->flags &= ~HFL_CHUNKED; } //Don't use chunked conn + } else if (strstarts(h, "Content-Length:")) { + i = 15; + //Skip trailing spaces + while (h[i] == ' ') { i++; } + //Get POST data length + conn->post->len = (int) strtol(h + i, NULL, 10); + + // Allocate the buffer + if (conn->post->len > HTTPD_MAX_POST_LEN) { + // we'll stream this in in chunks + conn->post->buffSize = HTTPD_MAX_POST_LEN; + } else { + conn->post->buffSize = conn->post->len; + } + http_dbg("Mallocced buffer for %d + 1 bytes of post data.", conn->post->buffSize); + conn->post->buff = (char *) httpdPlatMalloc(conn->post->buffSize + 1); + if (conn->post->buff == NULL) { + http_error("...failed!"); + return; + } + conn->post->buffLen = 0; + } else if (strstarts(h, "Content-Type: ")) { + if (strstr(h, "multipart/form-data")) { + // It's multipart form data so let's pull out the boundary for future use + char *b; + if ((b = strstr(h, "boundary=")) != NULL) { + conn->post->multipartBoundary = b + 7; // move the pointer 2 chars before boundary then fill them with dashes + conn->post->multipartBoundary[0] = '-'; + conn->post->multipartBoundary[1] = '-'; + http_dbg("boundary = %s", conn->post->multipartBoundary); + } + } + } else if (strstarts(h, "Access-Control-Request-Headers: ")) { + // CORS crap that needs to be repeated in the response + + http_info("CORS preflight request."); + + strncpy(conn->priv->corsToken, h + strlen("Access-Control-Request-Headers: "), HTTPD_MAX_CORS_TOKEN_LEN); + } +} + +//Make a connection 'live' so we can do all the things a cgi can do to it. +//ToDo: Also make httpdRecvCb/httpdContinue use these? +//ToDo: Fail if malloc fails? +void httpdConnSendStart(HttpdConnData *conn) +{ + httpdPlatLock(); + char *sendBuff = httpdPlatMalloc(HTTPD_MAX_SENDBUFF_LEN); + if (sendBuff == NULL) { + http_error("Malloc sendBuff failed!"); + return; + } + conn->priv->sendBuff = sendBuff; + conn->priv->sendBuffLen = 0; +} + +//Finish the live-ness of a connection. Always call this after httpdConnStart +void httpdConnSendFinish(HttpdConnData *conn) +{ + if (conn->conn) { httpdFlushSendBuffer(conn); } + httpdPlatFree(conn->priv->sendBuff); + httpdPlatUnlock(); +} + +//Callback called when there's data available on a socket. +void httpdRecvCb(ConnTypePtr rconn, const char *remIp, int remPort, char *data, unsigned short len) +{ + int x, r; + char *p, *e; + httpdPlatLock(); + + HttpdConnData *conn = httpdFindConnData(rconn, remIp, remPort); + if (conn == NULL) { + httpdPlatUnlock(); + return; + } + + char *sendBuff = httpdPlatMalloc(HTTPD_MAX_SENDBUFF_LEN); + if (sendBuff == NULL) { + http_error("Malloc sendBuff failed!"); + httpdPlatUnlock(); + return; + } + + conn->priv->sendBuff = sendBuff; + conn->priv->sendBuffLen = 0; + conn->priv->corsToken[0] = 0; + + //This is slightly evil/dirty: we abuse conn->post->len as a state variable for where in the http communications we are: + //<0 (-1): Post len unknown because we're still receiving headers + //==0: No post data + //>0: Need to receive post data + //ToDo: See if we can use something more elegant for this. + + for (x = 0; x < len; x++) { + if (conn->post->len < 0) { + //This byte is a header byte. + if (data[x] == '\n') { + //Compatibility with clients that send \n only: fake a \r in front of this. + if (conn->priv->headPos != 0 && conn->priv->head[conn->priv->headPos - 1] != '\r') { + conn->priv->head[conn->priv->headPos++] = '\r'; + } + } + //ToDo: return http error code 431 (request header too long) if this happens + if (conn->priv->headPos != HTTPD_MAX_HEAD_LEN) { conn->priv->head[conn->priv->headPos++] = data[x]; } + conn->priv->head[conn->priv->headPos] = 0; + //Scan for /r/n/r/n. Receiving this indicate the headers end. + if (data[x] == '\n' && strstr(conn->priv->head, "\r\n\r\n") != NULL) { + //Indicate we're done with the headers. + conn->post->len = 0; + //Reset url data + conn->url = NULL; + //Iterate over all received headers and parse them. + p = conn->priv->head; + while (p < (&conn->priv->head[conn->priv->headPos - 4])) { + e = strstr(p, "\r\n"); //Find end of header line + if (e == NULL) { break; } //Shouldn't happen. + e[0] = 0; //Zero-terminate header + httpdParseHeader(p, conn); //and parse it. + p = e + 2; //Skip /r/n (now /0/n) + } + //If we don't need to receive post data, we can send the response now. + if (conn->post->len == 0) { + httpdProcessRequest(conn); + } + } + } else if (conn->post->len != 0) { + //This byte is a POST byte. + conn->post->buff[conn->post->buffLen++] = data[x]; + conn->post->received++; + conn->hostName = NULL; + if (conn->post->buffLen >= conn->post->buffSize || conn->post->received == conn->post->len) { + //Received a chunk of post data + conn->post->buff[conn->post->buffLen] = 0; //zero-terminate, in case the cgi handler knows it can use strings + //Process the data + if (conn->cgi) { + r = conn->cgi(conn); + if (r == HTTPD_CGI_DONE) { + httpdCgiIsDone(conn); + } + } else { + //No CGI fn set yet: probably first call. Allow httpdProcessRequest to choose CGI and + //call it the first time. + httpdProcessRequest(conn); + } + conn->post->buffLen = 0; + conn->post->len = 0; // this causes transfer to the recvHdl branch + } + } else { + //Let cgi handle data if it registered a recvHdl callback. If not, ignore. + if (conn->recvHdl) { + r = conn->recvHdl(conn, data + x, len - x); + if (r == HTTPD_CGI_DONE) { + http_dbg("Recvhdl returned DONE"); + httpdCgiIsDone(conn); + //We assume the recvhdlr has sent something; we'll kill the sock in the sent callback. + } + break; //ignore rest of data, recvhdl has parsed it. + } else { + http_warn("Eh? Got unexpected data from client. %s", data); + } + } + } + if (conn->conn) { httpdFlushSendBuffer(conn); } + httpdPlatFree(sendBuff); + httpdPlatUnlock(); +} + +//The platform layer should ALWAYS call this function, regardless if the connection is closed by the server +//or by the client. +void httpdDisconCb(ConnTypePtr rconn, const char *remIp, int remPort) +{ + httpdPlatLock(); + HttpdConnData *hconn = httpdFindConnData(rconn, remIp, remPort); + if (hconn == NULL) { + httpdPlatUnlock(); + return; + } + http_info("Pool slot %d: socket closed.", hconn->slot); + hconn->conn = NULL; //indicate cgi the connection is gone + if (hconn->cgi) { hconn->cgi(hconn); } //Execute cgi fn if needed + httpdRetireConn(hconn); + httpdPlatUnlock(); +} + + +int httpdConnectCb(ConnTypePtr conn, const char *remIp, int remPort) +{ + int i; + httpdPlatLock(); + //Find empty conndata in pool + for (i = 0; i < HTTPD_MAX_CONNECTIONS; i++) { if (s_connData[i] == NULL) { break; }} + http_info("Conn req from %d.%d.%d.%d:%d, using pool slot %d", remIp[0] & 0xff, remIp[1] & 0xff, remIp[2] & 0xff, remIp[3] & 0xff, remPort, i); + if (i == HTTPD_MAX_CONNECTIONS) { + http_error("Aiee, conn pool overflow!"); + httpdPlatUnlock(); + return 0; + } + s_connData[i] = httpdPlatMalloc(sizeof(HttpdConnData)); + if (s_connData[i] == NULL) { + http_warn("Out of memory allocating connData!"); + httpdPlatUnlock(); + return 0; + } + memset(s_connData[i], 0, sizeof(HttpdConnData)); + s_connData[i]->priv = httpdPlatMalloc(sizeof(HttpdPriv)); + memset(s_connData[i]->priv, 0, sizeof(HttpdPriv)); + s_connData[i]->conn = conn; + s_connData[i]->slot = i; + s_connData[i]->priv->headPos = 0; + s_connData[i]->post = httpdPlatMalloc(sizeof(HttpdPostData)); + if (s_connData[i]->post == NULL) { + http_error("Out of memory allocating connData post struct!"); + httpdPlatUnlock(); + return 0; + } + memset(s_connData[i]->post, 0, sizeof(HttpdPostData)); + s_connData[i]->post->buff = NULL; + s_connData[i]->post->buffLen = 0; + s_connData[i]->post->received = 0; + s_connData[i]->post->len = -1; + s_connData[i]->hostName = NULL; + s_connData[i]->remote_port = remPort; + s_connData[i]->priv->sendBacklog = NULL; + s_connData[i]->priv->sendBacklogSize = 0; + memcpy(s_connData[i]->remote_ip, remIp, 4); + + httpdPlatUnlock(); + return 1; +} + +//Httpd initialization routine. Call this to kick off webserver functionality. +httpd_thread_handle_t *httpdInit(const HttpdBuiltInUrl *fixedUrls, struct httpd_options *options) +{ + int i; + + for (i = 0; i < HTTPD_MAX_CONNECTIONS; i++) { + s_connData[i] = NULL; + } + builtInUrls = fixedUrls; + + httpdPlatInit(); + + http_info("Httpd init"); + + return httpdPlatStart(options); +} + +void httpdJoin(httpd_thread_handle_t * handle) +{ + httpdPlatJoin(handle); +} + +/** + * Set server name (must be constant / strdup) + * @param name + */ +void httpdSetName(const char *name) +{ + serverName = name; +} diff --git a/lib/src/httpdespfs.c b/lib/src/httpdespfs.c new file mode 100644 index 0000000..5582b8b --- /dev/null +++ b/lib/src/httpdespfs.c @@ -0,0 +1,402 @@ +/* +Connector to let httpd use the espfs filesystem to serve the files in it. +*/ + +/* + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * Jeroen Domburg wrote this file. As long as you retain + * this notice you can do whatever you want with this stuff. If we meet some day, + * and you think this stuff is worth it, you can buy me a beer in return. + * ---------------------------------------------------------------------------- + */ + +#include +#include +#include "httpd.h" +#include "httpd-platform.h" +#include "httpdespfs.h" +#include "espfs.h" +#include "espfsformat.h" +#include "logging.h" + +#define FILE_CHUNK_LEN 1024 + +// The static files marked with FLAG_GZIP are compressed and will be served with GZIP compression. +// If the client does not advertise that he accepts GZIP send following warning message (telnet users for e.g.) +static const char *gzipNonSupportedMessage = "HTTP/1.0 501 Not implemented\r\n" + "Connection: close\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 52\r\n" + "\r\n" + "Your browser does not accept gzip-compressed data.\r\n"; + +/** + * Try to open a file + * @param path - path to the file, may end with slash + * @param indexname - filename at the path + * @return file pointer or NULL + */ +static EspFsFile *tryOpenIndex_do(const char *path, const char *indexname) +{ + char fname[100]; + size_t url_len = strlen(path); + strncpy(fname, path, 99); + + // Append slash if missing + if (path[url_len - 1] != '/') { + fname[url_len++] = '/'; + } + + strcpy(fname + url_len, indexname); + + // Try to open, returns NULL if failed + return espFsOpen(fname); +} + +/** + * Try to find index file on a path + * @param path - directory + * @return file pointer or NULL + */ +EspFsFile *tryOpenIndex(const char *path) +{ + EspFsFile *file; + // A dot in the filename probably means extension + // no point in trying to look for index. + if (strchr(path, '.') != NULL) { return NULL; } + + file = tryOpenIndex_do(path, "index.html"); + if (file != NULL) { return file; } + + file = tryOpenIndex_do(path, "index.htm"); + if (file != NULL) { return file; } + + file = tryOpenIndex_do(path, "index.tpl.html"); + if (file != NULL) { return file; } + + file = tryOpenIndex_do(path, "index.tpl"); + if (file != NULL) { return file; } + + return NULL; // failed to guess the right name +} + +httpd_cgi_state +serveStaticFile(HttpdConnData *connData, const char *filepath) +{ + EspFsFile *file = connData->cgiData; + int len; + char buff[FILE_CHUNK_LEN + 1]; + char acceptEncodingBuffer[64 + 1]; + int isGzip; + + if (connData->conn == NULL) { + //Connection aborted. Clean up. + espFsClose(file); + return HTTPD_CGI_DONE; + } + + // invalid call. + if (filepath == NULL) { + espfs_error("serveStaticFile called with NULL path!"); + return HTTPD_CGI_NOTFOUND; + } + + //First call to this cgi. + if (file == NULL) { + //First call to this cgi. Open the file so we can read it. + file = espFsOpen(filepath); + if (file == NULL) { + // file not found + + // If this is a folder, look for index file + file = tryOpenIndex(filepath); + if (file == NULL) { return HTTPD_CGI_NOTFOUND; } + } + + // The gzip checking code is intentionally without #ifdefs because checking + // for FLAG_GZIP (which indicates gzip compressed file) is very easy, doesn't + // mean additional overhead and is actually safer to be on at all times. + // If there are no gzipped files in the image, the code bellow will not cause any harm. + + // Check if requested file was GZIP compressed + isGzip = espFsFlags(file) & FLAG_GZIP; + if (isGzip) { + // Check the browser's "Accept-Encoding" header. If the client does not + // advertise that he accepts GZIP send a warning message (telnet users for e.g.) + httpdGetHeader(connData, "Accept-Encoding", acceptEncodingBuffer, 64); + if (strstr(acceptEncodingBuffer, "gzip") == NULL) { + //No Accept-Encoding: gzip header present + httpdSend(connData, gzipNonSupportedMessage, -1); + espFsClose(file); + return HTTPD_CGI_DONE; + } + } + + connData->cgiData = file; + httpdStartResponse(connData, 200); + const char *mime = httpdGetMimetype(filepath); + httpdHeader(connData, "Content-Type", mime); + if (isGzip) { + httpdHeader(connData, "Content-Encoding", "gzip"); + } + httpdAddCacheHeaders(connData, mime); + httpdEndHeaders(connData); + return HTTPD_CGI_MORE; + } + + len = espFsRead(file, buff, FILE_CHUNK_LEN); + if (len > 0) { httpdSend(connData, buff, len); } + if (len != FILE_CHUNK_LEN) { + //We're done. + espFsClose(file); + return HTTPD_CGI_DONE; + } else { + //Ok, till next time. + return HTTPD_CGI_MORE; + } +} + + +//This is a catch-all cgi function. It takes the url passed to it, looks up the corresponding +//path in the filesystem and if it exists, passes the file through. This simulates what a normal +//webserver would do with static files. +httpd_cgi_state cgiEspFsHook(HttpdConnData *connData) +{ + const char *filepath = (connData->cgiArg == NULL) ? connData->url : (char *) connData->cgiArg; + return serveStaticFile(connData, filepath); +} + + +//cgiEspFsTemplate can be used as a template. + +typedef enum { + ENCODE_PLAIN = 0, + ENCODE_HTML, + ENCODE_JS, +} TplEncode; + +typedef struct { + EspFsFile *file; + void *tplArg; + char token[64]; + int tokenPos; + + char buff[FILE_CHUNK_LEN + 1]; + + bool chunk_resume; + int buff_len; + int buff_x; + int buff_sp; + char *buff_e; + TplEncode tokEncode; +} TplData; + + +int +tplSend(HttpdConnData *conn, const char *str, int len) +{ + if (conn == NULL) { return 0; } + TplData *tpd = conn->cgiData; + + if (tpd == NULL || tpd->tokEncode == ENCODE_PLAIN) { return httpdSend(conn, str, len); } + if (tpd->tokEncode == ENCODE_HTML) { return httpdSend_html(conn, str, len); } + if (tpd->tokEncode == ENCODE_JS) { return httpdSend_js(conn, str, len); } + return 0; +} + + +httpd_cgi_state cgiEspFsTemplate(HttpdConnData *connData) +{ + TplData *tpd = connData->cgiData; + int len; + int x, sp = 0; + char *e = NULL; + int tokOfs; + + if (connData->conn == NULL) { + //Connection aborted. Clean up. + ((TplCallback) (connData->cgiArg))(connData, NULL, &tpd->tplArg); + espFsClose(tpd->file); + httpdPlatFree(tpd); + return HTTPD_CGI_DONE; + } + + if (tpd == NULL) { + //First call to this cgi. Open the file so we can read it. + tpd = (TplData *) httpdPlatMalloc(sizeof(TplData)); + if (tpd == NULL) { + espfs_error("Failed to malloc tpl struct"); + return HTTPD_CGI_NOTFOUND; + } + + tpd->chunk_resume = false; + + const char *filepath = connData->url; + // check for custom template URL + if (connData->cgiArg2 != NULL) { + filepath = connData->cgiArg2; + espfs_dbg("Using filepath %s", filepath); + } + + tpd->file = espFsOpen(filepath); + + if (tpd->file == NULL) { + // maybe a folder, look for index file + tpd->file = tryOpenIndex(filepath); + if (tpd->file == NULL) { + httpdPlatFree(tpd); + return HTTPD_CGI_NOTFOUND; + } + } + + tpd->tplArg = NULL; + tpd->tokenPos = -1; + if (espFsFlags(tpd->file) & FLAG_GZIP) { + espfs_error("cgiEspFsTemplate: Trying to use gzip-compressed file %s as template!", connData->url); + espFsClose(tpd->file); + httpdPlatFree(tpd); + return HTTPD_CGI_NOTFOUND; + } + connData->cgiData = tpd; + httpdStartResponse(connData, 200); + const char *mime = httpdGetMimetype(connData->url); + httpdHeader(connData, "Content-Type", mime); + httpdAddCacheHeaders(connData, mime); + httpdEndHeaders(connData); + return HTTPD_CGI_MORE; + } + + char *buff = tpd->buff; + + // resume the parser state from the last token, + // if subst. func wants more data to be sent. + if (tpd->chunk_resume) { + //espfs_dbg("Resuming tpl parser for multi-part subst"); + len = tpd->buff_len; + e = tpd->buff_e; + sp = tpd->buff_sp; + x = tpd->buff_x; + } else { + len = espFsRead(tpd->file, buff, FILE_CHUNK_LEN); + tpd->buff_len = len; + + e = buff; + sp = 0; + x = 0; + } + + if (len > 0) { + for (; x < len; x++) { + if (tpd->tokenPos == -1) { + //Inside ordinary text. + if (buff[x] == '%') { + //Send raw data up to now + if (sp != 0) { httpdSend(connData, e, sp); } + sp = 0; + //Go collect token chars. + tpd->tokenPos = 0; + } else { + sp++; + } + } else { + if (buff[x] == '%') { + if (tpd->tokenPos == 0) { + //This is the second % of a %% escape string. + //Send a single % and resume with the normal program flow. + httpdSend(connData, "%", 1); + } else { + if (!tpd->chunk_resume) { + //This is an actual token. + tpd->token[tpd->tokenPos++] = 0; //zero-terminate token + + tokOfs = 0; + tpd->tokEncode = ENCODE_PLAIN; + if (strneq(tpd->token, "html:", 5)) { + tokOfs = 5; + tpd->tokEncode = ENCODE_HTML; + } else if (strneq(tpd->token, "h:", 2)) { + tokOfs = 2; + tpd->tokEncode = ENCODE_HTML; + } else if (strneq(tpd->token, "js:", 3)) { + tokOfs = 3; + tpd->tokEncode = ENCODE_JS; + } else if (strneq(tpd->token, "j:", 2)) { + tokOfs = 2; + tpd->tokEncode = ENCODE_JS; + } + + // do the shifting + if (tokOfs > 0) { + for (int i = tokOfs; i <= tpd->tokenPos; i++) { + tpd->token[i - tokOfs] = tpd->token[i]; + } + } + } + + tpd->chunk_resume = false; + + httpd_cgi_state status = ((TplCallback) (connData->cgiArg))(connData, tpd->token, &tpd->tplArg); + if (status == HTTPD_CGI_MORE) { +// espfs_dbg("Multi-part tpl subst, saving parser state"); + // wants to send more in this token's place..... + tpd->chunk_resume = true; + tpd->buff_len = len; + tpd->buff_e = e; + tpd->buff_sp = sp; + tpd->buff_x = x; + break; + } + } + //Go collect normal chars again. + e = &buff[x + 1]; + tpd->tokenPos = -1; + } else { + // Add char to the token buf + char c = buff[x]; + bool outOfSpace = tpd->tokenPos >= (sizeof(tpd->token) - 1); + if (outOfSpace || + (!(c >= 'a' && c <= 'z') && + !(c >= 'A' && c <= 'Z') && + !(c >= '0' && c <= '9') && + c != '.' && c != '_' && c != '-' && c != ':' + )) { + // looks like we collected some garbage + httpdSend(connData, "%", 1); + if (tpd->tokenPos > 0) { + httpdSend(connData, tpd->token, tpd->tokenPos); + } + // the bad char + httpdSend(connData, &c, 1); + + //Go collect normal chars again. + e = &buff[x + 1]; + tpd->tokenPos = -1; + } else { + // collect it + tpd->token[tpd->tokenPos++] = c; + } + } + } + } + } + + if (tpd->chunk_resume) { + return HTTPD_CGI_MORE; + } + + //Send remaining bit. + if (sp != 0) { httpdSend(connData, e, sp); } + if (len != FILE_CHUNK_LEN) { + //We're done. + ((TplCallback) (connData->cgiArg))(connData, NULL, &tpd->tplArg); + espfs_info("Template sent."); + espFsClose(tpd->file); + httpdPlatFree(tpd); + return HTTPD_CGI_DONE; + } else { + //Ok, till next time. + return HTTPD_CGI_MORE; + } +} + diff --git a/lib/src/port/httpd-freertos.c b/lib/src/port/httpd-freertos.c new file mode 100644 index 0000000..55bae32 --- /dev/null +++ b/lib/src/port/httpd-freertos.c @@ -0,0 +1,56 @@ +#include "httpd.h" +#include "httpd-platform.h" + +#include "FreeRTOS/FreeRTOS.h" + +#define HTTPD_STACKSIZE 4096 // TODO + +static xQueueHandle httpdMux; + +//Set/clear global httpd lock. +void httpdPlatLock() +{ + xSemaphoreTakeRecursive(httpdMux, portMAX_DELAY); +} + +void httpdPlatUnlock() +{ + xSemaphoreGiveRecursive(httpdMux); +} + +void httpdPlatDelayMs(uint32_t ms) +{ + vTaskDelay(1000 / portTICK_RATE_MS); +} + +void httpdPlatTaskEnd() +{ + vTaskDelete(NULL); +} + +void httpdPlatDisableTimeout(ConnTypePtr conn) +{ + //Unimplemented +} + +//Initialize listening socket, do general initialization +void httpdPlatStart(struct httpd_options *opts) +{ + httpMaxConnCt = maxConnCt; + httpdMux = xSemaphoreCreateRecursiveMutex(); // TODO verify arguments + xTaskCreate(platHttpServerTask, (const char *) "httpd", HTTPD_STACKSIZE, opts, 4, NULL); +} + +void* httpdPlatMalloc(size_t len) +{ + return malloc(len); +} + +void httpdPlatFree(void *ptr) +{ + free(ptr); +} + +char * httpdPlatStrdup(const char *s) { + return strdup(s); // FIXME +} diff --git a/lib/src/port/httpd-posix.c b/lib/src/port/httpd-posix.c new file mode 100644 index 0000000..1c15d9e --- /dev/null +++ b/lib/src/port/httpd-posix.c @@ -0,0 +1,87 @@ +#include "httpd.h" +#include "httpd-platform.h" +#include +#include +#include +#include + +#define HTTPD_STACKSIZE 4096 // TODO + +static pthread_mutex_t Mutex; +static pthread_mutexattr_t MutexAttr; + +//Set/clear global httpd lock. +void httpdPlatLock() +{ + pthread_mutex_lock(&Mutex); +} + +void httpdPlatUnlock() +{ + pthread_mutex_unlock(&Mutex); +} + +void httpdPlatDelayMs(uint32_t ms) +{ + usleep(ms * 1000); +} + +void httpdPlatTaskEnd() +{ + // TODO +} + +void httpdPlatDisableTimeout(ConnTypePtr conn) +{ + //Unimplemented +} + +void httpdPlatInit() { + pthread_mutexattr_init(&MutexAttr); + pthread_mutexattr_settype(&MutexAttr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&Mutex, &MutexAttr); +} + +struct httpd_thread_handle { + pthread_t handle; + // TODO some way to signal shutdown? +}; + +void* platHttpServerTaskPosix(void *pvParameters) +{ + platHttpServerTask(pvParameters); + return NULL; +} + +//Initialize listening socket, do general initialization +httpd_thread_handle_t *httpdPlatStart(struct httpd_options *opts) +{ + struct httpd_thread_handle* handle = httpdPlatMalloc(sizeof(struct httpd_thread_handle)); + if (!handle) { + return NULL; + } + + pthread_create( &handle->handle, NULL, platHttpServerTaskPosix, (void*) opts); + return handle; +} + +void httpdPlatJoin(httpd_thread_handle_t * handle) +{ + if (handle) { + pthread_join(handle->handle, NULL); + } +} + +void* httpdPlatMalloc(size_t len) +{ + return malloc(len); +} + +void httpdPlatFree(void *ptr) +{ + free(ptr); +} + +char * httpdPlatStrdup(const char *s) { + return strdup(s); +} diff --git a/lib/src/utils/base64.c b/lib/src/utils/base64.c new file mode 100644 index 0000000..2b8aba0 --- /dev/null +++ b/lib/src/utils/base64.c @@ -0,0 +1,112 @@ +/* base64.c : base-64 / MIME encode/decode */ +/* PUBLIC DOMAIN - Jon Mayo - November 13, 2003 */ +#include "base64.h" +#include +#include + +static const int base64dec_tab[256] = { + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255, + 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, + 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +}; + +#if 0 +static int base64decode(const char in[4], char out[3]) { + uint8_t v[4]; + + v[0]=base64dec_tab[(unsigned)in[0]]; + v[1]=base64dec_tab[(unsigned)in[1]]; + v[2]=base64dec_tab[(unsigned)in[2]]; + v[3]=base64dec_tab[(unsigned)in[3]]; + + out[0]=(v[0]<<2)|(v[1]>>4); + out[1]=(v[1]<<4)|(v[2]>>2); + out[2]=(v[2]<<6)|(v[3]); + return (v[0]|v[1]|v[2]|v[3])!=255 ? in[3]=='=' ? in[2]=='=' ? 1 : 2 : 3 : 0; +} +#endif + +/* decode a base64 string in one shot */ +int __attribute__((weak)) base64_decode(size_t in_len, const char *in, size_t out_len, unsigned char *out) +{ + unsigned int ii, io; + uint32_t v; + unsigned int rem; + + for (io = 0, ii = 0, v = 0, rem = 0; ii < in_len; ii++) { + unsigned char ch; + if (isspace((int) in[ii])) { continue; } + if (in[ii] == '=') { break; } /* stop at = */ + ch = base64dec_tab[(unsigned int) in[ii]]; + if (ch == 255) { break; } /* stop at a parse error */ + v = (v << 6) | ch; + rem += 6; + if (rem >= 8) { + rem -= 8; + if (io >= out_len) return -1; /* truncation is failure */ + out[io++] = (v >> rem) & 255; + } + } + if (rem >= 8) { + rem -= 8; + if (io >= out_len) { return -1; } /* truncation is failure */ + out[io++] = (v >> rem) & 255; + } + return (int) io; +} + +static const uint8_t base64enc_tab[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +#if 0 +void base64encode(const unsigned char in[3], unsigned char out[4], int count) { + out[0]=base64enc_tab[(in[0]>>2)]; + out[1]=base64enc_tab[((in[0]&3)<<4)|(in[1]>>4)]; + out[2]=count<2 ? '=' : base64enc_tab[((in[1]&15)<<2)|(in[2]>>6)]; + out[3]=count<3 ? '=' : base64enc_tab[(in[2]&63)]; +} +#endif + +int __attribute__((weak)) base64_encode(size_t in_len, const unsigned char *in, size_t out_len, char *out) +{ + unsigned ii, io; + uint32_t v; + unsigned rem; + + for (io = 0, ii = 0, v = 0, rem = 0; ii < in_len; ii++) { + unsigned char ch; + ch = in[ii]; + v = (v << 8) | ch; + rem += 8; + while (rem >= 6) { + rem -= 6; + if (io >= out_len) { return -1; } /* truncation is failure */ + out[io++] = (char) base64enc_tab[(v >> rem) & 63]; + } + } + if (rem) { + v <<= (6 - rem); + if (io >= out_len) { return -1; } /* truncation is failure */ + out[io++] = (char) base64enc_tab[v & 63]; + } + while (io & 3) { + if (io >= out_len) { return -1; } /* truncation is failure */ + out[io++] = '='; + } + if (io >= out_len) { return -1; } /* no room for null terminator */ + out[io] = 0; + return (int) io; +} + diff --git a/lib/src/utils/base64.h b/lib/src/utils/base64.h new file mode 100644 index 0000000..bd862a4 --- /dev/null +++ b/lib/src/utils/base64.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +int base64_decode(size_t in_len, const char *in, size_t out_len, unsigned char *out); +int base64_encode(size_t in_len, const unsigned char *in, size_t out_len, char *out); diff --git a/lib/src/utils/sha1.c b/lib/src/utils/sha1.c new file mode 100644 index 0000000..bf75428 --- /dev/null +++ b/lib/src/utils/sha1.c @@ -0,0 +1,174 @@ +/* This code is public-domain - it is based on libcrypt + * placed in the public domain by Wei Dai and other contributors. + */ +// gcc -Wall -DSHA1TEST -o sha1test sha1.c && ./sha1test + +#include +#include +#include "sha1.h" + +//according to http://ip.cadence.com/uploads/pdf/xtensalx_overview_handbook.pdf +// the cpu is normally defined as little ending, but can be big endian too. +// for the esp this seems to work +//#define SHA_BIG_ENDIAN + +/* code */ +#define SHA1_K0 0x5a827999 +#define SHA1_K20 0x6ed9eba1 +#define SHA1_K40 0x8f1bbcdc +#define SHA1_K60 0xca62c1d6 + +void sha1_init(sha1nfo *s) +{ + s->state[0] = 0x67452301; + s->state[1] = 0xefcdab89; + s->state[2] = 0x98badcfe; + s->state[3] = 0x10325476; + s->state[4] = 0xc3d2e1f0; + s->byteCount = 0; + s->bufferOffset = 0; +} + +uint32_t sha1_rol32(uint32_t number, uint8_t bits) +{ + return ((number << bits) | (number >> (32 - bits))); +} + +void sha1_hashBlock(sha1nfo *s) +{ + uint8_t i; + uint32_t a, b, c, d, e, t; + + a = s->state[0]; + b = s->state[1]; + c = s->state[2]; + d = s->state[3]; + e = s->state[4]; + for (i = 0; i < 80; i++) { + if (i >= 16) { + t = s->buffer[(i + 13) & 15] ^ s->buffer[(i + 8) & 15] ^ s->buffer[(i + 2) & 15] ^ s->buffer[i & 15]; + s->buffer[i & 15] = sha1_rol32(t, 1); + } + if (i < 20) { + t = (d ^ (b & (c ^ d))) + SHA1_K0; + } else if (i < 40) { + t = (b ^ c ^ d) + SHA1_K20; + } else if (i < 60) { + t = ((b & c) | (d & (b | c))) + SHA1_K40; + } else { + t = (b ^ c ^ d) + SHA1_K60; + } + t += sha1_rol32(a, 5) + e + s->buffer[i & 15]; + e = d; + d = c; + c = sha1_rol32(b, 30); + b = a; + a = t; + } + s->state[0] += a; + s->state[1] += b; + s->state[2] += c; + s->state[3] += d; + s->state[4] += e; +} + +void sha1_addUncounted(sha1nfo *s, uint8_t data) +{ + uint8_t *const b = (uint8_t *) s->buffer; +#ifdef SHA_BIG_ENDIAN + b[s->bufferOffset] = data; +#else + b[s->bufferOffset ^ 3] = data; +#endif + s->bufferOffset++; + if (s->bufferOffset == BLOCK_LENGTH) { + sha1_hashBlock(s); + s->bufferOffset = 0; + } +} + +void sha1_writebyte(sha1nfo *s, uint8_t data) +{ + ++s->byteCount; + sha1_addUncounted(s, data); +} + +void sha1_write(sha1nfo *s, const char *data, size_t len) +{ + for (; len--;) { sha1_writebyte(s, (uint8_t) *data++); } +} + +void sha1_pad(sha1nfo *s) +{ + // Implement SHA-1 padding (fips180-2 §5.1.1) + + // Pad with 0x80 followed by 0x00 until the end of the block + sha1_addUncounted(s, 0x80); + while (s->bufferOffset != 56) { sha1_addUncounted(s, 0x00); } + + // Append length in the last 8 bytes + sha1_addUncounted(s, 0); // We're only using 32 bit lengths + sha1_addUncounted(s, 0); // But SHA-1 supports 64 bit lengths + sha1_addUncounted(s, 0); // So zero pad the top bits + sha1_addUncounted(s, s->byteCount >> 29); // Shifting to multiply by 8 + sha1_addUncounted(s, s->byteCount >> 21); // as SHA-1 supports bitstreams as well as + sha1_addUncounted(s, s->byteCount >> 13); // byte. + sha1_addUncounted(s, s->byteCount >> 5); + sha1_addUncounted(s, s->byteCount << 3); +} + +uint8_t *sha1_result(sha1nfo *s) +{ + // Pad to complete the last block + sha1_pad(s); + +#ifndef SHA_BIG_ENDIAN + // Swap byte order back + int i; + for (i = 0; i < 5; i++) { + s->state[i] = + (((s->state[i]) << 24) & 0xff000000) + | (((s->state[i]) << 8) & 0x00ff0000) + | (((s->state[i]) >> 8) & 0x0000ff00) + | (((s->state[i]) >> 24) & 0x000000ff); + } +#endif + + // Return pointer to hash (20 characters) + return (uint8_t *) s->state; +} + +#define HMAC_IPAD 0x36 +#define HMAC_OPAD 0x5c + +void sha1_initHmac(sha1nfo *s, const uint8_t *key, int keyLength) +{ + uint8_t i; + memset(s->keyBuffer, 0, BLOCK_LENGTH); + if (keyLength > BLOCK_LENGTH) { + // Hash long keys + sha1_init(s); + for (; keyLength--;) { sha1_writebyte(s, *key++); } + memcpy(s->keyBuffer, sha1_result(s), HASH_LENGTH); + } else { + // Block length keys are used as is + memcpy(s->keyBuffer, key, keyLength); + } + // Start inner hash + sha1_init(s); + for (i = 0; i < BLOCK_LENGTH; i++) { + sha1_writebyte(s, s->keyBuffer[i] ^ HMAC_IPAD); + } +} + +uint8_t *sha1_resultHmac(sha1nfo *s) +{ + uint8_t i; + // Complete inner hash + memcpy(s->innerHash, sha1_result(s), HASH_LENGTH); + // Calculate outer hash + sha1_init(s); + for (i = 0; i < BLOCK_LENGTH; i++) { sha1_writebyte(s, s->keyBuffer[i] ^ HMAC_OPAD); } + for (i = 0; i < HASH_LENGTH; i++) { sha1_writebyte(s, s->innerHash[i]); } + return sha1_result(s); +} diff --git a/lib/src/utils/sha1.h b/lib/src/utils/sha1.h new file mode 100644 index 0000000..8a99d49 --- /dev/null +++ b/lib/src/utils/sha1.h @@ -0,0 +1,34 @@ +#pragma once + +#define HASH_LENGTH 20 +#define BLOCK_LENGTH 64 + +typedef struct sha1nfo { + uint32_t buffer[BLOCK_LENGTH/4]; + uint32_t state[HASH_LENGTH/4]; + uint32_t byteCount; + uint8_t bufferOffset; + uint8_t keyBuffer[BLOCK_LENGTH]; + uint8_t innerHash[HASH_LENGTH]; +} sha1nfo; + +/* public API - prototypes - TODO: doxygen*/ + +/** + */ +void sha1_init(sha1nfo *s); +/** + */ +void sha1_writebyte(sha1nfo *s, uint8_t data); +/** + */ +void sha1_write(sha1nfo *s, const char *data, size_t len); +/** + */ +uint8_t* sha1_result(sha1nfo *s); +/** + */ +void sha1_initHmac(sha1nfo *s, const uint8_t* key, int keyLength); +/** + */ +uint8_t* sha1_resultHmac(sha1nfo *s); diff --git a/lib/todo/esphttpclient/LICENSE b/lib/todo/esphttpclient/LICENSE new file mode 100644 index 0000000..24a8fc0 --- /dev/null +++ b/lib/todo/esphttpclient/LICENSE @@ -0,0 +1,9 @@ +---------------------------------------------------------------------------- +"THE BEER-WARE LICENSE" (Revision 42): +Martin d'Allens wrote this file. As long as you retain +this notice you can do whatever you want with this stuff. If we meet some day, +and you think this stuff is worth it, you can buy me a beer in return. +---------------------------------------------------------------------------- + +Upstream: https://github.com/Caerbannog/esphttpclient + diff --git a/lib/todo/esphttpclient/README.md b/lib/todo/esphttpclient/README.md new file mode 100644 index 0000000..b49d3f6 --- /dev/null +++ b/lib/todo/esphttpclient/README.md @@ -0,0 +1,78 @@ +# esphttpclient + +This is a short library for ESP8266(EX) chips to make HTTP requests. + +## Features + + * Easy to use. + * Supports multiple requests in parallel. + * Supports GET and POST requests. + * Tested with Espressif SDK v1.0.0 + +## Building +If you don't have a toolchain yet, install one with then get Espressif's SDK. + +### The submodule way +If your project looks like esphttpd from Sprite_tm: +```bash +git clone http://git.spritesserver.nl/esphttpd.git/ +cd esphttpd +git submodule add https://github.com/Caerbannog/esphttpclient.git lib/esphttpclient +git submodule update --init +``` + +Now append `lib/esphttpclient` to the following `Makefile` line and you should be ready: +``` +MODULES = driver user lib/esphttpclient +``` +In case you want to use SSL don't forget to add `ssl` to `LIBS` in the `Makefile` +``` +LIBS = c gcc hal pp phy net80211 lwip wpa main ssl +``` + +### The dirty way +Alternatively you could create a simple project: +```bash +git clone https://github.com/esp8266/source-code-examples.git +cd source-code-examples/basic_example +# Set your Wifi credentials in user_config.h +# I could not test this because of the UART baud rate (74880) +``` + +Then download this library and move the files to `user/`: +```bash +git clone https://github.com/Caerbannog/esphttpclient.git +mv esphttpclient/*.* user/ +``` + +## Usage +Include `httpclient.h` from `user_main.c` then call one of these functions: +```c +void http_get(const char * url, const char * headers, http_callback user_callback); +void http_post(const char * url, const char * post_data, const char * headers, http_callback user_callback); + +void http_callback_example(char * response_body, int http_status, char * response_headers, int body_size) +{ + os_printf("http_status=%d\n", http_status); + if (http_status != HTTP_STATUS_GENERIC_ERROR) { + os_printf("strlen(headers)=%d\n", strlen(response_headers)); + os_printf("body_size=%d\n", body_size); + os_printf("body=%s\n", response_body); + } +} +``` + +## Example +The following code performs a single request, then calls `http_callback_example` to display your public IP address. +```c +http_get("http://wtfismyip.com/text", "", http_callback_example); +``` + +The output looks like this: +``` +http_status=200 +strlen(full_response)=244 +body_size=15 +response_body=208.97.177.124 + +``` diff --git a/lib/todo/esphttpclient/httpclient.c b/lib/todo/esphttpclient/httpclient.c new file mode 100644 index 0000000..8823dc0 --- /dev/null +++ b/lib/todo/esphttpclient/httpclient.c @@ -0,0 +1,589 @@ +/* + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * Martin d'Allens wrote this file. As long as you retain + * this notice you can do whatever you want with this stuff. If we meet some day, + * and you think this stuff is worth it, you can buy me a beer in return. + * ---------------------------------------------------------------------------- + */ + +// FIXME: sprintf->snprintf everywhere. + +#include +#include +#include + +#include "httpclient.h" +#include "esp_utils.h" + +// Internal state. +typedef struct { + char *path; + int port; + char *post_data; + char *headers; + char *hostname; + char *buffer; + int buffer_size; + int max_buffer_size; + bool secure; + httpclient_cb user_callback; + int timeout; + ETSTimer timeout_timer; + httpd_method method; + void *userData; +} request_args; + +static int ICACHE_FLASH_ATTR +chunked_decode(char *chunked, int size) +{ + char *src = chunked; + char *end = chunked + size; + int i, dst = 0; + + do { + char *endstr = NULL; + //[chunk-size] + i = (int) esp_strtol(src, &endstr, 16); + httpc_dbg("Chunk Size:%d\r\n", i); + if (i <= 0) + break; + //[chunk-size-end-ptr] + src = strstr(src, "\r\n") + 2; + //[chunk-data] + memmove(&chunked[dst], src, (size_t) i); + src += i + 2; /* CRLF */ + dst += i; + } while (src < end); + + // + //footer CRLF + // + + /* decoded size */ + return dst; +} + +static void ICACHE_FLASH_ATTR +receive_callback(void *arg, char *buf, unsigned short len) +{ + struct espconn *conn = (struct espconn *) arg; + request_args *req = (request_args *) conn->reserve; + + if (req->buffer == NULL) { + return; + } + + // Let's do the equivalent of a realloc(). + int new_size = req->buffer_size + len; + + if (new_size > req->max_buffer_size) { + system_soft_wdt_feed(); + + httpc_warn("Long resp, truncate (got %d, %d more rx, max %d)", + req->buffer_size, len, req->max_buffer_size); + + int nlen = (req->max_buffer_size - req->buffer_size); + if (nlen <= 0) { + req->buffer[req->buffer_size - 1] = '\0'; + + if (req->secure) { +#ifdef USE_SECURE + espconn_secure_disconnect(conn); +#endif + } + else { + espconn_disconnect(conn); + } + return; + } + len = (unsigned short) nlen; + new_size = req->buffer_size + len; + } + + char *new_buffer; + if (NULL == (new_buffer = (char *) malloc(new_size))) { + httpc_error("Failed to alloc more bytes (%d)", new_size); + req->buffer[0] = '\0'; // Discard the buffer to avoid using an incomplete response. + if (req->secure) { +#ifdef USE_SECURE + espconn_secure_disconnect(conn); +#endif + } + else { + espconn_disconnect(conn); + } + return; // The disconnect callback will be called. + } + + memcpy(new_buffer, req->buffer, req->buffer_size); + memcpy(new_buffer + req->buffer_size - 1 /*overwrite the null character*/, buf, len); // Append new data. + new_buffer[new_size - 1] = '\0'; // Make sure there is an end of string. + + free(req->buffer); + req->buffer = new_buffer; + req->buffer_size = new_size; +} + + +static void ICACHE_FLASH_ATTR +sent_callback(void *arg) +{ + struct espconn *conn = (struct espconn *) arg; + request_args *req = (request_args *) conn->reserve; + + if (req->post_data == NULL) { + httpc_dbg("All sent"); + } + else { + // The headers were sent, now send the contents. + httpc_dbg("Sending request body"); + if (req->secure) { +#ifdef USE_SECURE + espconn_secure_sent(conn, (uint8_t *)req->post_data, strlen(req->post_data)); +#endif + } + else { + espconn_sent(conn, (uint8_t *) req->post_data, (uint16) strlen(req->post_data)); + } + free(req->post_data); + req->post_data = NULL; + } +} + +static void ICACHE_FLASH_ATTR +connect_callback(void *arg) +{ + httpc_info("Connected!"); + struct espconn *conn = (struct espconn *) arg; + request_args *req = (request_args *) conn->reserve; + + espconn_regist_recvcb(conn, receive_callback); + espconn_regist_sentcb(conn, sent_callback); + + char post_headers[32] = ""; + + if (req->post_data != NULL) { // If there is data this is a POST request. + sprintf(post_headers, "Content-Length: %d\r\n", strlen(req->post_data)); + + if (req->method == HTTPD_METHOD_GET) { + req->method = HTTPD_METHOD_POST; + } + } + + const char *method = httpdMethodName(req->method); + + if (req->headers == NULL) { /* Avoid NULL pointer, it may cause exception */ + req->headers = (char *) malloc(sizeof(char)); + req->headers[0] = '\0'; + } + + const size_t buflen = 80 // for literals in tpl string + some margin + + strlen(method) + + strlen(req->path) + + strlen(req->hostname) + + strlen(req->headers) + + strlen(post_headers); + char buf[buflen]; + + int len = sprintf(buf, + "%s %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Connection: close\r\n" + "%s" + "%s" + "\r\n", + method, req->path, req->hostname, req->port, req->headers, post_headers); + + httpc_dbg("Sending request"); + if (req->secure) { +#ifdef USE_SECURE + espconn_secure_sent(conn, (uint8_t *)buf, len); +#endif + } + else { + espconn_sent(conn, (uint8_t *) buf, (uint16) len); + } + + if (req->headers != NULL) { + free(req->headers); + req->headers = NULL; + } +} + +/** + * @brief Free all that could be allocated in a request, including the struct itself. + * @param req : req to free + */ +static void +free_req(request_args *req) +{ + if (!req) return; + + if (req->buffer) free(req->buffer); + if (req->hostname) free(req->hostname); + if (req->path) free(req->path); + if (req->post_data) free(req->post_data); + if (req->headers) free(req->headers); + free(req); +} + +static void ICACHE_FLASH_ATTR +disconnect_callback(void *arg) +{ + httpc_dbg("Disconnected"); + struct espconn *conn = (struct espconn *) arg; + + if (conn == NULL) { + httpc_dbg("conn is null!"); + return; + } + + if (conn->reserve != NULL) { + httpc_dbg("Processing response"); + + request_args *req = (request_args *) conn->reserve; + int http_status = HTTP_STATUS_GENERIC_ERROR; + int body_size = 0; + char *body = ""; + + /* Turn off timeout timer */ + os_timer_disarm(&(req->timeout_timer)); + + if (req->buffer == NULL) { + httpc_error("Buffer shouldn't be NULL"); + } + else if (req->buffer[0] != '\0') { + // FIXME: make sure this is not a partial response, using the Content-Length header. + + const char *version10 = "HTTP/1.0 "; + const char *version11 = "HTTP/1.1 "; + if (!strstarts(req->buffer, version10) && !strstarts(req->buffer, version11)) { + httpc_error("Invalid version in %s", req->buffer); + } + else { + http_status = atoi(req->buffer + strlen(version10)); + /* find body and zero terminate headers */ + char * headend = strstr(req->buffer, "\r\n\r\n"); + if (headend != NULL) { + body = headend + 2; + *body++ = '\0'; + *body++ = '\0'; + + body_size = (int) (req->buffer_size - (body - req->buffer)); + + if (strstr(req->buffer, "Transfer-Encoding: chunked")) { + body_size = chunked_decode(body, body_size); + } + } + } + } + + httpc_info("Request completed."); + if (req->user_callback != NULL) { // Callback is optional. + req->user_callback(http_status, req->buffer, body, body_size, req->userData); + } + + free_req(req); + } else { + httpc_error("Reserve is NULL!"); + } + + espconn_delete(conn); + if (conn->proto.tcp != NULL) { + free(conn->proto.tcp); + } + free(conn); +} + +static void ICACHE_FLASH_ATTR +error_callback(void *arg, sint8 errType) +{ + (void) errType; + + httpc_error("Disconnected with error, type %d", errType); + disconnect_callback(arg); +} + +static void ICACHE_FLASH_ATTR +http_timeout_callback(void *arg) +{ + httpc_error("Connection timeout\n"); + struct espconn *conn = (struct espconn *) arg; + + if (conn == NULL) { + return; + } + + request_args *req = (request_args *) conn->reserve; + if (req) { + /* Call disconnect */ + if (req->secure) { +#ifdef USE_SECURE + espconn_secure_disconnect(conn); +#endif + } + else { + espconn_disconnect(conn); + } + + if (req->user_callback != NULL) { + // fire callback, so user can free the userData + req->user_callback(HTTP_STATUS_TIMEOUT, req->buffer, "", 0, req->userData); + } + free_req(req); + } + + // experimental - better cleanup + if (conn->proto.tcp != NULL) { + free(conn->proto.tcp); + } + free(conn); +} + +static void ICACHE_FLASH_ATTR +dns_callback(const char *hostname, ip_addr_t *addr, void *arg) +{ + (void)hostname; + request_args *req = (request_args *) arg; + + if (addr == NULL) { + httpc_error("DNS failed for %s", hostname); + if (req->user_callback != NULL) { + req->user_callback(HTTP_STATUS_GENERIC_ERROR, "", "", 0, req->userData); + } + free(req); + } + else { + httpc_info("DNS found %s "IPSTR, hostname, IP2STR(addr)); + + struct espconn *conn = (struct espconn *) malloc(sizeof(struct espconn)); + conn->type = ESPCONN_TCP; + conn->state = ESPCONN_NONE; + conn->proto.tcp = (esp_tcp *) malloc(sizeof(esp_tcp)); + conn->proto.tcp->local_port = espconn_port(); + conn->proto.tcp->remote_port = req->port; + conn->reserve = req; + + memcpy(conn->proto.tcp->remote_ip, addr, 4); + + espconn_regist_connectcb(conn, connect_callback); + espconn_regist_disconcb(conn, disconnect_callback); + espconn_regist_reconcb(conn, error_callback); + + /* Set connection timeout timer */ + os_timer_disarm(&(req->timeout_timer)); + os_timer_setfn(&(req->timeout_timer), (os_timer_func_t *) http_timeout_callback, conn); + os_timer_arm(&(req->timeout_timer), req->timeout, false); + + if (req->secure) { +#ifdef USE_SECURE + espconn_secure_set_size(ESPCONN_CLIENT, 5120); // set SSL buffer size + espconn_secure_connect(conn); +#endif + } + else { + espconn_connect(conn); + } + } +} + +bool ICACHE_FLASH_ATTR +http_request(const httpclient_args *args, httpclient_cb user_callback) +{ + // --- prepare port, secure... --- + + // FIXME: handle HTTP auth with http://user:pass@host/ + const char *url = args->url; + + char hostname[128] = ""; + int port = 80; + bool secure = false; + + if (strstarts(url, "http://")) + url += strlen("http://"); // Get rid of the protocol. + else if (strstarts(url, "https://")) { + port = 443; + secure = true; + url += strlen("https://"); // Get rid of the protocol. + } + else { + httpc_error("Invalid URL protocol: %s", url); + return false; + } + + char *path = strchr(url, '/'); + if (path == NULL) { + path = strchr(url, '\0'); // Pointer to end of string. + } + + char *colon = strchr(url, ':'); + if (colon > path) { + colon = NULL; // Limit the search to characters before the path. + } + + if (colon == NULL) { // The port is not present. + memcpy(hostname, url, (size_t) (path - url)); + hostname[path - url] = '\0'; + } + else { + port = atoi(colon + 1); + if (port == 0) { + httpc_error("Port error %s\n", url); + return false; + } + + memcpy(hostname, url, (size_t) (colon - url)); + hostname[colon - url] = '\0'; + } + + if (path[0] == '\0') { // Empty path is not allowed. + path = "/"; + } + + // --- + + httpc_info("HTTP request: %s:%d%s", hostname, port, path); + + request_args *req = (request_args *) malloc(sizeof(request_args)); + req->hostname = esp_strdup(hostname); + req->path = esp_strdup(path); + + // remove #anchor + char *hash = strchr(req->path, '#'); + if (hash != NULL) *hash = '\0'; // remove the hash part + + req->port = port; + req->secure = secure; + req->headers = esp_strdup(args->headers); + req->post_data = esp_strdup(args->body); + req->buffer_size = 1; + req->buffer = (char *) malloc(1); + req->buffer[0] = '\0'; // Empty string. + req->user_callback = user_callback; + req->timeout = HTTPCLIENT_DEF_TIMEOUT_MS; + req->method = args->method; + req->userData = args->userData; + req->max_buffer_size = (int) args->max_response_len; + + ip_addr_t addr; + err_t error = espconn_gethostbyname((struct espconn *) req, // It seems we don't need a real espconn pointer here. + hostname, &addr, dns_callback); + + if (error == ESPCONN_INPROGRESS) { + httpc_dbg("DNS pending"); + } + else if (error == ESPCONN_OK) { + // Already in the local names table (or hostname was an IP address), execute the callback ourselves. + dns_callback(hostname, &addr, req); + } + else { + if (error == ESPCONN_ARG) { + httpc_error("DNS arg error %s", hostname); + } + else { + httpc_error("DNS error code %d", error); + } + dns_callback(hostname, NULL, req); // Handle all DNS errors the same way. + } + + return true; +} + +void httpclient_args_init(httpclient_args *args) +{ + args->url = NULL; + args->body = NULL; + args->method = HTTPD_METHOD_GET; + args->headers = NULL; + args->max_response_len = HTTPCLIENT_DEF_MAX_LEN; + args->timeout = HTTPCLIENT_DEF_TIMEOUT_MS; + args->userData = NULL; +} + + +bool ICACHE_FLASH_ATTR +http_post(const char *url, const char *body, void *userData, httpclient_cb user_callback) +{ + httpclient_args args; + httpclient_args_init(&args); + + args.url = url; + args.body = body; + args.method = HTTPD_METHOD_POST; + args.userData = userData; + + return http_request(&args, user_callback); +} + + +bool ICACHE_FLASH_ATTR +http_get(const char *url, void *userData, httpclient_cb user_callback) +{ + httpclient_args args; + httpclient_args_init(&args); + + args.url = url; + args.method = HTTPD_METHOD_GET; + args.userData = userData; + + return http_request(&args, user_callback); +} + + +bool ICACHE_FLASH_ATTR +http_put(const char *url, const char *body, void *userData, httpclient_cb user_callback) +{ + httpclient_args args; + httpclient_args_init(&args); + + args.url = url; + args.body = body; + args.method = HTTPD_METHOD_PUT; + args.userData = userData; + + return http_request(&args, user_callback); +} + + +void ICACHE_FLASH_ATTR +http_callback_example(int http_status, + char *response_headers, + char *response_body, + size_t body_size, + void *userData) +{ + (void) userData; + dbg("Response: code %d", http_status); + if (http_status != HTTP_STATUS_GENERIC_ERROR) { + dbg("len(headers) = %d, len(body) = %d", (int) strlen(response_headers), body_size); + dbg("body: %s", response_body); // FIXME: this does not handle binary data. + } +} + + +void ICACHE_FLASH_ATTR +http_callback_showstatus(int code, + char *response_headers, + char *response_body, + size_t body_size, + void *userData) +{ + (void) response_body; + (void) response_headers; + (void) body_size; + (void) userData; + + if (code == 200) { + info("Response OK (200)"); + } + else if (code >= 400) { + error("Response ERROR (%d)", code); + dbg("Body: %s", response_body); + } + else { + // ??? + warn("Response %d", code); + dbg("Body: %s", response_body); + } +} diff --git a/lib/todo/httpclient.h b/lib/todo/httpclient.h new file mode 100644 index 0000000..d65544b --- /dev/null +++ b/lib/todo/httpclient.h @@ -0,0 +1,96 @@ +/* + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * Martin d'Allens wrote this file. As long as you retain + * this notice you can do whatever you want with this stuff. If we meet some day, + * and you think this stuff is worth it, you can buy me a beer in return. + * ---------------------------------------------------------------------------- + */ + +#ifndef HTTPCLIENT_H +#define HTTPCLIENT_H + +#include +#include + +#define HTTP_STATUS_GENERIC_ERROR -1 // In case of TCP or DNS error the callback is called with this status. +#define HTTP_STATUS_TIMEOUT -2 + +#define HTTPCLIENT_DEF_MAX_LEN 5000 // Size of http responses that will cause an error. +#define HTTPCLIENT_DEF_TIMEOUT_MS 5000 + +/* Define this if ssl is needed. Also link the ssl lib */ +//#define USE_SECURE + +/** + * "full_response" is a string containing all response headers and the response body. + * "response_body and "http_status" are extracted from "full_response" for convenience. + * + * A successful request corresponds to an HTTP status code of 200 (OK). + * More info at http://en.wikipedia.org/wiki/List_of_HTTP_status_codes + */ +typedef void (* httpclient_cb)(int http_status, + char *response_headers, + char *response_body, + size_t body_size, + void *userData); + +typedef struct { + const char* url; //!< The full URL + const char* headers; //!< Headers string (\r\n separated lines) + const char* body; //!< Request body string + httpd_method method; //!< HTTP method constant + int timeout; //!< Timeout in ms + size_t max_response_len; //!< Max response length to allow + void *userData; //!< Opaque pointer that is passed to the user callback +} httpclient_args; + +/** + * Populate args struct with defaults + * @param args + */ +void httpclient_args_init(httpclient_args *args); + +/** + * @brief Download a web page from its URL. + * + * Try: + * http_get("http://wtfismyip.com/text", NULL, http_callback_example); + */ +bool http_get(const char * url, void *userData, httpclient_cb user_callback); + +/** + * @brief Post data to a web form. + * + * The data should be encoded as application/x-www-form-urlencoded. + * + * Try: + * http_post("http://httpbin.org/post", "first_word=hello&second_word=world", NULL, http_callback_example); + */ +bool http_post(const char *url, const char *post_data, void *userData, httpclient_cb user_callback); + +/** Like POST, but with the PUT method. */ +bool http_put(const char *url, const char *body, void *userData, httpclient_cb user_callback); + +/** + * @brief Send a HTTP request + * @param url : protocol://host[:port][/path] + * @param method : get, post, ... + * @param body : request body. If GET & body != NULL, method changes to POST. + * @param headers : additional headers string. Must end with \r\n + * @param user_callback : callback for parsing the response + * @return success (in sending) + */ +bool http_request(const httpclient_args *args, httpclient_cb user_callback); + +/** + * Output on the UART. + */ +void http_callback_example(int http_status, char *response_headers, char *response_body, size_t body_size, void *userData); + +/** + * Show status code, and body on error. Error/warn log msg on error. + */ +void http_callback_showstatus(int code, char *response_headers, char *response_body, size_t body_size, void *userData); + +#endif diff --git a/lib/todo/uptime.c b/lib/todo/uptime.c new file mode 100755 index 0000000..d145863 --- /dev/null +++ b/lib/todo/uptime.c @@ -0,0 +1,59 @@ +#include + +volatile uint32_t uptime = 0; + +static ETSTimer prUptimeTimer; + +static void uptimeTimerCb(void *arg) +{ + uptime++; +} + +void uptime_timer_init(void) +{ + os_timer_disarm(&prUptimeTimer); + os_timer_setfn(&prUptimeTimer, uptimeTimerCb, NULL); + os_timer_arm(&prUptimeTimer, 1000, 1); +} + +void uptime_str(char *buf) +{ + u32 a = uptime; + u32 days = a / 86400; + a -= days * 86400; + + u32 hours = a / 3600; + a -= hours * 3600; + + u32 mins = a / 60; + a -= mins * 60; + + u32 secs = a; + + if (days > 0) { + sprintf(buf, "%ud %02u:%02u:%02u", days, hours, mins, secs); + } else { + sprintf(buf, "%02u:%02u:%02u", hours, mins, secs); + } +} + +void uptime_print(void) +{ + u32 a = uptime; + u32 days = a / 86400; + a -= days * 86400; + + u32 hours = a / 3600; + a -= hours * 3600; + + u32 mins = a / 60; + a -= mins * 60; + + u32 secs = a; + + if (days > 0) { + printf("%ud %02u:%02u:%02u", days, hours, mins, secs); + } else { + printf("%02u:%02u:%02u", hours, mins, secs); + } +} diff --git a/lib/todo/uptime.h b/lib/todo/uptime.h new file mode 100755 index 0000000..e53bb12 --- /dev/null +++ b/lib/todo/uptime.h @@ -0,0 +1,22 @@ +#ifndef UPTIME_H +#define UPTIME_H + +#include + +extern volatile uint32_t uptime; + +/** + * Initialize the virtual timer for uptime counter. + */ +void uptime_timer_init(void); + +/** + * Print uptime to a buffer in user-friendly format. + * Should be at least 20 bytes long. + */ +void uptime_str(char *buf); + +/** Print uptime to stdout in user-friendly format */ +void uptime_print(void); + +#endif // UPTIME_H diff --git a/lib/todo/webpages-espfs.h b/lib/todo/webpages-espfs.h new file mode 100644 index 0000000..e6889d7 --- /dev/null +++ b/lib/todo/webpages-espfs.h @@ -0,0 +1,3 @@ +extern char webpages_espfs_start[]; +extern char webpages_espfs_end[]; +extern int webpages_espfs_size; diff --git a/lib/todo/webpages.espfs.ld b/lib/todo/webpages.espfs.ld new file mode 100644 index 0000000..0968d4b --- /dev/null +++ b/lib/todo/webpages.espfs.ld @@ -0,0 +1,12 @@ +OUTPUT_FORMAT("elf32-xtensa-le") + + +SECTIONS +{ + .irom0.literal : ALIGN(4) SUBALIGN(4) { + webpages_espfs_start = .; + *(*) + webpages_espfs_end = .; + webpages_espfs_size = webpages_espfs_end - webpages_espfs_start; + } +} \ No newline at end of file diff --git a/main.c b/main.c new file mode 100644 index 0000000..8fa35b7 --- /dev/null +++ b/main.c @@ -0,0 +1,58 @@ +#include +#include "httpd.h" +#include "httpd-utils.h" +#include "httpdespfs.h" + + +#include +#include +#include +#include + +#include "logging.h" + +/** "About" page */ +httpd_cgi_state tplIndex(HttpdConnData *connData, char *token, void **arg) +{ + if (token == NULL) { return HTTPD_CGI_DONE; } + + else if (streq(token, "date")) { + tplSend(connData, __DATE__, -1); + } else if (streq(token, "time")) { + tplSend(connData, __TIME__, -1); + } else if (streq(token, "vers_httpd")) { + tplSend(connData, httpdGetVersion(), -1); + } + + return HTTPD_CGI_DONE; +} + + +/** + * Application routes + */ +const HttpdBuiltInUrl routes[] = { + // TODO password lock ... + + // --- Web pages --- + ROUTE_TPL_FILE("/", tplIndex, "/index.tpl"), + + ROUTE_FILESYSTEM(), + ROUTE_END(), +}; + +int main() +{ + printf("Hello, World!\n"); + + struct httpd_options opts = { + .port = 80, + }; + + httpd_thread_handle_t *handle = httpdInit(routes, &opts); + httpdSetName("ServerName"); + + httpdJoin(handle); + + return 0; +}