From ca4b804479e2cfa5fcf1140a375c4ac2dcb8ed22 Mon Sep 17 00:00:00 2001 From: Peter Ohler Date: Thu, 28 Dec 2017 23:22:57 -0500 Subject: [PATCH] Initial implementation. --- .gitignore | 12 + .travis.yml | 17 + CHANGELOG.md | 5 + Gemfile | 3 + LICENSE | 2 +- README.md | 63 ++- agoo.gemspec | 31 ++ ext/agoo/agoo.c | 19 + ext/agoo/con.c | 515 ++++++++++++++++++++ ext/agoo/con.h | 46 ++ ext/agoo/dtime.c | 52 ++ ext/agoo/dtime.h | 10 + ext/agoo/err.c | 78 +++ ext/agoo/err.h | 48 ++ ext/agoo/error_stream.c | 92 ++++ ext/agoo/error_stream.h | 13 + ext/agoo/extconf.rb | 13 + ext/agoo/hook.c | 74 +++ ext/agoo/hook.h | 33 ++ ext/agoo/http.c | 617 ++++++++++++++++++++++++ ext/agoo/http.h | 13 + ext/agoo/log.c | 497 +++++++++++++++++++ ext/agoo/log.h | 106 +++++ ext/agoo/log_queue.h | 30 ++ ext/agoo/page.c | 342 ++++++++++++++ ext/agoo/page.h | 39 ++ ext/agoo/queue.c | 191 ++++++++ ext/agoo/queue.h | 39 ++ ext/agoo/request.c | 563 ++++++++++++++++++++++ ext/agoo/request.h | 36 ++ ext/agoo/res.c | 38 ++ ext/agoo/res.h | 28 ++ ext/agoo/response.c | 271 +++++++++++ ext/agoo/response.h | 33 ++ ext/agoo/server.c | 891 +++++++++++++++++++++++++++++++++++ ext/agoo/server.h | 47 ++ ext/agoo/text.c | 66 +++ ext/agoo/text.h | 24 + ext/agoo/types.h | 18 + lib/agoo.rb | 9 + lib/agoo/version.rb | 5 + notes | 23 + test/base_handler_test.rb | 170 +++++++ test/log_test.rb | 269 +++++++++++ test/rack_handler_test.rb | 147 ++++++ test/root/index.html | 5 + test/root/nest/something.txt | 1 + test/static_test.rb | 81 ++++ test/tests.rb | 8 + 49 files changed, 5730 insertions(+), 3 deletions(-) create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 Gemfile create mode 100644 agoo.gemspec create mode 100644 ext/agoo/agoo.c create mode 100644 ext/agoo/con.c create mode 100644 ext/agoo/con.h create mode 100644 ext/agoo/dtime.c create mode 100644 ext/agoo/dtime.h create mode 100644 ext/agoo/err.c create mode 100644 ext/agoo/err.h create mode 100644 ext/agoo/error_stream.c create mode 100644 ext/agoo/error_stream.h create mode 100644 ext/agoo/extconf.rb create mode 100644 ext/agoo/hook.c create mode 100644 ext/agoo/hook.h create mode 100644 ext/agoo/http.c create mode 100644 ext/agoo/http.h create mode 100644 ext/agoo/log.c create mode 100644 ext/agoo/log.h create mode 100644 ext/agoo/log_queue.h create mode 100644 ext/agoo/page.c create mode 100644 ext/agoo/page.h create mode 100644 ext/agoo/queue.c create mode 100644 ext/agoo/queue.h create mode 100644 ext/agoo/request.c create mode 100644 ext/agoo/request.h create mode 100644 ext/agoo/res.c create mode 100644 ext/agoo/res.h create mode 100644 ext/agoo/response.c create mode 100644 ext/agoo/response.h create mode 100644 ext/agoo/server.c create mode 100644 ext/agoo/server.h create mode 100644 ext/agoo/text.c create mode 100644 ext/agoo/text.h create mode 100644 ext/agoo/types.h create mode 100644 lib/agoo.rb create mode 100644 lib/agoo/version.rb create mode 100644 notes create mode 100755 test/base_handler_test.rb create mode 100755 test/log_test.rb create mode 100755 test/rack_handler_test.rb create mode 100644 test/root/index.html create mode 100644 test/root/nest/something.txt create mode 100755 test/static_test.rb create mode 100755 test/tests.rb diff --git a/.gitignore b/.gitignore index 5e1422c..8781163 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,16 @@ /test/tmp/ /test/version_tmp/ /tmp/ +\#*\# +.\#* +*~ +*.o +*.so +Makefile +*.bundle +.rbenv-version +.ruby-version +Gemfile.lock # Used by dotenv library to load environment variables. # .env @@ -48,3 +58,5 @@ build-iPhoneSimulator/ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc + +test/log diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2b3ba28 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +os: + - linux + - osx + +sudo: false +env: + global: + - MAKE="make -j 2" + +language: ruby +rvm: + - 2.4.2 + - 2.5.0 + - ruby-head + +script: + - ./test/tests.rb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1930b81 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG + +### 0.9.0 - 2018-01-28 + +Initial release. diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..b4e2a20 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gemspec diff --git a/LICENSE b/LICENSE index 5d85720..4ccd067 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Peter Ohler +Copyright (c) 2018 Peter Ohler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f6adf4f..9848522 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,61 @@ -# hayai -An HTTP Server for Ruby +# agoo + +[![Build Status](https://img.shields.io/travis/ohler55/agoo/master.svg)](http://travis-ci.org/ohler55/agoo?branch=master) + +A High Performance HTTP Server for Ruby + +## Usage + +```ruby +require 'agoo' + +server = Agoo::Server.new(6464, 'root') + +class MyHandler + def initialize + end + + def call(req) + [ 200, { }, [ "hello world" ] ] + end +end + +handler = TellMeHandler.new +server.handle(:GET, "/hello", handler) +server.start() +``` + +## Installation +``` +gem install agoo +``` + +## What Is This? + +Agoo is Japanese for a type of flying fish. This gem flies. It is a high +performance HTTP server that serves static resource at hundreds of thousands +of fetchs per second. A a simple hello world Ruby handler at over 100,000 +requests per second on a desktop computer. That places Agoo at about 80 times +faster than Sinatra and 1000 times faster than Rails. In both cases the +latency was an order of magnitude lower or more. Checkout the benchmarks on OpO +benchmarks. Note that the benchmarks had to use a C program called _hose_ +from the OpO downloads to hit +the Agoo limits. Ruby benchmarks driver could not push Agoo hard enough. + +Agoo supports the [Ruby rack API](https://rack.github.io) which allows for the +use of rack compatible gems. + +## Releases + +See [{file:CHANGELOG.md}](CHANGELOG.md) + +## Links + + - *Documentation*: http://rubydoc.info/gems/agoo + + - *GitHub* *repo*: https://github.com/ohler55/agoo + + - *RubyGems* *repo*: https://rubygems.org/gems/agoo + +Follow [@peterohler on Twitter](http://twitter.com/#!/peterohler) for announcements and news about the Agoo gem. diff --git a/agoo.gemspec b/agoo.gemspec new file mode 100644 index 0000000..975106d --- /dev/null +++ b/agoo.gemspec @@ -0,0 +1,31 @@ + +require 'date' +require File.join(File.dirname(__FILE__), 'lib/agoo/version') + +Gem::Specification.new do |s| + s.name = "agoo" + s.version = Agoo::VERSION + s.authors = "Peter Ohler" + s.date = Date.today.to_s + s.email = "peter@ohler.com" + s.homepage = "http://www.ohler.com/agoo" + s.summary = "An HTTP server" + s.description = "A fast HTTP server supporting rack." + s.licenses = ['MIT'] + s.required_ruby_version = ">= 2.0" + + s.files = Dir["{lib,ext,test}/**/*.{rb,h,c}"] + ['LICENSE', 'README.md'] + s.test_files = Dir["test/**/*.rb"] + s.extensions = ["ext/agoo/extconf.rb"] + + s.has_rdoc = true + s.extra_rdoc_files = ['README.md'] + Dir["pages/*.md"] + s.rdoc_options = ['-t', 'Agoo', '-m', 'README.md', '-x', 'test/*'] + + s.rubyforge_project = 'agoo' + + s.add_development_dependency 'rake-compiler', '>= 0.9', '< 2.0' + s.add_development_dependency 'minitest', '~> 5' + s.add_development_dependency 'test-unit', '~> 3.0' + s.add_development_dependency 'wwtd', '~> 0' +end diff --git a/ext/agoo/agoo.c b/ext/agoo/agoo.c new file mode 100644 index 0000000..b9349e0 --- /dev/null +++ b/ext/agoo/agoo.c @@ -0,0 +1,19 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include +#include + +#include "error_stream.h" +#include "request.h" +#include "response.h" +#include "server.h" + +void +Init_agoo() { + VALUE mod = rb_define_module("Agoo"); + + error_stream_init(mod); + request_init(mod); + response_init(mod); + server_init(mod); +} diff --git a/ext/agoo/con.c b/ext/agoo/con.c new file mode 100644 index 0000000..6c6b178 --- /dev/null +++ b/ext/agoo/con.c @@ -0,0 +1,515 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include +#include + +#include "con.h" +#include "dtime.h" +#include "hook.h" +#include "http.h" +#include "res.h" +#include "server.h" + +#define MAX_SOCK 4096 +#define CON_TIMEOUT 5.0 + +Con +con_create(Err err, Server server, int sock, uint64_t id) { + Con c; + + if (MAX_SOCK <= sock) { + err_set(err, ERR_TOO_MANY, "Too many connections."); + return NULL; + } + if (NULL == (c = (Con)malloc(sizeof(struct _Con)))) { + err_set(err, ERR_MEMORY, "Failed to allocate memory for a connection."); + } else { + memset(c, 0, sizeof(struct _Con)); + c->sock = sock; + c->iid = id; + sprintf(c->id, "%llu", (unsigned long long)id); + c->server = server; + } + return c; +} + +void +con_destroy(Con c) { + if (0 < c->sock) { + close(c->sock); + c->sock = 0; + } + if (NULL != c->req) { + free(c->req); + } + free(c); +} + +const char* +con_header_value(const char *header, int hlen, const char *key, int *vlen) { + // Search for \r then check for \n and then the key followed by a :. Keep + // trying until the end of the header. + const char *h = header; + const char *hend = header + hlen; + const char *value; + int klen = strlen(key); + + while (h < hend) { + if (0 == strncmp(key, h, klen) && ':' == h[klen]) { + h += klen + 1; + for (; ' ' == *h; h++) { + } + value = h; + for (; '\r' != *h && '\0' != *h; h++) { + } + *vlen = h - value; + + return value; + } + for (; h < hend; h++) { + if ('\r' == *h && '\n' == *(h + 1)) { + h += 2; + break; + } + } + } + return NULL; +} + +static bool +bad_request(Con c, int status, int line) { + Res res; + const char *msg = http_code_message(status); + + if (NULL == (res = res_create())) { + log_cat(&c->server->error_cat, "memory allocation of response failed on connection %llu @ %d.", c->iid, line); + } else { + char buf[256]; + int cnt = snprintf(buf, sizeof(buf), "HTTP/1.1 %d %s\r\nConnection: Close\r\nContent-Length: 0\r\n\r\n", status, msg); + Text message = text_create(buf, cnt); + + if (NULL == c->res_tail) { + c->res_head = res; + } else { + c->res_tail->next = res; + } + c->res_tail = res; + res->close = true; + res_set_message(res, message); + } + return true; +} + +static bool +should_close(const char *header, int hlen) { + const char *v; + int vlen = 0; + + if (NULL != (v = con_header_value(header, hlen, "Connection", &vlen))) { + return (5 == vlen && 0 == strncasecmp("Close", v, 5)); + } + return false; +} + +// Returns true if request has handled. +static bool +con_header_read(Con c) { + Server server = c->server; + char *hend = strstr(c->buf, "\r\n\r\n"); + Method method; + char *path; + char *pend; + char *query = NULL; + char *qend; + char *b; + size_t clen = 0; + size_t mlen; + Hook hook = NULL; + + if (NULL == hend) { + if (sizeof(c->buf) - 1 <= c->bcnt) { + return bad_request(c, 431, __LINE__); + } + return false; + } + if (server->req_cat.on) { + *hend = '\0'; + log_cat(&server->req_cat, "%llu: %s", c->iid, c->buf); + *hend = '\r'; + } + for (b = c->buf; ' ' != *b; b++) { + if ('\0' == *b) { + return bad_request(c, 400, __LINE__); + } + } + switch (toupper(*c->buf)) { + case 'G': + if (3 != b - c->buf || 0 != strncmp("GET", c->buf, 3)) { + return bad_request(c, 400, __LINE__); + } + method = GET; + break; + case 'P': { + const char *v; + int vlen = 0; + char *vend; + + if (3 == b - c->buf && 0 == strncmp("PUT", c->buf, 3)) { + method = PUT; + } else if (4 == b - c->buf && 0 == strncmp("POST", c->buf, 4)) { + method = POST; + } else { + return bad_request(c, 400, __LINE__); + } + if (NULL == (v = con_header_value(c->buf, hend - c->buf, "Content-Length", &vlen))) { + return bad_request(c, 411, __LINE__); + } + clen = (size_t)strtoul(v, &vend, 10); + if (vend != v + vlen) { + return bad_request(c, 411, __LINE__); + } + break; + } + case 'D': + if (6 != b - c->buf || 0 != strncmp("DELETE", c->buf, 6)) { + return bad_request(c, 400, __LINE__); + } + method = DELETE; + break; + case 'H': + if (4 != b - c->buf || 0 != strncmp("HEAD", c->buf, 4)) { + return bad_request(c, 400, __LINE__); + } + method = HEAD; + break; + case 'O': + if (7 != b - c->buf || 0 != strncmp("OPTIONS", c->buf, 7)) { + return bad_request(c, 400, __LINE__); + } + method = OPTIONS; + break; + case 'C': + if (7 != b - c->buf || 0 != strncmp("CONNECT", c->buf, 7)) { + return bad_request(c, 400, __LINE__); + } + method = CONNECT; + break; + default: + return bad_request(c, 400, __LINE__); + } + for (; ' ' == *b; b++) { + if ('\0' == *b) { + return bad_request(c, 400, __LINE__); + } + } + path = b; + for (; ' ' != *b; b++) { + switch (*b) { + case '?': + pend = b; + query = b + 1; + break; + case '\0': + return bad_request(c, 400, __LINE__); + default: + break; + } + } + if (NULL == query) { + pend = b; + query = b; + qend = b; + } else { + qend = b; + } + if (NULL == (hook = hook_find(server->hooks, method, path, pend))) { + if (GET == method) { + struct _Err err = ERR_INIT; + Page p = page_get(&err, &server->pages, server->root, path, pend - path); + Res res; + + if (NULL == p) { + return bad_request(c, 404, __LINE__); + } + if (NULL == (res = res_create())) { + return bad_request(c, 500, __LINE__); + } + if (NULL == c->res_tail) { + c->res_head = res; + } else { + c->res_tail->next = res; + } + c->res_tail = res; + + b = strstr(c->buf, "\r\n"); + res->close = should_close(b, hend - b); + + text_ref(p->resp); + res_set_message(res, p->resp); + + return true; + } + } + // Create request and populate. + mlen = hend - c->buf + 4 + clen; + if (NULL == (c->req = (Req)malloc(mlen + sizeof(struct _Req) - 8 + 1))) { + return bad_request(c, 413, __LINE__); + } + memcpy(c->req->msg, c->buf, c->bcnt); + if (c->bcnt < mlen) { + memset(c->req->msg + c->bcnt, 0, mlen - c->bcnt); + } + c->req->server = server; + c->req->method = method; + c->req->path.start = c->req->msg + (path - c->buf); + c->req->path.len = pend - path; + c->req->query.start = c->req->msg + (query - c->buf); + c->req->query.len = qend - query; + c->req->mlen = mlen; + c->req->body.start = c->req->msg + (hend - c->buf + 4); + c->req->body.len = clen; + b = strstr(b, "\r\n"); + c->req->header.start = c->req->msg + (b + 2 - c->buf); + c->req->header.len = hend - b - 2; + c->req->res = NULL; + if (NULL != hook) { + c->req->handler = hook->handler; + c->req->handler_type = hook->type; + } else { + c->req->handler = Qnil; + c->req->handler_type = NO_HOOK; + } + return false; +} + +// return true to remove/close connection +static bool +con_read(Con c) { + ssize_t cnt; + + if (NULL != c->req) { + cnt = recv(c->sock, c->req->msg + c->bcnt, c->req->mlen - c->bcnt, 0); + } else { + cnt = recv(c->sock, c->buf + c->bcnt, sizeof(c->buf) - c->bcnt - 1, 0); + } + c->timeout = dtime() + CON_TIMEOUT; + if (0 >= cnt) { + // If nothing read then no need to complain. Just close. + if (0 < c->bcnt) { + if (0 == cnt) { + log_cat(&c->server->warn_cat, "Nothing to read. Client closed socket %s.", c->id); + } else { + log_cat(&c->server->warn_cat, "Failed to read request. %s.", strerror(errno)); + } + } + return true; + } + c->bcnt += cnt; + // Terminate with \0 for debug and strstr() check + if (NULL == c->req) { + c->buf[c->bcnt] = '\0'; + if (con_header_read(c)) { // already handled + c->bcnt = 0; + *c->buf = '\0'; + // TBD handle extra data in buf + + return false; + } + } + if (NULL != c->req) { + c->req->msg[c->bcnt] = '\0'; + if (c->req->mlen <= c->bcnt) { + Res res; + + if (c->server->debug_cat.on && NULL != c->req && NULL != c->req->body.start) { + log_cat(&c->server->debug_cat, "request on %llu: %s", c->iid, c->req->body.start); + } + if (NULL == (res = res_create())) { + c->req = NULL; + log_cat(&c->server->error_cat, "memory allocation of response failed on connection %llu.", c->iid); + return bad_request(c, 500, __LINE__); + } else { + if (NULL == c->res_tail) { + c->res_head = res; + } else { + c->res_tail->next = res; + } + c->res_tail = res; + res->close = should_close(c->req->header.start, c->req->header.len); + } + c->req->res = res; + queue_push(&c->server->eval_queue, (void*)c->req); + c->req = NULL; + c->bcnt = 0; + *c->buf = '\0'; + // TBD handle extra data in buf + + return false; + } + } + return false; +} + +// return true to remove/close connection +static bool +con_write(Con c) { + Text message = res_message(c->res_head); + ssize_t cnt; + + c->timeout = dtime() + CON_TIMEOUT; + if (0 == c->wcnt) { + if (c->server->resp_cat.on) { + char buf[4096]; + char *hend = strstr(message->text, "\r\n\r\n"); + + if (NULL == hend) { + hend = message->text + message->len; + } + memcpy(buf, message->text, hend - message->text); + log_cat(&c->server->resp_cat, "%llu: %s", c->iid, buf); + } + if (c->server->debug_cat.on) { + log_cat(&c->server->debug_cat, "response on %llu: %s", c->iid, message->text); + } + } + if (0 > (cnt = send(c->sock, message->text + c->wcnt, message->len - c->wcnt, 0))) { + if (EAGAIN == errno) { + return false; + } + log_cat(&c->server->error_cat, "Socket error @ %llu.", c->iid); + + return true; + } + c->wcnt += cnt; + if (c->wcnt == message->len) { // finished + Res res = c->res_head; + bool done = res->close; + + c->res_head = res->next; + if (res == c->res_tail) { + c->res_tail = NULL; + } + c->wcnt = 0; + res_destroy(res); + + return done; + } + return false; +} + +void* +con_loop(void *x) { + Server server = (Server)x; + Con c; + Con ca[MAX_SOCK]; + struct pollfd pa[MAX_SOCK]; + struct pollfd *pp; + Con *end = ca + MAX_SOCK; + Con *cp; + int ccnt = 0; + int i; + long mcnt = 0; + double now; + + atomic_fetch_add(&server->running, 1); + memset(ca, 0, sizeof(ca)); + memset(pa, 0, sizeof(pa)); + while (server->active) { + while (NULL != (c = (Con)queue_pop(&server->con_queue, 0.0))) { + mcnt++; + ca[c->sock] = c; + ccnt++; + } + pp = pa; + pp->fd = queue_listen(&server->con_queue); + pp->events = POLLIN; + pp->revents = 0; + pp++; + for (i = ccnt, cp = ca; 0 < i && cp < end; cp++) { + if (NULL == *cp) { + continue; + } + c = *cp; + c->pp = pp; + pp->fd = c->sock; + if (NULL != c->res_head && NULL != res_message(c->res_head)) { + pp->events = POLLIN | POLLOUT; + } else { + pp->events = POLLIN; + } + pp->revents = 0; + i--; + pp++; + } + if (0 > (i = poll(pa, pp - pa, 100))) { + if (EAGAIN == errno) { + continue; + } + log_cat(&server->error_cat, "Polling error. %s.", strerror(errno)); + // Either a signal or something bad like out of memory. Might as well exit. + break; + } + now = dtime(); + + if (0 == i) { // nothing to read or write + // TBD check for cons to close + continue; + } + if (0 != (pa->revents & POLLIN)) { + queue_release(&server->con_queue); + while (NULL != (c = (Con)queue_pop(&server->con_queue, 0.0))) { + mcnt++; + ca[c->sock] = c; + ccnt++; + } + } + for (i = ccnt, cp = ca; 0 < i && cp < end; cp++) { + if (NULL == *cp || 0 == (*cp)->sock || NULL == (*cp)->pp) { + continue; + } + c = *cp; + i--; + pp = c->pp; + if (0 != (pp->revents & POLLIN)) { + if (con_read(c)) { + goto CON_RM; + } + } + if (0 != (pp->revents & POLLOUT)) { + if (con_write(c)) { + goto CON_RM; + } + } + if (0 != (pp->revents & (POLLERR | POLLHUP | POLLNVAL))) { + if (0 < c->bcnt) { + if (0 != (pp->revents & (POLLHUP | POLLNVAL))) { + log_cat(&server->error_cat, "Socket %llu closed.", c->iid); + } else if (!c->closing) { + log_cat(&server->error_cat, "Socket %llu error. %s", c->iid, strerror(errno)); + } + } + goto CON_RM; + } + if (0.0 == c->timeout || now < c->timeout) { + continue; + } else if (c->closing) { + goto CON_RM; + } else { + c->closing = true; + c->timeout = now + 0.5; + //wush_text_set(&c->resp, (char*)close_resp, sizeof(close_resp) - 1, false); + continue; + } + continue; + CON_RM: + ca[c->sock] = NULL; + ccnt--; + log_cat(&server->con_cat, "Connection %llu closed.", c->iid); + con_destroy(c); + } + } + atomic_fetch_sub(&server->running, 1); + + return NULL; +} + diff --git a/ext/agoo/con.h b/ext/agoo/con.h new file mode 100644 index 0000000..603f3b6 --- /dev/null +++ b/ext/agoo/con.h @@ -0,0 +1,46 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_CON_H__ +#define __AGOO_CON_H__ + +#include +#include +#include + +#include "err.h" +#include "request.h" +#include "response.h" +#include "server.h" + +#define MAX_HEADER_SIZE 8192 + +typedef struct _Con { + int sock; + struct pollfd *pp; + char id[32]; + uint64_t iid; + char buf[MAX_HEADER_SIZE]; + size_t bcnt; + + ssize_t msize; // size of complete message + ssize_t mcnt; // how much has been read so far + + ssize_t wcnt; // how much has been written + + Server server; + double timeout; + bool closing; + Req req; + Res res_head; + Res res_tail; + + //FEval eval; +} *Con; + +extern Con con_create(Err err, Server server, int sock, uint64_t id); +extern void con_destroy(Con c); +extern const char* con_header_value(const char *header, int hlen, const char *key, int *vlen); + +extern void* con_loop(void *ctx); + +#endif /* __AGOO_CON_H__ */ diff --git a/ext/agoo/dtime.c b/ext/agoo/dtime.c new file mode 100644 index 0000000..c40a27f --- /dev/null +++ b/ext/agoo/dtime.c @@ -0,0 +1,52 @@ +// Copyright 2009, 2015, 2016, 2018 by Peter Ohler, All Rights Reserved + +#include +#include +#include + +#include "dtime.h" + +#define MIN_SLEEP (1.0 / (double)CLOCKS_PER_SEC) + +double +dtime() { + struct timeval tv; + struct timezone tz; + + gettimeofday(&tv, &tz); + + return (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0; +} + +double +dsleep(double t) { + struct timespec req, rem; + + if (MIN_SLEEP > t) { + t = MIN_SLEEP; + } + req.tv_sec = (time_t)t; + req.tv_nsec = (long)(1000000000.0 * (t - (double)req.tv_sec)); + if (nanosleep(&req, &rem) == -1 && EINTR == errno) { + return (double)rem.tv_sec + (double)rem.tv_nsec / 1000000000.0; + } + return 0.0; +} + +double +dwait(double t) { + double end = dtime() + t; + + if (MIN_SLEEP < t) { + struct timespec req, rem; + + t -= MIN_SLEEP; + req.tv_sec = (time_t)t; + req.tv_nsec = (long)(1000000000.0 * (t - (double)req.tv_sec)); + nanosleep(&req, &rem); + } + while (dtime() < end) { + continue; + } + return 0.0; +} diff --git a/ext/agoo/dtime.h b/ext/agoo/dtime.h new file mode 100644 index 0000000..13675cc --- /dev/null +++ b/ext/agoo/dtime.h @@ -0,0 +1,10 @@ +// Copyright 2009, 2015, 2018, 2016 by Peter Ohler, All Rights Reserved + +#ifndef __AGOO_DTIME_H__ +#define __AGOO_DTIME_H__ + +extern double dtime(void); +extern double dsleep(double t); +extern double dwait(double t); + +#endif /* __AGOO_DTIME_H__ */ diff --git a/ext/agoo/err.c b/ext/agoo/err.c new file mode 100644 index 0000000..3c42f0a --- /dev/null +++ b/ext/agoo/err.c @@ -0,0 +1,78 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include +#include +#include + +#include "err.h" + +#ifdef PLATFORM_LINUX +#pragma GCC diagnostic ignored "-Wsuggest-attribute=format" +#endif + +int +err_set(Err err, int code, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + vsnprintf(err->msg, sizeof(err->msg), fmt, ap); + va_end(ap); + err->code = code; + + return err->code; +} + +int +err_no(Err err, const char *fmt, ...) { + int cnt = 0; + + if (NULL != fmt) { + va_list ap; + + va_start(ap, fmt); + cnt = vsnprintf(err->msg, sizeof(err->msg), fmt, ap); + va_end(ap); + } + if (cnt < (int)sizeof(err->msg) + 2 && 0 <= cnt) { + err->msg[cnt] = ' '; + cnt++; + strncpy(err->msg + cnt, strerror(errno), sizeof(err->msg) - cnt); + err->msg[sizeof(err->msg) - 1] = '\0'; + } + err->code = errno; + + return err->code; +} + +void +err_clear(Err err) { + err->code = ERR_OK; + *err->msg = '\0'; +} + +const char* +err_str(ErrCode code) { + const char *str = NULL; + + if (code <= ELAST) { + str = strerror(code); + } + if (NULL == str) { + switch (code) { + case ERR_PARSE: str = "parse error"; break; + case ERR_READ: str = "read failed"; break; + case ERR_WRITE: str = "write failed"; break; + case ERR_ARG: str = "invalid argument"; break; + case ERR_NOT_FOUND: str = "not found"; break; + case ERR_THREAD: str = "thread error"; break; + case ERR_NETWORK: str = "network error"; break; + case ERR_LOCK: str = "lock error"; break; + case ERR_FREE: str = "already freed"; break; + case ERR_IN_USE: str = "in use"; break; + case ERR_TOO_MANY: str = "too many"; break; + case ERR_TYPE: str = "type error"; break; + default: str = "unknown error"; break; + } + } + return str; +} diff --git a/ext/agoo/err.h b/ext/agoo/err.h new file mode 100644 index 0000000..c034493 --- /dev/null +++ b/ext/agoo/err.h @@ -0,0 +1,48 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_ERR_H__ +#define __AGOO_ERR_H__ + +#include + +#ifndef ELAST +#define ELAST 300 +#endif + +#define ERR_INIT { 0, { 0 } } + +typedef enum { + ERR_OK = 0, + ERR_MEMORY = ENOMEM, + ERR_DENIED = EACCES, + ERR_IMPL = ENOSYS, + ERR_PARSE = ELAST + 1, + ERR_READ, + ERR_WRITE, + ERR_OVERFLOW, + ERR_ARG, + ERR_NOT_FOUND, + ERR_THREAD, + ERR_NETWORK, + ERR_LOCK, + ERR_FREE, + ERR_IN_USE, + ERR_TOO_MANY, + ERR_TYPE, + ERR_LAST +} ErrCode; + +// The struct used to report errors or status after a function returns. The +// struct must be initialized before use as most calls that take an err +// argument will return immediately if an error has already occurred. +typedef struct _Err { + int code; + char msg[256]; +} *Err; + +extern int err_set(Err err, int code, const char *fmt, ...); +extern int err_no(Err err, const char *fmt, ...); +extern const char* err_str(ErrCode code); +extern void err_clear(Err err); + +#endif /* __AGOO_ERR_H__ */ diff --git a/ext/agoo/error_stream.c b/ext/agoo/error_stream.c new file mode 100644 index 0000000..5ae7c59 --- /dev/null +++ b/ext/agoo/error_stream.c @@ -0,0 +1,92 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include "error_stream.h" +#include "text.h" + +static VALUE es_class = Qundef; + +typedef struct _ErrorStream { + Server server; + Text text; +} *ErrorStream; + +static void +es_free(void *ptr) { + ErrorStream es = (ErrorStream)ptr; + + free(es->text); // allocated with malloc + xfree(ptr); +} + +VALUE +error_stream_new(Server server) { + ErrorStream es = ALLOC(struct _ErrorStream); + + es->text = text_allocate(1024); + es->server = server; + + return Data_Wrap_Struct(es_class, NULL, es_free, es); +} + +/* Document-method: puts + + * call-seq: puts(str) + * + * Write the _str_ to the stream along with a newline character, accumulating + * it until _flush_ is called. + */ +static VALUE +es_puts(VALUE self, VALUE str) { + ErrorStream es = (ErrorStream)DATA_PTR(self); + + es->text = text_append(es->text, StringValuePtr(str), RSTRING_LEN(str)); + es->text = text_append(es->text, "\n", 1); + + return Qnil; +} + +/* Document-method: write + + * call-seq: write(str) + * + * Write the _str_ to the stream, accumulating it until _flush_ is called. + */ +static VALUE +es_write(VALUE self, VALUE str) { + ErrorStream es = (ErrorStream)DATA_PTR(self); + int cnt = RSTRING_LEN(str); + + es->text = text_append(es->text, StringValuePtr(str), cnt); + + return INT2NUM(cnt); +} + +/* Document-method: flush + + * call-seq: flush() + * + * Flushs the accumulated text in the stream as an error log entry. + */ +static VALUE +es_flush(VALUE self) { + ErrorStream es = (ErrorStream)DATA_PTR(self); + + log_cat(&es->server->error_cat, "%s", es->text->text); + es->text->len = 0; + + return self; +} + +/* Document-class: Agoo::ErrorStream + * + * Used in a reqquest as the _rack.errors_ attribute. Writing to the stream + * and flushing will make an error log entry. + */ +void +error_stream_init(VALUE mod) { + es_class = rb_define_class_under(mod, "ErrorStream", rb_cObject); + + rb_define_method(es_class, "puts", es_puts, 1); + rb_define_method(es_class, "write", es_write, 1); + rb_define_method(es_class, "flush", es_flush, 0); +} diff --git a/ext/agoo/error_stream.h b/ext/agoo/error_stream.h new file mode 100644 index 0000000..3dff8d7 --- /dev/null +++ b/ext/agoo/error_stream.h @@ -0,0 +1,13 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_ERROR_STREAM_H__ +#define __AGOO_ERROR_STREAM_H__ + +#include + +#include "server.h" + +extern void error_stream_init(VALUE mod); +extern VALUE error_stream_new(Server server); + +#endif // __AGOO_ERROR_STREAM_H__ diff --git a/ext/agoo/extconf.rb b/ext/agoo/extconf.rb new file mode 100644 index 0000000..9e1459a --- /dev/null +++ b/ext/agoo/extconf.rb @@ -0,0 +1,13 @@ +require 'mkmf' +require 'rbconfig' + +extension_name = 'agoo' +dir_config(extension_name) + +$CPPFLAGS += " -DPLATFORM_LINUX" if 'x86_64-linux' == RUBY_PLATFORM + +create_makefile(File.join(extension_name, extension_name)) + +puts ">>>>> Created Makefile for #{RUBY_DESCRIPTION.split(' ')[0]} version #{RUBY_VERSION} on #{RUBY_PLATFORM} <<<<<" + +%x{make clean} diff --git a/ext/agoo/hook.c b/ext/agoo/hook.c new file mode 100644 index 0000000..8c8f7cc --- /dev/null +++ b/ext/agoo/hook.c @@ -0,0 +1,74 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include +#include + +#include "hook.h" + +Hook +hook_create(Method method, const char *pattern, VALUE handler) { + Hook hook = (Hook)malloc(sizeof(struct _Hook)); + + if (NULL != hook) { + if (NULL == pattern) { + pattern = ""; + } + hook->next = NULL; + hook->handler = handler; + rb_gc_register_address(&handler); + hook->pattern = strdup(pattern); + hook->method = method; + if (rb_respond_to(handler, rb_intern("on_request"))) { + hook->type = BASE_HOOK; + } else if (rb_respond_to(handler, rb_intern("call"))) { + hook->type = RACK_HOOK; + } else if (rb_respond_to(handler, rb_intern("create")) && + rb_respond_to(handler, rb_intern("read")) && + rb_respond_to(handler, rb_intern("update")) && + rb_respond_to(handler, rb_intern("delete"))) { + hook->type = WAB_HOOK; + } else { + rb_raise(rb_eArgError, "handler does not have a on_request, or call method nor is it a WAB::Controller"); + } + } + return hook; +} + +void +hook_destroy(Hook hook) { + free(hook->pattern); + free(hook); +} + +bool +hook_match(Hook hook, Method method, const char *path, const char *pend) { + const char *pat = hook->pattern; + + if (method != hook->method && ALL != hook->method) { + return false; + } + for (; '\0' != *pat && path < pend; pat++) { + if (*path == *pat) { + path++; + } else if ('*' == *pat) { + if ('*' == *(pat + 1)) { + return true; + } + for (; path < pend && '/' != *path; path++) { + } + } else { + break; + } + } + return '\0' == *pat && path == pend; +} + +Hook +hook_find(Hook hook, Method method, const char *path, const char *pend) { + for (; NULL != hook; hook = hook->next) { + if (hook_match(hook, method, path, pend)) { + return hook; + } + } + return NULL; +} diff --git a/ext/agoo/hook.h b/ext/agoo/hook.h new file mode 100644 index 0000000..f3e6d33 --- /dev/null +++ b/ext/agoo/hook.h @@ -0,0 +1,33 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_HOOK_H__ +#define __AGOO_HOOK_H__ + +#include + +#include + +#include "types.h" + +typedef enum { + NO_HOOK = '\0', + RACK_HOOK = 'R', + BASE_HOOK = 'B', + WAB_HOOK = 'W', +} HookType; + +typedef struct _Hook { + struct _Hook *next; + Method method; + VALUE handler; + char *pattern; + HookType type; +} *Hook; + +extern Hook hook_create(Method method, const char *pattern, VALUE handler); +extern void hook_destroy(Hook hook); + +extern bool hook_match(Hook hook, Method method, const char *path, const char *pend); +extern Hook hook_find(Hook hook, Method method, const char *path, const char *pend); + +#endif // __AGOO_HOOK_H__ diff --git a/ext/agoo/http.c b/ext/agoo/http.c new file mode 100644 index 0000000..330ce66 --- /dev/null +++ b/ext/agoo/http.c @@ -0,0 +1,617 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include +#include +#include + +#include + +#include "http.h" + +#define BUCKET_SIZE 1024 +#define BUCKET_MASK 1023 +#define MAX_KEY_UNIQ 9 + +typedef struct _Slot { + struct _Slot *next; + const char *key; + uint64_t hash; + int klen; +} *Slot; + +typedef struct _Cache { + Slot buckets[BUCKET_SIZE]; +} *Cache; + +struct _Cache key_cache; + +// The rack spec indicates the characters (),/:;<=>?@[]{} are invalid which +// clearly is not consisten with RFC7230 so stick with the RFC. +static char header_value_chars[256] = "\ +xxxxxxxxxxoxxxxxxxxxxxxxxxxxxxxx\ +oooooooooooooooooooooooooooooooo\ +oooooooooooooooooooooooooooooooo\ +ooooooooooooooooooooooooooooooox\ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; + +static const char *header_keys[] = { + "A-IM", + "ALPN", + "ARC-Authentication-Results", + "ARC-Message-Signature", + "ARC-Seal", + "Accept", + "Accept-Additions", + "Accept-Charset", + "Accept-Datetime", + "Accept-Encoding", + "Accept-Features", + "Accept-Language", + "Accept-Language", + "Accept-Patch", + "Accept-Post", + "Accept-Ranges", + "Access-Control", + "Access-Control-Allow-Credentials", + "Access-Control-Allow-Headers", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Origin", + "Access-Control-Max-Age", + "Access-Control-Request-Headers", + "Access-Control-Request-Method", + "Age", + "Allow", + "Also-Control", + "Alt-Svc", + "Alt-Used", + "Alternate-Recipient", + "Alternates", + "Apparently-To", + "Apply-To-Redirect-Ref", + "Approved", + "Archive", + "Archived-At", + "Archived-At", + "Article-Names", + "Article-Updates", + "Authentication-Control", + "Authentication-Info", + "Authentication-Results", + "Authorization", + "Auto-Submitted", + "Autoforwarded", + "Autosubmitted", + "Base", + "Bcc", + "Body", + "C-Ext", + "C-Man", + "C-Opt", + "C-PEP", + "C-PEP-Info", + "Cache-Control", + "CalDAV-Timezones", + "Cancel-Key", + "Cancel-Lock", + "Cc", + "Close", + "Comments", + "Comments", + "Compliance", + "Connection", + "Content-Alternative", + "Content-Base", + "Content-Base", + "Content-Description", + "Content-Disposition", + "Content-Disposition", + "Content-Duration", + "Content-Encoding", + "Content-ID", + "Content-ID", + "Content-Identifier", + "Content-Language", + "Content-Language", + "Content-Length", + "Content-Location", + "Content-Location", + "Content-MD5", + "Content-MD5", + "Content-Range", + "Content-Return", + "Content-Script-Type", + "Content-Style-Type", + "Content-Transfer-Encoding", + "Content-Transfer-Encoding", + "Content-Translation-Type", + "Content-Type", + "Content-Type", + "Content-Version", + "Content-features", + "Control", + "Conversion", + "Conversion-With-Loss", + "Cookie", + "Cookie2", + "Cost", + "DASL", + "DAV", + "DKIM-Signature", + "DL-Expansion-History", + "Date", + "Date", + "Date", + "Date-Received", + "Default-Style", + "Deferred-Delivery", + "Delivery-Date", + "Delta-Base", + "Depth", + "Derived-From", + "Destination", + "Differential-ID", + "Digest", + "Discarded-X400-IPMS-Extensions", + "Discarded-X400-MTS-Extensions", + "Disclose-Recipients", + "Disposition-Notification-Options", + "Disposition-Notification-To", + "Distribution", + "Downgraded-Bcc", + "Downgraded-Cc", + "Downgraded-Disposition-Notification-To", + "Downgraded-Final-Recipient", + "Downgraded-From", + "Downgraded-In-Reply-To", + "Downgraded-Mail-From", + "Downgraded-Message-Id", + "Downgraded-Original-Recipient", + "Downgraded-Rcpt-To", + "Downgraded-References", + "Downgraded-Reply-To", + "Downgraded-Resent-Bcc", + "Downgraded-Resent-Cc", + "Downgraded-Resent-From", + "Downgraded-Resent-Reply-To", + "Downgraded-Resent-Sender", + "Downgraded-Resent-To", + "Downgraded-Return-Path", + "Downgraded-Sender", + "Downgraded-To", + "EDIINT-Features", + "EDIINT-Features", + "ETag", + "Eesst-Version", + "Encoding", + "Encrypted", + "Errors-To", + "Expect", + "Expires", + "Expires", + "Expires", + "Expiry-Date", + "Ext", + "Followup-To", + "Form-Sub", + "Forwarded", + "From", + "From", + "From", + "Generate-Delivery-Report", + "GetProfile", + "HTTP2-Settings", + "Hobareg", + "Host", + "IM", + "If", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Range", + "If-Schedule-Tag-Match", + "If-Unmodified-Since", + "Importance", + "In-Reply-To", + "Incomplete-Copy", + "Injection-Date", + "Injection-Info", + "Jabber-ID", + "Jabber-ID", + "Keep-Alive", + "Keywords", + "Keywords", + "Label", + "Language", + "Last-Modified", + "Latest-Delivery-Time", + "Lines", + "Link", + "List-Archive", + "List-Help", + "List-ID", + "List-Owner", + "List-Post", + "List-Subscribe", + "List-Unsubscribe", + "List-Unsubscribe-Post", + "Location", + "Lock-Token", + "MIME-Version", + "MIME-Version", + "MMHS-Acp127-Message-Identifier", + "MMHS-Authorizing-Users", + "MMHS-Codress-Message-Indicator", + "MMHS-Copy-Precedence", + "MMHS-Exempted-Address", + "MMHS-Extended-Authorisation-Info", + "MMHS-Handling-Instructions", + "MMHS-Message-Instructions", + "MMHS-Message-Type", + "MMHS-Originator-PLAD", + "MMHS-Originator-Reference", + "MMHS-Other-Recipients-Indicator-CC", + "MMHS-Other-Recipients-Indicator-To", + "MMHS-Primary-Precedence", + "MMHS-Subject-Indicator-Codes", + "MT-Priority", + "Man", + "Max-Forwards", + "Memento-Datetime", + "Message-Context", + "Message-ID", + "Message-ID", + "Message-ID", + "Message-Type", + "Meter", + "Method-Check", + "Method-Check-Expires", + "NNTP-Posting-Date", + "NNTP-Posting-Host", + "Negotiate", + "Newsgroups", + "Non-Compliance", + "Obsoletes", + "Opt", + "Optional", + "Optional-WWW-Authenticate", + "Ordering-Type", + "Organization", + "Organization", + "Origin", + "Original-Encoded-Information-Types", + "Original-From", + "Original-Message-ID", + "Original-Recipient", + "Original-Sender", + "Original-Subject", + "Originator-Return-Address", + "Overwrite", + "P3P", + "PEP", + "PICS-Label", + "PICS-Label", + "Path", + "Pep-Info", + "Position", + "Posting-Version", + "Pragma", + "Prefer", + "Preference-Applied", + "Prevent-NonDelivery-Report", + "Priority", + "Privicon", + "ProfileObject", + "Protocol", + "Protocol-Info", + "Protocol-Query", + "Protocol-Request", + "Proxy-Authenticate", + "Proxy-Authentication-Info", + "Proxy-Authorization", + "Proxy-Features", + "Proxy-Instruction", + "Public", + "Public-Key-Pins", + "Public-Key-Pins-Report-Only", + "Range", + "Received", + "Received-SPF", + "Redirect-Ref", + "References", + "References", + "Referer", + "Referer-Root", + "Relay-Version", + "Reply-By", + "Reply-To", + "Reply-To", + "Require-Recipient-Valid-Since", + "Resent-Bcc", + "Resent-Cc", + "Resent-Date", + "Resent-From", + "Resent-Message-ID", + "Resent-Reply-To", + "Resent-Sender", + "Resent-To", + "Resolution-Hint", + "Resolver-Location", + "Retry-After", + "Return-Path", + "SIO-Label", + "SIO-Label-History", + "SLUG", + "Safe", + "Schedule-Reply", + "Schedule-Tag", + "Sec-WebSocket-Accept", + "Sec-WebSocket-Extensions", + "Sec-WebSocket-Key", + "Sec-WebSocket-Protocol", + "Sec-WebSocket-Version", + "Security-Scheme", + "See-Also", + "Sender", + "Sender", + "Sensitivity", + "Server", + "Set-Cookie", + "Set-Cookie2", + "SetProfile", + "SoapAction", + "Solicitation", + "Status-URI", + "Strict-Transport-Security", + "SubOK", + "Subject", + "Subject", + "Subst", + "Summary", + "Supersedes", + "Supersedes", + "Surrogate-Capability", + "Surrogate-Control", + "TCN", + "TE", + "TTL", + "Timeout", + "Title", + "To", + "Topic", + "Trailer", + "Transfer-Encoding", + "UA-Color", + "UA-Media", + "UA-Pixels", + "UA-Resolution", + "UA-Windowpixels", + "URI", + "Upgrade", + "Urgency", + "User-Agent", + "User-Agent", + "VBR-Info", + "Variant-Vary", + "Vary", + "Version", + "Via", + "WWW-Authenticate", + "Want-Digest", + "Warning", + "X-Archived-At", + "X-Archived-At", + "X-Content-Type-Options", + "X-Device-Accept", + "X-Device-Accept-Charset", + "X-Device-Accept-Encoding", + "X-Device-Accept-Language", + "X-Device-User-Agent", + "X-Frame-Options", + "X-Mittente", + "X-PGP-Sig", + "X-Ricevuta", + "X-Riferimento-Message-ID", + "X-TipoRicevuta", + "X-Trasporto", + "X-VerificaSicurezza", + "X400-Content-Identifier", + "X400-Content-Return", + "X400-Content-Type", + "X400-MTS-Identifier", + "X400-Originator", + "X400-Received", + "X400-Recipients", + "X400-Trace", + "Xref", + NULL +}; + +static uint64_t +calc_hash(const char *key, int *lenp) { + int len = 0; + int klen = *lenp; + uint64_t h = 0; + bool special = false; + + if (NULL != key) { + const uint8_t *k = (const uint8_t*)key; + + for (; len < klen; k++) { + // narrow to most used range of 0x4D (77) in size + if (*k < 0x2D || 0x7A < *k) { + special = true; + } + // fast, just spread it out + h = 77 * h + ((*k | 0x20) - 0x2D); + len++; + } + } + if (special) { + *lenp = -len; + } else { + *lenp = len; + } + return h; +} + +static Slot* +get_bucketp(uint64_t h) { + return key_cache.buckets + (BUCKET_MASK & (h ^ (h << 5) ^ (h >> 7))); +} + +static void +key_set(const char *key) { + int len = strlen(key); + int64_t h = calc_hash(key, &len); + Slot *bucket = get_bucketp(h); + Slot s; + + if (NULL != (s = (Slot)malloc(sizeof(struct _Slot)))) { + s->hash = h; + s->klen = len; + s->key = key; + s->next = *bucket; + *bucket = s; + } +} + +void +http_init() { + const char **kp = header_keys; + + memset(&key_cache, 0, sizeof(struct _Cache)); + for (; NULL != *kp; kp++) { + key_set(*kp); + } +} + +void +http_cleanup() { + Slot *sp = key_cache.buckets; + Slot s; + Slot n; + + for (int i = BUCKET_SIZE; 0 < i; i--, sp++) { + for (s = *sp; NULL != s; s = n) { + n = s->next; + free(s); + } + *sp = NULL; + } +} + +void +http_header_ok(const char *key, int klen, const char *value, int vlen) { + int len = klen; + int64_t h = calc_hash(key, &len); + Slot *bucket = get_bucketp(h); + Slot s; + bool found = false; + + for (s = *bucket; NULL != s; s = s->next) { + if (h == (int64_t)s->hash && len == (int)s->klen && + ((0 <= len && len <= MAX_KEY_UNIQ) || 0 == strncasecmp(s->key, key, klen))) { + found = true; + break; + } + } + if (!found) { + char buf[256]; + + if ((int)sizeof(buf) <= klen) { + klen = sizeof(buf) - 1; + } + strncpy(buf, key, klen); + buf[klen] = '\0'; + rb_raise(rb_eArgError, "%s is not a valid HTTP header key.", buf); + } + // Now check the value. + found = false; // reuse as indicator for in a quoted string + for (; 0 < vlen; vlen--, value++) { + if ('o' != header_value_chars[(uint8_t)*value]) { + rb_raise(rb_eArgError, "%02x is not a valid HTTP header value character.", *value); + } + if ('"' == *value) { + found = !found; + } + } + if (found) { + rb_raise(rb_eArgError, "HTTP header has unmatched quote."); + } +} + +const char* +http_code_message(int code) { + const char *msg = ""; + + switch (code) { + case 100: msg = "Continue"; break; + case 101: msg = "Switching Protocols"; break; + case 102: msg = "Processing"; break; + case 200: msg = "OK"; break; + case 201: msg = "Created"; break; + case 202: msg = "Accepted"; break; + case 203: msg = "Non-authoritative Information"; break; + case 204: msg = "No Content"; break; + case 205: msg = "Reset Content"; break; + case 206: msg = "Partial Content"; break; + case 207: msg = "Multi-Status"; break; + case 208: msg = "Already Reported"; break; + case 226: msg = "IM Used"; break; + case 300: msg = "Multiple Choices"; break; + case 301: msg = "Moved Permanently"; break; + case 302: msg = "Found"; break; + case 303: msg = "See Other"; break; + case 304: msg = "Not Modified"; break; + case 305: msg = "Use Proxy"; break; + case 307: msg = "Temporary Redirect"; break; + case 308: msg = "Permanent Redirect"; break; + case 400: msg = "Bad Request"; break; + case 401: msg = "Unauthorized"; break; + case 402: msg = "Payment Required"; break; + case 403: msg = "Forbidden"; break; + case 404: msg = "Not Found"; break; + case 405: msg = "Method Not Allowed"; break; + case 406: msg = "Not Acceptable"; break; + case 407: msg = "Proxy Authentication Required"; break; + case 408: msg = "Request Timeout"; break; + case 409: msg = "Conflict"; break; + case 410: msg = "Gone"; break; + case 411: msg = "Length Required"; break; + case 412: msg = "Precondition Failed"; break; + case 413: msg = "Payload Too Large"; break; + case 414: msg = "Request-URI Too Long"; break; + case 415: msg = "Unsupported Media Type"; break; + case 416: msg = "Requested Range Not Satisfiable"; break; + case 417: msg = "Expectation Failed"; break; + case 418: msg = "I'm a teapot"; break; + case 421: msg = "Misdirected Request"; break; + case 422: msg = "Unprocessable Entity"; break; + case 423: msg = "Locked"; break; + case 424: msg = "Failed Dependency"; break; + case 426: msg = "Upgrade Required"; break; + case 428: msg = "Precondition Required"; break; + case 429: msg = "Too Many Requests"; break; + case 431: msg = "Request Header Fields Too Large"; break; + case 444: msg = "Connection Closed Without Response"; break; + case 451: msg = "Unavailable For Legal Reasons"; break; + case 499: msg = "Client Closed Request"; break; + case 500: msg = "Internal Server Error"; break; + case 501: msg = "Not Implemented"; break; + case 502: msg = "Bad Gateway"; break; + case 503: msg = "Service Unavailable"; break; + case 504: msg = "Gateway Timeout"; break; + case 505: msg = "HTTP Version Not Supported"; break; + case 506: msg = "Variant Also Negotiates"; break; + case 507: msg = "Insufficient Storage"; break; + case 508: msg = "Loop Detected"; break; + case 510: msg = "Not Extended"; break; + case 511: msg = "Network Authentication Required"; break; + case 599: msg = "Network Connect Timeout Error"; break; + default: break; + } + return msg; +} diff --git a/ext/agoo/http.h b/ext/agoo/http.h new file mode 100644 index 0000000..05d9b40 --- /dev/null +++ b/ext/agoo/http.h @@ -0,0 +1,13 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_HTTP_H__ +#define __AGOO_HTTP_H__ + +#include + +extern void http_init(); +extern void http_header_ok(const char *key, int klen, const char *value, int vlen); + +extern const char* http_code_message(int code); + +#endif // __AGOO_HTTP_H__ diff --git a/ext/agoo/log.c b/ext/agoo/log.c new file mode 100644 index 0000000..3ee0f2e --- /dev/null +++ b/ext/agoo/log.c @@ -0,0 +1,497 @@ +// Copyright 2018 by Peter Ohler, All Rights Reserved + +#include +#include +#include +#include +#include +#include +#include + +#include "dtime.h" +#include "log.h" + +// lower gives faster response but burns more CPU. This is a reasonable compromise. +#define RETRY_SECS 0.0001 +#define NOT_WAITING 0 +#define WAITING 1 +#define NOTIFIED 2 +#define RESET_COLOR "\033[0m" +#define RESET_SIZE 4 + +static const char log_name[] = "agoo.log"; +static const char log_prefix[] = "agoo.log."; +static const char log_format[] = "%s/agoo.log.%d"; + +static struct _Color colors[] = { + { .name = "black", .ansi = "\033[30;1m" }, + { .name = "red", .ansi = "\033[31;1m" }, + { .name = "green", .ansi = "\033[32;1m" }, + { .name = "yellow", .ansi = "\033[33;1m" }, + { .name = "blue", .ansi = "\033[34;1m" }, + { .name = "magenta", .ansi = "\033[35;1m" }, + { .name = "cyan", .ansi = "\033[36;1m" }, + { .name = "white", .ansi = "\033[37;1m" }, + { .name = "gray", .ansi = "\033[37m" }, + { .name = "dark_red", .ansi = "\033[31m" }, + { .name = "dark_green", .ansi = "\033[32m" }, + { .name = "brown", .ansi = "\033[33m" }, + { .name = "dark_blue", .ansi = "\033[34m" }, + { .name = "purple", .ansi = "\033[35m" }, + { .name = "dark_cyan", .ansi = "\033[36m" }, + { .name = NULL, .ansi = NULL } +}; + +static const char level_chars[] = { 'F', 'E', 'W', 'I', 'D', '?' }; + +static Color +find_color(const char *name) { + if (NULL != name) { + for (Color c = colors; NULL != c->name; c++) { + if (0 == strcasecmp(c->name, name)) { + return c; + } + } + } + return NULL; +} + +static bool +log_queue_empty(Log log) { + LogEntry head = atomic_load(&log->head); + LogEntry next = head + 1; + + if (log->end <= next) { + next = log->q; + } + if (!head->ready && log->tail == next) { + return true; + } + return false; +} + +static LogEntry +log_queue_pop(Log log, double timeout) { + LogEntry e = log->head; + LogEntry next; + + if (e->ready) { + return e; + } + next = log->head + 1; + if (log->end <= next) { + next = log->q; + } + // If the next is the tail then wait for something to be appended. + for (int cnt = (int)(timeout / RETRY_SECS); atomic_load(&log->tail) == next; cnt--) { + // TBD poll would be better + if (cnt <= 0) { + return NULL; + } + dsleep(RETRY_SECS); + } + atomic_store(&log->head, next); + + return log->head; +} + + +static int +jwrite(LogEntry e, FILE *file) { + // TBD make e->what JSON friendly + return fprintf(file, "{\"when\":%lld.%09lld,\"where\":\"%s\",\"level\":%d,\"what\":\"%s\"}\n", + (long long)(e->when / 1000000000LL), + (long long)(e->when % 1000000000LL), + e->cat->label, + e->cat->level, + (NULL == e->whatp ? e->what : e->whatp)); +} + +//I 2015/05/23 11:22:33.123456789 label: The contents of the what field. +static int +classic_write(Log log, LogEntry e, FILE *file) { + time_t t = (time_t)(e->when / 1000000000LL); + int hour = 0; + int min = 0; + int sec = 0; + long long frac = (long long)e->when % 1000000000LL; + char levelc = level_chars[e->cat->level]; + int cnt = 0; + + t += log->zone; + if (log->day_start <= t && t < log->day_end) { + t -= log->day_start; + hour = t / 3600; + min = t % 3600 / 60; + sec = t % 60; + } else { + struct tm *tm = gmtime(&t); + + hour = tm->tm_hour; + min = tm->tm_min; + sec = tm->tm_sec; + sprintf(log->day_buf, "%04d/%02d/%02d ", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday); + log->day_start = t - (hour * 3600 + min * 60 + sec); + log->day_end = log->day_start + 86400; + } + if (log->colorize) { + cnt = fprintf(file, "%s%c %s%02d:%02d:%02d.%09lld %s: %s%s\n", + e->cat->color->ansi, levelc, log->day_buf, hour, min, sec, frac, + e->cat->label, + (NULL == e->whatp ? e->what : e->whatp), + RESET_COLOR); + } else { + cnt += fprintf(file, "%c %s%02d:%02d:%02d.%09lld %s: %s\n", + levelc, log->day_buf, hour, min, sec, frac, + e->cat->label, + (NULL == e->whatp ? e->what : e->whatp)); + } + return cnt; +} + +// Remove all file with sequence numbers higher than max_files. max_files is +// max number of archived version. It does not include the primary. +static void +remove_old_logs(Log log) { + struct dirent *de; + long seq; + char *end; + char path[1024]; + DIR *dir = opendir(log->dir); + + while (NULL != (de = readdir(dir))) { + if ('.' == *de->d_name || '\0' == *de->d_name) { + continue; + } + if (0 != strncmp(log_prefix, de->d_name, sizeof(log_prefix) - 1)) { + continue; + } + // Don't remove the primary log file. + if (0 == strcmp(log_name, de->d_name)) { + continue; + } + seq = strtol(de->d_name + sizeof(log_prefix) - 1, &end, 10); + if (log->max_files < seq) { + snprintf(path, sizeof(path), "%s/%s", log->dir, de->d_name); + remove(path); + } + } + closedir(dir); +} + +static int +rotate(Err err, Log log) { + char from[1024]; + char to[1024]; + + if (NULL != log->file) { + fclose(log->file); + log->file = NULL; + } + for (int seq = log->max_files; 0 < seq; seq--) { + snprintf(to, sizeof(to), log_format, log->dir, seq + 1); + snprintf(from, sizeof(from), log_format, log->dir, seq); + rename(from, to); + } + snprintf(to, sizeof(to), log_format, log->dir, 1); + snprintf(from, sizeof(from), "%s/%s", log->dir, log_name); + rename(from, to); + + log->file = fopen(from, "w"); + log->size = 0; + + remove_old_logs(log); + + return ERR_OK; +} + +static void* +loop(void *ctx) { + Log log = (Log)ctx; + LogEntry e; + + while (!log->done || !log_queue_empty(log)) { + if (NULL != (e = log_queue_pop(log, 0.5))) { + if (log->console) { + if (log->classic) { + classic_write(log, e, stdout); + } else { + jwrite(e, stdout); + } + } + if (NULL != log->file) { + if (log->classic) { + log->size += classic_write(log, e, log->file); + } else { + log->size += jwrite(e, log->file); + } + if (log->max_size <= log->size) { + rotate(NULL, log); + } + } + if (NULL != e->whatp) { + free(e->whatp); + } + e->ready = false; + } + } + return NULL; +} + +bool +log_flush(Log log, double timeout) { + timeout += dtime(); + + while (!log->done && !log_queue_empty(log)) { + if (timeout < dtime()) { + return false; + } + dsleep(0.001); + } + if (NULL != log->file) { + fflush(log->file); + } + return true; +} + +static int +configure(Err err, Log log, VALUE options) { + if (Qnil != options) { + VALUE v; + + if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("log_dir"))))) { + rb_check_type(v, T_STRING); + strncpy(log->dir, StringValuePtr(v), sizeof(log->dir)); + log->dir[sizeof(log->dir) - 1] = '\0'; + } + if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("log_max_files"))))) { + int max = FIX2INT(v); + + if (1 <= max || max < 100) { + log->max_files = max; + } else { + rb_raise(rb_eArgError, "log_max_files must be between 1 and 100."); + } + } + if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("log_max_size"))))) { + int max = FIX2INT(v); + + if (1 <= max) { + log->max_size = max; + } else { + rb_raise(rb_eArgError, "log_max_size must be 1 or more."); + } + } + if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("log_console"))))) { + log->console = (Qtrue == v); + } + if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("log_classic"))))) { + log->classic = (Qtrue == v); + } + if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("log_colorize"))))) { + log->colorize = (Qtrue == v); + } + if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("log_states"))))) { + if (T_HASH == rb_type(v)) { + LogCat cat = log->cats; + VALUE cv; + + for (; NULL != cat; cat = cat->next) { + if (Qnil != (cv = rb_hash_lookup(v, ID2SYM(rb_intern(cat->label))))) { + if (Qtrue == cv) { + cat->on = true; + } else if (Qfalse == cv) { + cat->on = false; + } + } + } + } else { + rb_raise(rb_eArgError, "log_states must be a Hash."); + } + } + } + return ERR_OK; +} + +static int +open_log_file(Err err, Log log) { + char path[1024]; + + snprintf(path, sizeof(path), "%s/%s", log->dir, log_name); + + log->file = fopen(path, "a"); + if (NULL == log->file) { + return err_no(err, "failed to open '%s'.", path); + } + log->size = ftell(log->file); + if (log->max_size <= log->size) { + return rotate(err, log); + } + return ERR_OK; +} + +int +log_init(Err err, Log log, VALUE cfg) { + time_t t = time(NULL); + struct tm *tm = localtime(&t); + int qsize = 1024; + + //log->cats = NULL; done outside of here + *log->dir = '\0'; + log->file = NULL; + log->max_files = 3; + log->max_size = 100000000; // 100M + log->size = 0; + log->done = false; + log->console = true; + log->classic = true; + log->colorize = true; + log->zone = (int64_t)(timegm(tm) - t); + log->day_start = 0; + log->day_end = 0; + *log->day_buf = '\0'; + log->thread = 0; + + if (ERR_OK != configure(err, log, cfg)) { + return err->code; + } + if ('\0' != *log->dir) { + if (0 != mkdir(log->dir, 0770) && EEXIST != errno) { + return err_no(err, "Failed to create '%s'.", log->dir); + } + if (ERR_OK != open_log_file(err, log)) { + return err->code; + } + } + log->q = (LogEntry)malloc(sizeof(struct _LogEntry) * qsize); + log->end = log->q + qsize; + + memset(log->q, 0, sizeof(struct _LogEntry) * qsize); + log->head = log->q; + log->tail = log->q + 1; + atomic_flag_clear(&log->push_lock); + log->wait_state = NOT_WAITING; + // Create when/if needed. + log->rsock = 0; + log->wsock = 0; + + pthread_create(&log->thread, NULL, loop, log); + + return ERR_OK; +} + +void +log_close(Log log) { + log->done = true; + // TBD wake up loop like push does + log_cat_on(log, NULL, false); + if (0 != log->thread) { + pthread_join(log->thread, NULL); + log->thread = 0; + } + if (NULL != log->file) { + fclose(log->file); + log->file = NULL; + } + free(log->q); + log->q = NULL; + log->end = NULL; + if (0 < log->wsock) { + close(log->wsock); + } + if (0 < log->rsock) { + close(log->rsock); + } +} + +void +log_cat_reg(Log log, LogCat cat, const char *label, LogLevel level, const char *color, bool on) { + cat->log = log; + strncpy(cat->label, label, sizeof(cat->label)); + cat->label[sizeof(cat->label) - 1] = '\0'; + cat->level = level; + cat->color = find_color(color); + cat->on = on; + cat->next = log->cats; + log->cats = cat; +} + +void +log_cat_on(Log log, const char *label, bool on) { + LogCat cat; + + for (cat = log->cats; NULL != cat; cat = cat->next) { + if (NULL == label || 0 == strcasecmp(label, cat->label)) { + cat->on = on; + break; + } + } +} + +LogCat +log_cat_find(Log log, const char *label) { + LogCat cat; + + for (cat = log->cats; NULL != cat; cat = cat->next) { + if (0 == strcasecmp(label, cat->label)) { + return cat; + } + } + return NULL; +} + +void +log_catv(LogCat cat, const char *fmt, va_list ap) { + if (cat->on && !cat->log->done) { + Log log = cat->log; + struct timespec ts; + LogEntry e; + LogEntry tail; + int cnt; + va_list ap2; + + va_copy(ap2, ap); + + while (atomic_flag_test_and_set(&log->push_lock)) { + dsleep(RETRY_SECS); + } + // Wait for head to move on. + while (atomic_load(&log->head) == log->tail) { + dsleep(RETRY_SECS); + } + // TBD fill in the entry at tail + clock_gettime(CLOCK_REALTIME, &ts); + e = log->tail; + e->cat = cat; + e->when = (int64_t)ts.tv_sec * 1000000000LL + (int64_t)ts.tv_nsec; + e->whatp = NULL; + if ((int)sizeof(e->what) <= (cnt = vsnprintf(e->what, sizeof(e->what), fmt, ap))) { + e->whatp = (char*)malloc(cnt + 1); + + if (NULL != e->whatp) { + vsnprintf(e->whatp, cnt + 1, fmt, ap2); + } + } + tail = log->tail + 1; + if (log->end <= tail) { + tail = log->q; + } + atomic_store(&log->tail, tail); + atomic_flag_clear(&log->push_lock); + va_end(ap2); + + if (0 != log->wsock && WAITING == atomic_load(&log->wait_state)) { + if (write(log->wsock, ".", 1)) {} + atomic_store(&log->wait_state, NOTIFIED); + } + } +} + +void +log_cat(LogCat cat, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + log_catv(cat, fmt, ap); + va_end(ap); +} diff --git a/ext/agoo/log.h b/ext/agoo/log.h new file mode 100644 index 0000000..0d85401 --- /dev/null +++ b/ext/agoo/log.h @@ -0,0 +1,106 @@ +// Copyright 2018 by Peter Ohler, All Rights Reserved + +#ifndef __AGOO_LOG_H__ +#define __AGOO_LOG_H__ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "err.h" + +typedef enum { + FATAL = 0, + ERROR = 1, + WARN = 2, + INFO = 3, + DEBUG = 4, + UNKNOWN = 5, +} LogLevel; + +typedef struct _Color { + const char *name; + const char *ansi; +} *Color; + +#define BLACK "black" +#define RED "red" +#define GREEN "green" +#define YELLOW "yellow" +#define BLUE "blue" +#define MAGENTA "magenta" +#define CYAN "cyan" +#define WHITE "white" +#define GRAY "gray" +#define DARK_RED "dark_red" +#define DARK_GREEN "dark_green" +#define BROWN "brown" +#define DARK_BLUE "dark_blue" +#define PURPLE "purple" +#define DARK_CYAN "dark_cyan" + +typedef struct _Log *Log; + +typedef struct _LogCat { + struct _LogCat *next; + Log log; + char label[32]; + Color color; + int level; + bool on; +} *LogCat; + +typedef struct _LogEntry { + LogCat cat; + int64_t when; // nano UTC + char *whatp; + char what[104]; + volatile bool ready; +} *LogEntry; + +struct _Log { + LogCat cats; + char dir[1024]; + FILE *file; // current output file + int max_files; + int max_size; + long size; // current file size + pthread_t thread; + volatile bool done; + bool console; // if true print log message to stdout + bool classic; // classic in stdout + bool colorize; // color in stdout + int zone; // timezone offset from GMT in seconds + int64_t day_start; + int64_t day_end; + char day_buf[16]; + + LogEntry q; + LogEntry end; + _Atomic(LogEntry) head; + _Atomic(LogEntry) tail; + atomic_flag push_lock; + atomic_int wait_state; + int rsock; + int wsock; +}; + +extern int log_init(Err err, Log log, VALUE cfg); +extern void log_close(Log log); +extern bool log_flush(Log log, double timeout); + +extern void log_cat_reg(Log log, LogCat cat, const char *label, LogLevel level, const char *color, bool on); +extern void log_cat_on(Log log, const char *label, bool on); +extern LogCat log_cat_find(Log log, const char *label); + +// Function to call to make a log entry. +extern void log_cat(LogCat cat, const char *fmt, ...); +extern void log_catv(LogCat cat, const char *fmt, va_list ap); + +#endif /* __AGOO_LOG_H__ */ diff --git a/ext/agoo/log_queue.h b/ext/agoo/log_queue.h new file mode 100644 index 0000000..75e2137 --- /dev/null +++ b/ext/agoo/log_queue.h @@ -0,0 +1,30 @@ +// Copyright 2018 by Peter Ohler, All Rights Reserved + +#ifndef __AGOO_LOG_QUEUE_H__ +#define __AGOO_LOG_QUEUE_H__ + +#include +#include + +#include "val.h" + +typedef struct _LogQueue { + opoMsg *q; + opoMsg *end; + _Atomic(opoMsg*) head; + _Atomic(opoMsg*) tail; + atomic_flag pop_lock; // set to true when ppo in progress + atomic_int wait_state; + int rsock; + int wsock; +} *LogQueue; + +extern void queue_init(Queue q, size_t qsize); + +extern void queue_cleanup(Queue q); +extern void queue_push(Queue q, opoMsg item); +extern opoMsg queue_pop(Queue q, double timeout); +extern bool queue_empty(Queue q); +extern int queue_count(Queue q); + +#endif /* __AGOO_LOG_QUEUE_H__ */ diff --git a/ext/agoo/page.c b/ext/agoo/page.c new file mode 100644 index 0000000..125c41c --- /dev/null +++ b/ext/agoo/page.c @@ -0,0 +1,342 @@ +// Copyright 2016, 2018 by Peter Ohler, All Rights Reserved + +#include +#include +#include +#include + +#include "dtime.h" +#include "page.h" + +#define PAGE_RECHECK_TIME 5.0 +#define MAX_KEY_UNIQ 9 + +typedef struct _Mime { + const char *suffix; + const char *type; +} *Mime; + +static struct _Mime mime_map[] = { + { "css", "text/css" }, + { "eot", "application/vnd.ms-fontobject" }, + { "es5", "application/javascript" }, + { "es6", "application/javascript" }, + { "gif", "image/gif" }, + { "html", "text/html" }, + { "ico", "image/x-icon" }, + { "jpeg", "image/jpeg" }, + { "jpg", "image/jpeg" }, + { "js", "application/javascript" }, + { "json", "application/json" }, + { "png", "image/png" }, + { "sse", "text/plain" }, + { "svg", "image/svg+xml" }, + { "ttf", "application/font-sfnt" }, + { "txt", "text/plain" }, + { "woff", "application/font-woff" }, + { "woff2", "font/woff2" }, + { NULL, NULL } +}; + +static const char page_fmt[] = "HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %ld\r\n\r\n"; + +static uint64_t +calc_hash(const char *key, int *lenp) { + int len = 0; + int klen = *lenp; + uint64_t h = 0; + bool special = false; + const uint8_t *k = (const uint8_t*)key; + + for (; len < klen; k++) { + // narrow to most used range of 0x4D (77) in size + if (*k < 0x2D || 0x7A < *k) { + special = true; + } + // fast, just spread it out + h = 77 * h + (*k - 0x2D); + len++; + } + + if (special) { + *lenp = -len; + } else { + *lenp = len; + } + return h; +} + +// Buckets are a twist on the hash to mix it up a bit. Odd shifts and XORs. +static Slot* +get_bucketp(Cache cache, uint64_t h) { + return cache->buckets + (PAGE_BUCKET_MASK & (h ^ (h << 5) ^ (h >> 7))); +} + +void +cache_init(Cache cache) { + memset(cache, 0, sizeof(struct _Cache)); +} + +void +cache_destroy(Cache cache) { + Slot *sp = cache->buckets; + Slot s; + Slot n; + + for (int i = PAGE_BUCKET_SIZE; 0 < i; i--, sp++) { + for (s = *sp; NULL != s; s = n) { + n = s->next; + free(s); + } + *sp = NULL; + } + free(cache); +} + +Page +cache_get(Cache cache, const char *key, int klen) { + int len = klen; + int64_t h = calc_hash(key, &len); + Slot *bucket = get_bucketp(cache, h); + Slot s; + Page v = NULL; + + for (s = *bucket; NULL != s; s = s->next) { + if (h == (int64_t)s->hash && len == (int)s->klen && + ((0 <= len && len <= MAX_KEY_UNIQ) || 0 == strncmp(s->key, key, klen))) { + v = s->value; + break; + } + } + return v; +} + +Page +cache_set(Cache cache, const char *key, int klen, Page value) { + int len = klen; + int64_t h = calc_hash(key, &len); + Slot *bucket = get_bucketp(cache, h); + Slot s; + Page old = NULL; + + if (MAX_KEY_LEN < len) { + return value; + } + for (s = *bucket; NULL != s; s = s->next) { + if (h == (int64_t)s->hash && len == s->klen && + ((0 <= len && len <= MAX_KEY_UNIQ) || 0 == strcmp(s->key, key))) { + if (h == (int64_t)s->hash && len == s->klen && + ((0 <= len && len <= MAX_KEY_UNIQ) || 0 == strcmp(s->key, key))) { + old = s->value; + // replace + s->value = value; + + return old; + } + } + } + if (NULL == (s = (Slot)malloc(sizeof(struct _Slot)))) { + return value; + } + s->hash = h; + s->klen = len; + if (NULL == key) { + *s->key = '\0'; + } else { + strcpy(s->key, key); + } + s->value = value; + s->next = *bucket; + *bucket = s; + + return old; +} + +// The page resp contents point to the page resp msg to save memory and reduce +// allocations. +Page +page_create(const char *path) { + Page p = (Page)malloc(sizeof(struct _Page)); + + if (NULL != p) { + p->resp = NULL; + if (NULL == path) { + p->path = NULL; + } else { + p->path = strdup(path); + } + p->mtime = 0; + p->last_check = 0.0; + } + return p; +} + +void +page_destroy(Page p) { + if (NULL != p->resp) { + text_release(p->resp); + p->resp = NULL; + } + free(p->path); + free(p); +} + +static bool +update_contents(Page p) { + const char *mime = NULL; + int plen = strlen(p->path); + const char *suffix = p->path + plen - 1; + FILE *f; + long size; + struct stat fattr; + long msize; + char *msg; + int cnt; + struct stat fs; + + for (; '.' != *suffix; suffix--) { + if (suffix <= p->path) { + suffix = NULL; + break; + } + } + if (NULL != suffix) { + Mime m; + + suffix++; + for (m = mime_map; NULL != m->suffix; m++) { + if (0 == strcasecmp(m->suffix, suffix)) { + break; + } + } + if (NULL == m) { + mime = "text/plain"; + } else { + mime = m->type; + } + } else { + mime = "text/plain"; + } + + f = fopen(p->path, "rb"); + // On linux a directory is opened by fopen (sometimes? all the time?) so + // fstat is called to get the file mode and verify it is a regular file or + // a symlink. + if (NULL != f) { + fstat(fileno(f), &fs); + if (!S_ISREG(fs.st_mode) && !S_ISLNK(fs.st_mode)) { + fclose(f); + f = NULL; + } + } + if (NULL == f) { + // If not found how about with a /index.html added? + if (NULL == suffix) { + char path[1024]; + int cnt; + + if ('/' == p->path[plen - 1]) { + cnt = snprintf(path, sizeof(path), "%sindex.html", p->path); + } else { + cnt = snprintf(path, sizeof(path), "%s/index.html", p->path); + } + if ((int)sizeof(path) < cnt) { + return false; + } + if (NULL == (f = fopen(path, "rb"))) { + return false; + } + } else { + return false; + } + } + if (0 != fseek(f, 0, SEEK_END)) { + fclose(f); + return false; + } + if (0 >= (size = ftell(f))) { + fclose(f); + return false; + } + rewind(f); + // Format size plus space for the length, the mime type, and some + // padding. Then add the content length. + msize = sizeof(page_fmt) + 60 + size; + if (NULL == (msg = (char*)malloc(msize))) { + return false; + } + cnt = sprintf(msg, page_fmt, mime, size); + + msize = cnt + size; + if (size != (long)fread(msg + cnt, 1, size, f)) { + fclose(f); + free(msg); + return false; + } + fclose(f); + msg[msize] = '\0'; + if (0 == stat(p->path, &fattr)) { + p->mtime = fattr.st_mtime; + } else { + p->mtime = 0; + } + if (NULL != p->resp) { + text_release(p->resp); + p->resp = NULL; + } + p->resp = text_create(msg, msize); + text_ref(p->resp); + p->last_check = dtime(); + + return true; +} + +Page +page_get(Err err, Cache cache, const char *dir, const char *path, int plen) { + Page page; + + if (NULL == (page = cache_get(cache, path, plen))) { + Page old; + char full_path[2048]; + char *s = stpcpy(full_path, dir); + + if ('/' != *dir && '/' != *path) { + *s++ = '/'; + } + if ((int)sizeof(full_path) <= plen + (s - full_path)) { + err_set(err, ERR_MEMORY, "Failed to allocate memory for page path."); + return NULL; + } + strncpy(s, path, plen); + s[plen] = '\0'; + if (NULL == (page = page_create(full_path))) { + err_set(err, ERR_MEMORY, "Failed to allocate memory for Page."); + return NULL; + } + if (!update_contents(page) || NULL == page->resp) { + page_destroy(page); + err_set(err, ERR_NOT_FOUND, "not found."); + return NULL; + } + if (NULL != (old = cache_set(cache, path, plen, page))) { + page_destroy(old); + } + } else { + double now = dtime(); + + if (page->last_check + PAGE_RECHECK_TIME < now) { + struct stat fattr; + + if (0 == stat(page->path, &fattr) && page->mtime != fattr.st_mtime) { + update_contents(page); + if (NULL == page->resp) { + page_destroy(page); + err_set(err, ERR_NOT_FOUND, "not found."); + return NULL; + } + } + page->last_check = now; + } + } + return page; +} diff --git a/ext/agoo/page.h b/ext/agoo/page.h new file mode 100644 index 0000000..cbd8bd3 --- /dev/null +++ b/ext/agoo/page.h @@ -0,0 +1,39 @@ +// Copyright 2016, 2018 by Peter Ohler, All Rights Reserved + +#ifndef __AGOO_PAGE_H__ +#define __AGOO_PAGE_H__ + +#include +#include + +#include "err.h" +#include "text.h" + +#define MAX_KEY_LEN 1024 +#define PAGE_BUCKET_SIZE 1024 +#define PAGE_BUCKET_MASK 1023 + +typedef struct _Page { + Text resp; + char *path; + time_t mtime; + double last_check; +} *Page; + +typedef struct _Slot { + struct _Slot *next; + char key[MAX_KEY_LEN + 1]; + Page value; + uint64_t hash; + int klen; +} *Slot; + +typedef struct _Cache { + Slot buckets[PAGE_BUCKET_SIZE]; +} *Cache; + +extern void cache_init(Cache cache); +extern void page_destroy(Page p); +extern Page page_get(Err err, Cache cache, const char *dir, const char *path, int plen); + +#endif /* __AGOO_PAGE_H__ */ diff --git a/ext/agoo/queue.c b/ext/agoo/queue.c new file mode 100644 index 0000000..c79c351 --- /dev/null +++ b/ext/agoo/queue.c @@ -0,0 +1,191 @@ +// Copyright 2015, 2016, 2018 by Peter Ohler, All Rights Reserved + +#include +#include +#include +#include +#include + +#include "dtime.h" +#include "queue.h" + +// lower gives faster response but burns more CPU. This is a reasonable compromise. +#define RETRY_SECS 0.0001 + +#define NOT_WAITING 0 +#define WAITING 1 +#define NOTIFIED 2 + +// head and tail both increment and wrap. +// tail points to next open space. +// When head == tail the queue is full. This happens when tail catches up with head. +// + +void +queue_init(Queue q, size_t qsize) { + queue_multi_init(q, qsize, false, false); +} + +void +queue_multi_init(Queue q, size_t qsize, bool multi_push, bool multi_pop) { + if (qsize < 4) { + qsize = 4; + } + q->q = (QItem*)malloc(sizeof(QItem) * qsize); + q->end = q->q + qsize; + + memset(q->q, 0, sizeof(QItem) * qsize); + q->head = q->q; + q->tail = q->q + 1; + atomic_flag_clear(&q->push_lock); + atomic_flag_clear(&q->pop_lock); + q->wait_state = 0; + q->multi_push = multi_push; + q->multi_pop = multi_pop; + // Create when/if needed. + q->rsock = 0; + q->wsock = 0; +} + +void +queue_cleanup(Queue q) { + free(q->q); + q->q = NULL; + q->end = NULL; + if (0 < q->wsock) { + close(q->wsock); + } + if (0 < q->rsock) { + close(q->rsock); + } +} + +void +queue_push(Queue q, QItem item) { + QItem *tail; + + if (q->multi_push) { + while (atomic_flag_test_and_set(&q->push_lock)) { + dsleep(RETRY_SECS); + } + } + // Wait for head to move on. + while (atomic_load(&q->head) == q->tail) { + dsleep(RETRY_SECS); + } + *q->tail = item; + tail = q->tail + 1; + + if (q->end <= tail) { + tail = q->q; + } + atomic_store(&q->tail, tail); + if (q->multi_push) { + atomic_flag_clear(&q->push_lock); + } + if (0 != q->wsock && WAITING == atomic_load(&q->wait_state)) { + if (write(q->wsock, ".", 1)) {} + atomic_store(&q->wait_state, NOTIFIED); + } +} + +void +queue_wakeup(Queue q) { + if (0 != q->wsock) { + if (write(q->wsock, ".", 1)) {} + } +} + +QItem +queue_pop(Queue q, double timeout) { + QItem item; + QItem *next; + + if (q->multi_pop) { + while (atomic_flag_test_and_set(&q->pop_lock)) { + dsleep(RETRY_SECS); + } + } + item = *q->head; + + if (NULL != item) { + *q->head = NULL; + if (q->multi_pop) { + atomic_flag_clear(&q->pop_lock); + } + return item; + } + next = q->head + 1; + + if (q->end <= next) { + next = q->q; + } + // If the next is the tail then wait for something to be appended. + for (int cnt = (int)(timeout / RETRY_SECS); atomic_load(&q->tail) == next; cnt--) { + if (cnt <= 0) { + if (q->multi_pop) { + atomic_flag_clear(&q->pop_lock); + } + return NULL; + } + dsleep(RETRY_SECS); + } + atomic_store(&q->head, next); + + item = *q->head; + *q->head = NULL; + if (q->multi_pop) { + atomic_flag_clear(&q->pop_lock); + } + return item; +} + +// Called by the popper usually. +bool +queue_empty(Queue q) { + QItem *head = atomic_load(&q->head); + QItem *next = head + 1; + + if (q->end <= next) { + next = q->q; + } + if (NULL == *head && q->tail == next) { + return true; + } + return false; +} + +int +queue_listen(Queue q) { + if (0 == q->rsock) { + int fd[2]; + + if (0 == pipe(fd)) { + fcntl(fd[0], F_SETFL, O_NONBLOCK); + fcntl(fd[1], F_SETFL, O_NONBLOCK); + q->rsock = fd[0]; + q->wsock = fd[1]; + } + } + atomic_store(&q->wait_state, WAITING); + + return q->rsock; +} + +void +queue_release(Queue q) { + char buf[8]; + + // clear pipe + while (0 < read(q->rsock, buf, sizeof(buf))) { + } + atomic_store(&q->wait_state, NOT_WAITING); +} + +int +queue_count(Queue q) { + int size = q->end - q->q; + + return (q->tail - q->head + size) % size; +} + diff --git a/ext/agoo/queue.h b/ext/agoo/queue.h new file mode 100644 index 0000000..bc06af3 --- /dev/null +++ b/ext/agoo/queue.h @@ -0,0 +1,39 @@ +// Copyright 2015, 2016, 2018 by Peter Ohler, All Rights Reserved + +#ifndef __AGOO_QUEUE_H__ +#define __AGOO_QUEUE_H__ + +#include +#include + +typedef void *QItem; + +typedef struct _Queue { + QItem *q; + QItem *end; + _Atomic(QItem*) head; + _Atomic(QItem*) tail; + bool multi_push; + bool multi_pop; + atomic_flag push_lock; // set to true when push in progress + atomic_flag pop_lock; // set to true when push in progress + atomic_int wait_state; + int rsock; + int wsock; +} *Queue; + +extern void queue_init(Queue q, size_t qsize); + +extern void queue_multi_init(Queue q, size_t qsize, bool multi_push, bool multi_pop); + +extern void queue_cleanup(Queue q); +extern void queue_push(Queue q, QItem item); +extern QItem queue_pop(Queue q, double timeout); +extern bool queue_empty(Queue q); +extern int queue_listen(Queue q); +extern void queue_release(Queue q); +extern int queue_count(Queue q); + +extern void queue_wakeup(Queue q); + +#endif /* __AGOO_QUEUE_H__ */ diff --git a/ext/agoo/request.c b/ext/agoo/request.c new file mode 100644 index 0000000..60c0bf9 --- /dev/null +++ b/ext/agoo/request.c @@ -0,0 +1,563 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include + +#include "con.h" +#include "error_stream.h" +#include "request.h" + +static VALUE req_class = Qundef; + +static VALUE connect_val = Qundef; +static VALUE content_length_val = Qundef; +static VALUE content_type_val = Qundef; +static VALUE delete_val = Qundef; +static VALUE empty_val = Qundef; +static VALUE get_val = Qundef; +static VALUE head_val = Qundef; +static VALUE http_val = Qundef; +static VALUE options_val = Qundef; +static VALUE path_info_val = Qundef; +static VALUE post_val = Qundef; +static VALUE put_val = Qundef; +static VALUE query_string_val = Qundef; +static VALUE rack_errors_val = Qundef; +static VALUE rack_input_val = Qundef; +static VALUE rack_multiprocess_val = Qundef; +static VALUE rack_multithread_val = Qundef; +static VALUE rack_run_once_val = Qundef; +static VALUE rack_url_scheme_val = Qundef; +static VALUE rack_version_val = Qundef; +static VALUE rack_version_val_val = Qundef; +static VALUE request_method_val = Qundef; +static VALUE script_name_val = Qundef; +static VALUE server_name_val = Qundef; +static VALUE server_port_val = Qundef; +static VALUE slash_val = Qundef; + +static VALUE stringio_class = Qundef; + +static ID new_id; + +static const char content_type[] = "Content-Type"; +static const char content_length[] = "Content-Length"; + +static VALUE +req_method(Req r) { + VALUE m; + + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + switch (r->method) { + case CONNECT: m = connect_val; break; + case DELETE: m = delete_val; break; + case GET: m = get_val; break; + case HEAD: m = head_val; break; + case OPTIONS: m = options_val; break; + case POST: m = post_val; break; + case PUT: m = put_val; break; + default: m = Qnil; break; + } + return m; +} + +/* Document-method: request_method + * + * call-seq: request_method() + * + * Returns the HTTP method of the request. + */ +static VALUE +method(VALUE self) { + return req_method((Req)DATA_PTR(self)); +} + +static VALUE +req_script_name(Req r) { + // The logic is a bit tricky here and for path_info. If the HTTP path is / + // then the script_name must be empty and the path_info will be /. All + // other cases are handled with the full path in script_name and path_info + // empty. + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + if (0 == r->path.len || (1 == r->path.len && '/' == *r->path.start)) { + return empty_val; + } + return rb_str_new(r->path.start, r->path.len); +} + +/* Document-method: script_name + * + * call-seq: script_name() + * + * Returns the path info which is assumed to be the full path unless the root + * and then the rack restrictions are followed on what the script name and + * path info should be. + */ +static VALUE +script_name(VALUE self) { + return req_script_name((Req)DATA_PTR(self)); +} + +static VALUE +req_path_info(Req r) { + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + if (0 == r->path.len || (1 == r->path.len && '/' == *r->path.start)) { + return slash_val; + } + return empty_val; +} + +/* Document-method: path_info + * + * call-seq: path_info() + * + * Returns the script name which is assumed to be either '/' or the empty + * according to the rack restrictions are followed on what the script name and + * path info should be. + */ +static VALUE +path_info(VALUE self) { + return req_path_info((Req)DATA_PTR(self)); +} + +static VALUE +req_query_string(Req r) { + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + if (NULL == r->query.start) { + return empty_val; + } + return rb_str_new(r->query.start, r->query.len); +} + +/* Document-method: query_string + * + * call-seq: query_string() + * + * Returns the query string of the request. + */ +static VALUE +query_string(VALUE self) { + return req_query_string((Req)DATA_PTR(self)); +} + +static VALUE +req_server_name(Req r) { + int len; + const char *host; + const char *colon; + + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + if (NULL == (host = con_header_value(r->header.start, r->header.len, "Host", &len))) { + return Qnil; + } + for (colon = host + len - 1; host < colon; colon--) { + if (':' == *colon) { + break; + } + } + if (host == colon) { + return Qnil; + } + return rb_str_new(host, colon - host); +} + +/* Document-method: server_name + * + * call-seq: server_name() + * + * Returns the server or host name. + */ +static VALUE +server_name(VALUE self) { + return req_server_name((Req)DATA_PTR(self)); +} + +static VALUE +req_server_port(Req r) { + int len; + const char *host; + const char *colon; + + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + if (NULL == (host = con_header_value(r->header.start, r->header.len, "Host", &len))) { + return Qnil; + } + for (colon = host + len - 1; host < colon; colon--) { + if (':' == *colon) { + break; + } + } + if (host == colon) { + return Qnil; + } + return rb_str_new(colon + 1, host + len - colon - 1); +} + +/* Document-method: server_port + * + * call-seq: server_port() + * + * Returns the server or host port as a string. + */ +static VALUE +server_port(VALUE self) { + return req_server_port((Req)DATA_PTR(self)); +} + +/* Document-method: rack_version + * + * call-seq: rack_version() + * + * Returns the rack version the request is compliant with. + */ +static VALUE +rack_version(VALUE self) { + return rack_version_val_val; +} + +static VALUE +req_rack_url_scheme(Req r) { + // TBD http or https when ssl is supported + return http_val; +} + +/* Document-method: rack_url_scheme + * + * call-seq: rack_url_scheme() + * + * Returns the URL scheme or either _http_ or _https_ as a string. + */ +static VALUE +rack_url_scheme(VALUE self) { + return req_rack_url_scheme((Req)DATA_PTR(self)); +} + +static VALUE +req_rack_input(Req r) { + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + if (NULL == r->body.start) { + return Qnil; + } + return rb_funcall(stringio_class, new_id, 1, rb_str_new(r->body.start, r->body.len)); +} + +/* Document-method: rack_input + * + * call-seq: rack_input() + * + * Returns an input stream for the request body. If no body is present then + * _nil_ is returned. + */ +static VALUE +rack_input(VALUE self) { + return req_rack_input((Req)DATA_PTR(self)); +} + +static VALUE +req_rack_errors(Req r) { + return error_stream_new(r->server); +} + +/* Document-method: rack_errors + * + * call-seq: rack_errors() + * + * Returns an error stream for the request. This stream is used to write error + * log entries. + */ +static VALUE +rack_errors(VALUE self) { + return req_rack_errors((Req)DATA_PTR(self)); +} + +static VALUE +req_rack_multithread(Req r) { + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + if (NULL != r->server && 1 < r->server->thread_cnt) { + return Qtrue; + } + return Qfalse; +} + +/* Document-method: rack_multithread + * + * call-seq: rack_multithread() + * + * Returns true is the server is using multiple handler worker threads. + */ +static VALUE +rack_multithread(VALUE self) { + return req_rack_multithread((Req)DATA_PTR(self)); +} + +/* Document-method: rack_multiprocess + * + * call-seq: rack_multiprocess() + * + * Returns false since the server is a single process. + */ +static VALUE +rack_multiprocess(VALUE self) { + return Qfalse; +} + +/* Document-method: rack_run_once + * + * call-seq: rack_run_once() + * + * Returns false. + */ +static VALUE +rack_run_once(VALUE self) { + return Qfalse; +} + +static void +add_header_value(VALUE hh, const char *key, int klen, const char *val, int vlen) { + if (sizeof(content_type) - 1 == klen && 0 == strncasecmp(key, content_type, sizeof(content_type) - 1)) { + rb_hash_aset(hh, content_type_val, rb_str_new(val, vlen)); + } else if (sizeof(content_length) - 1 == klen && 0 == strncasecmp(key, content_length, sizeof(content_length) - 1)) { + rb_hash_aset(hh, content_length_val, rb_str_new(val, vlen)); + } else { + char hkey[1024]; + char *k = hkey; + + strcpy(hkey, "HTTP_"); + k = hkey + 5; + if ((int)(sizeof(hkey) - 5) <= klen) { + klen = sizeof(hkey) - 6; + } + strncpy(k, key, klen); + hkey[klen + 5] = '\0'; + + rb_hash_aset(hh, rb_str_new(hkey, klen + 5), rb_str_new(val, vlen)); + } +} + +static void +fill_headers(Req r, VALUE hash) { + char *h = r->header.start; + char *end = h + r->header.len; + char *key = h; + char *kend; + char *val = NULL; + char *vend; + + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + for (; h < end; h++) { + switch (*h) { + case ':': + kend = h; + val = h + 1; + break; + case ' ': + if (NULL != val) { + val++; + } else { + // TBD handle trailing spaces as well + key++; + } + break; + case '\r': + if (NULL != val) { + vend = h; + } + if ('\n' == *(h + 1)) { + h++; + } + add_header_value(hash, key, kend - key, val, vend - val); + key = h + 1; + kend = NULL; + val = NULL; + vend = NULL; + break; + default: + break; + } + } +} + +/* Document-method: headers + * + * call-seq: headers() + * + * Returns the header of the request as a Hash. + */ +static VALUE +headers(VALUE self) { + Req r = DATA_PTR(self); + volatile VALUE h; + + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + h = rb_hash_new(); + fill_headers(r, h); + + return h; +} + +/* Document-method: body + * + * call-seq: body() + * + * Returns the body of the request as a String. If there is no body then _nil_ + * is returned. + */ +static VALUE +body(VALUE self) { + Req r = DATA_PTR(self); + + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + if (NULL == r->body.start) { + return Qnil; + } + return rb_str_new(r->body.start, r->body.len); +} + +/* Document-class: Agoo::Request + * + * A Request is passes to handler that respond to the _on_request_ method. The + * request is a more efficient encapsulation of the rack environment. + */ +VALUE +request_env(Req req) { + volatile VALUE env = rb_hash_new(); + + // As described by + // http://www.rubydoc.info/github/rack/rack/master/file/SPEC and + // https://github.com/rack/rack/blob/master/SPEC. + + rb_hash_aset(env, request_method_val, req_method(req)); + rb_hash_aset(env, script_name_val, req_script_name(req)); + rb_hash_aset(env, path_info_val, req_path_info(req)); + rb_hash_aset(env, query_string_val, req_query_string(req)); + rb_hash_aset(env, server_name_val, req_server_name(req)); + rb_hash_aset(env, server_port_val, req_server_port(req)); + fill_headers(req, env); + rb_hash_aset(env, rack_version_val, rack_version_val_val); + rb_hash_aset(env, rack_url_scheme_val, req_rack_url_scheme(req)); + rb_hash_aset(env, rack_input_val, req_rack_input(req)); + rb_hash_aset(env, rack_errors_val, req_rack_errors(req)); + rb_hash_aset(env, rack_multithread_val, req_rack_multithread(req)); + rb_hash_aset(env, rack_multiprocess_val, Qfalse); + rb_hash_aset(env, rack_run_once_val, Qfalse); + + return env; +} + +/* Document-method: to_h + * + * call-seq: to_h() + * + * Returns a Hash representation of the request which is the same as a rack + * environment Hash. + */ +static VALUE +to_h(VALUE self) { + Req r = DATA_PTR(self); + + if (NULL == r) { + rb_raise(rb_eArgError, "Request is no longer valid."); + } + return request_env(r); +} + +/* Document-method: to_s + * + * call-seq: to_s() + * + * Returns a string representation of the request. + */ +static VALUE +to_s(VALUE self) { + volatile VALUE h = to_h(self); + + return rb_funcall(h, rb_intern("to_s"), 0); +} + +VALUE +request_wrap(Req req) { + return Data_Wrap_Struct(req_class, NULL, NULL, req); +} + +/* Document-class: Agoo::Request + * + * A representation of an HTTP request that is used with a handler that + * responds to the _on_request_ method. The request is a more efficient + * encapsulation of the rack environment. + */ +void +request_init(VALUE mod) { + req_class = rb_define_class_under(mod, "Request", rb_cObject); + + rb_define_method(req_class, "to_s", to_s, 0); + rb_define_method(req_class, "to_h", to_h, 0); + rb_define_method(req_class, "environment", to_h, 0); + rb_define_method(req_class, "env", to_h, 0); + rb_define_method(req_class, "request_method", method, 0); + rb_define_method(req_class, "script_name", script_name, 0); + rb_define_method(req_class, "path_info", path_info, 0); + rb_define_method(req_class, "query_string", query_string, 0); + rb_define_method(req_class, "server_name", server_name, 0); + rb_define_method(req_class, "server_port", server_port, 0); + rb_define_method(req_class, "rack_version", rack_version, 0); + rb_define_method(req_class, "rack_url_scheme", rack_url_scheme, 0); + rb_define_method(req_class, "rack_input", rack_input, 0); + rb_define_method(req_class, "rack_errors", rack_errors, 0); + rb_define_method(req_class, "rack_multithread", rack_multithread, 0); + rb_define_method(req_class, "rack_multiprocess", rack_multiprocess, 0); + rb_define_method(req_class, "rack_run_once", rack_run_once, 0); + rb_define_method(req_class, "headers", headers, 0); + rb_define_method(req_class, "body", body, 0); + + new_id = rb_intern("new"); + + stringio_class = rb_const_get(rb_cObject, rb_intern("StringIO")); + + connect_val = rb_str_new_cstr("CONNECT"); rb_gc_register_address(&connect_val); + content_length_val = rb_str_new_cstr("CONTENT_LENGTH"); rb_gc_register_address(&content_length_val); + content_type_val = rb_str_new_cstr("CONTENT_TYPE"); rb_gc_register_address(&content_type_val); + delete_val = rb_str_new_cstr("DELETE"); rb_gc_register_address(&delete_val); + empty_val = rb_str_new_cstr(""); rb_gc_register_address(&empty_val); + get_val = rb_str_new_cstr("GET"); rb_gc_register_address(&get_val); + head_val = rb_str_new_cstr("HEAD"); rb_gc_register_address(&head_val); + http_val = rb_str_new_cstr("http"); rb_gc_register_address(&http_val); + options_val = rb_str_new_cstr("OPTIONS"); rb_gc_register_address(&options_val); + path_info_val = rb_str_new_cstr("PATH_INFO"); rb_gc_register_address(&path_info_val); + post_val = rb_str_new_cstr("POST"); rb_gc_register_address(&post_val); + put_val = rb_str_new_cstr("PUT"); rb_gc_register_address(&put_val); + query_string_val = rb_str_new_cstr("QUERY_STRING"); rb_gc_register_address(&query_string_val); + rack_errors_val = rb_str_new_cstr("rack.errors"); rb_gc_register_address(&rack_errors_val); + rack_input_val = rb_str_new_cstr("rack.input"); rb_gc_register_address(&rack_input_val); + rack_multiprocess_val = rb_str_new_cstr("rack.multiprocess");rb_gc_register_address(&rack_multiprocess_val); + rack_multithread_val = rb_str_new_cstr("rack.multithread");rb_gc_register_address(&rack_multithread_val); + rack_run_once_val = rb_str_new_cstr("rack.run_once"); rb_gc_register_address(&rack_run_once_val); + rack_url_scheme_val = rb_str_new_cstr("rack.url_scheme"); rb_gc_register_address(&rack_url_scheme_val); + rack_version_val = rb_str_new_cstr("rack.version"); rb_gc_register_address(&rack_version_val); + rack_version_val_val = rb_str_new_cstr("2.0.3"); rb_gc_register_address(&rack_version_val_val); + request_method_val = rb_str_new_cstr("REQUEST_METHOD"); rb_gc_register_address(&request_method_val); + script_name_val = rb_str_new_cstr("SCRIPT_NAME"); rb_gc_register_address(&script_name_val); + server_name_val = rb_str_new_cstr("SERVER_NAME"); rb_gc_register_address(&server_name_val); + server_port_val = rb_str_new_cstr("SERVER_PORT"); rb_gc_register_address(&server_port_val); + slash_val = rb_str_new_cstr("/"); rb_gc_register_address(&slash_val); +} diff --git a/ext/agoo/request.h b/ext/agoo/request.h new file mode 100644 index 0000000..ef8b578 --- /dev/null +++ b/ext/agoo/request.h @@ -0,0 +1,36 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_REQUEST_H__ +#define __AGOO_REQUEST_H__ + +#include + +#include "hook.h" +#include "res.h" +#include "server.h" +#include "types.h" + +typedef struct _Str { + char *start; + unsigned int len; +} *Str; + +typedef struct _Req { + Server server; + Method method; + struct _Str path; + struct _Str query; + struct _Str header; + struct _Str body; + VALUE handler; + HookType handler_type; + Res res; + size_t mlen; // allocated msg length + char msg[8]; // expanded to be full message +} *Req; + +extern void request_init(VALUE mod); +extern VALUE request_wrap(Req req); +extern VALUE request_env(Req req); + +#endif // __AGOO_REQUEST_H__ diff --git a/ext/agoo/res.c b/ext/agoo/res.c new file mode 100644 index 0000000..5f61f1a --- /dev/null +++ b/ext/agoo/res.c @@ -0,0 +1,38 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include + +#include "res.h" + +Res +res_create() { + Res res = (Res)malloc(sizeof(struct _Res)); + + if (NULL != res) { + res->next = NULL; + atomic_init(&res->message, NULL); + res->close = false; + } + return res; +} + +void +res_destroy(Res res) { + if (NULL != res) { + Text message = res_message(res); + + if (NULL != message) { + text_release(message); + } + free(res); + } +} + +void +res_set_message(Res res, Text t) { + if (NULL != t) { + text_ref(t); + } + atomic_store(&res->message, t); +} + diff --git a/ext/agoo/res.h b/ext/agoo/res.h new file mode 100644 index 0000000..ca8559d --- /dev/null +++ b/ext/agoo/res.h @@ -0,0 +1,28 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_RES_H__ +#define __AGOO_RES_H__ + +#include +#include + +#include + +#include "text.h" + +typedef struct _Res { + struct _Res *next; + _Atomic(Text) message; + bool close; +} *Res; + +extern Res res_create(); +extern void res_destroy(Res res); +extern void res_set_message(Res res, Text t); + +static inline Text +res_message(Res res) { + return atomic_load(&res->message); +} + +#endif // __AGOO_RES_H__ diff --git a/ext/agoo/response.c b/ext/agoo/response.c new file mode 100644 index 0000000..2c58ce3 --- /dev/null +++ b/ext/agoo/response.c @@ -0,0 +1,271 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include + +#include "http.h" +#include "response.h" +#include "text.h" + +static VALUE res_class = Qundef; + +static int +response_len(Response res) { + char c; + const char *msg = http_code_message(res->code); + int len = snprintf(&c, 1, "HTTP/1.1 %d %s\r\nContent-Length: %d\r\n", res->code, msg, res->blen); + Header h; + + for (h = res->headers; NULL != h; h = h->next) { + len += h->len; + } + len += 2; // for additional \r\n before body + len += res->blen; + + return len; +} + +static void +response_fill(Response res, char *buf) { + Header h; + const char *msg = http_code_message(res->code); + + buf += sprintf(buf, "HTTP/1.1 %d %s\r\nContent-Length: %d\r\n", res->code, msg, res->blen); + + for (h = res->headers; NULL != h; h = h->next) { + strncpy(buf, h->text, h->len); + buf += h->len; + } + *buf++ = '\r'; + *buf++ = '\n'; + if (NULL != res->body) { + memcpy(buf, res->body, res->blen); + } +} + +static void +response_free(void *ptr) { + Response res = (Response)ptr; + Header h; + + while (NULL != (h = res->headers)) { + res->headers = h->next; + xfree(h); + } + free(res->body); // allocated with strdup + xfree(ptr); +} + +VALUE +response_new(Server server ) { + Response res = ALLOC(struct _Response); + + memset(res, 0, sizeof(struct _Response)); + res->code = 200; + res->server = server; + + return Data_Wrap_Struct(res_class, NULL, response_free, res); +} + +/* Document-method: to_s + * + * call-seq: to_s() + * + * Returns a string representation of the response. + */ +static VALUE +to_s(VALUE self) { + Response res = (Response)DATA_PTR(self); + int len = response_len(res); + char *s = ALLOC_N(char, len + 1); + + response_fill(res, s); + + return rb_str_new(s, len); +} + +/* Document-method: content + * + * call-seq: content() + * + * alias for _body_ + */ + +/* Document-method: body + * + * call-seq: body() + * + * Gets the HTTP body for the response. + */ +static VALUE +body_get(VALUE self) { + Response res = (Response)DATA_PTR(self); + + if (NULL == res->body) { + return Qnil; + } + return rb_str_new(res->body, res->blen); +} + +/* Document-method: content= + * + * call-seq: content=(str) + * + * alias for _body=_ + */ + +/* Document-method: body= + * + * call-seq: body=(str) + * + * Sets the HTTP body for the response. + */ +static VALUE +body_set(VALUE self, VALUE val) { + Response res = (Response)DATA_PTR(self); + + if (T_STRING == rb_type(val)) { + res->body = strdup(StringValuePtr(val)); + res->blen = RSTRING_LEN(val); + } else { + // TBD use Oj + } + return Qnil; +} + +/* Document-method: code + * + * call-seq: code() + * + * Gets the HTTP status code for the response. + */ +static VALUE +code_get(VALUE self) { + return INT2NUM(((Response)DATA_PTR(self))->code); +} + +/* Document-method: code= + * + * call-seq: code=(value) + * + * Sets the HTTP status code for the response. + */ +static VALUE +code_set(VALUE self, VALUE val) { + int code = NUM2INT(val); + + if (100 <= code && code < 600) { + ((Response)DATA_PTR(self))->code = code; + } else { + rb_raise(rb_eArgError, "%d is not a valid HTTP status code.", code); + } + return Qnil; +} + +/* Document-method: [] + * + * call-seq: [](key) + * + * Gets a header element associated with the _key_. + */ +static VALUE +head_get(VALUE self, VALUE key) { + Response res = (Response)DATA_PTR(self); + Header h; + const char *ks = StringValuePtr(key); + int klen = RSTRING_LEN(key); + + for (h = res->headers; NULL != h; h = h->next) { + if (0 == strncasecmp(h->text, ks, klen) && klen + 1 < h->len && ':' == h->text[klen]) { + return rb_str_new(h->text + klen + 2, h->len - klen - 4); + } + } + return Qnil; +} + +/* Document-method: []= + * + * call-seq: []=(key, value) + * + * Sets a header element with the _key_ and _value_ provided. + */ +static VALUE +head_set(VALUE self, VALUE key, VALUE val) { + Response res = (Response)DATA_PTR(self); + Header h; + Header prev = NULL; + const char *ks = StringValuePtr(key); + const char *vs; + int klen = RSTRING_LEN(key); + int vlen; + int hlen; + + for (h = res->headers; NULL != h; h = h->next) { + if (0 == strncasecmp(h->text, ks, klen) && klen + 1 < h->len && ':' == h->text[klen]) { + if (NULL == prev) { + res->headers = h->next; + } else { + prev->next = h->next; + } + xfree(h); + break; + } + prev = h; + } + if (T_STRING != rb_type(val)) { + val = rb_funcall(val, rb_intern("to_s"), 0); + } + vs = StringValuePtr(val); + vlen = RSTRING_LEN(val); + + if (res->server->pedantic) { + http_header_ok(ks, klen, vs, vlen); + } + hlen = klen + vlen + 4; + h = (Header)ALLOC_N(char, sizeof(struct _Header) - 8 + hlen + 1); + h->next = NULL; + h->len = hlen; + strncpy(h->text, ks, klen); + strcpy(h->text + klen, ": "); + strncpy(h->text + klen + 2, vs, vlen); + strcpy(h->text + klen + 2 + vlen, "\r\n"); + if (NULL == res->headers) { + res->headers = h; + } else { + for (prev = res->headers; NULL != prev->next; prev = prev->next) { + } + prev->next = h; + } + return Qnil; +} + +Text +response_text(VALUE self) { + Response res = (Response)DATA_PTR(self); + int len = response_len(res); + Text t = text_allocate(len); + + response_fill(res, t->text); + t->len = len; + + return t; +} + +/* Document-class: Agoo::Response + * + * A response passed to a handler that responds to the _on_request_ + * method. The expected response is modified by the handler before returning. + */ +void +response_init(VALUE mod) { + res_class = rb_define_class_under(mod, "Response", rb_cObject); + + rb_define_method(res_class, "to_s", to_s, 0); + rb_define_method(res_class, "body", body_get, 0); + rb_define_method(res_class, "body=", body_set, 1); + rb_define_method(res_class, "content", body_get, 0); + rb_define_method(res_class, "content=", body_set, 1); + rb_define_method(res_class, "code", code_get, 0); + rb_define_method(res_class, "code=", code_set, 1); + rb_define_method(res_class, "[]", head_get, 1); + rb_define_method(res_class, "[]=", head_set, 2); +} diff --git a/ext/agoo/response.h b/ext/agoo/response.h new file mode 100644 index 0000000..c7c1acd --- /dev/null +++ b/ext/agoo/response.h @@ -0,0 +1,33 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_RESPONSE_H__ +#define __AGOO_RESPONSE_H__ + +#include +#include + +#include + +#include "server.h" +#include "text.h" + +typedef struct _Header { + struct _Header *next; + int len; + char text[8]; +} *Header; + +typedef struct _Response { + int code; + Header headers; + int blen; + char *body; + Server server; +} *Response; + +extern void response_init(VALUE mod); + +extern VALUE response_new(Server server); +extern Text response_text(VALUE self); + +#endif // __AGOO_RESPONSE_H__ diff --git a/ext/agoo/server.c b/ext/agoo/server.c new file mode 100644 index 0000000..2fe9dae --- /dev/null +++ b/ext/agoo/server.c @@ -0,0 +1,891 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "con.h" +#include "dtime.h" +#include "err.h" +#include "http.h" +#include "response.h" +#include "request.h" +#include "server.h" + +static VALUE server_class = Qundef; + +static VALUE connect_sym; +static VALUE delete_sym; +static VALUE get_sym; +static VALUE head_sym; +static VALUE options_sym; +static VALUE post_sym; +static VALUE put_sym; + +static ID call_id; +static ID each_id; +static ID on_request_id; +static ID to_i_id; + +static Server the_server = NULL; + +static void +shutdown_server(Server server) { + if (NULL != server && server->active) { + the_server = NULL; + + server->active = false; + if (0 != server->listen_thread) { + pthread_join(server->listen_thread, NULL); + server->listen_thread = 0; + } + if (0 != server->con_thread) { + pthread_join(server->con_thread, NULL); + server->con_thread = 0; + } + queue_cleanup(&server->con_queue); + // The preferred method to of waiting for the ruby threads would be + // either a join or even a kill but since we don't have the gvi here + // that would cause a segfault. Instead we set a timeout and wait for + // the running counter to drop to zero. + if (NULL != server->eval_threads) { + double timeout = dtime() + 2.0; + + while (dtime() < timeout) { + if (0 >= atomic_load(&server->running)) { + break; + } + dsleep(0.02); + } + xfree(server->eval_threads); + server->eval_threads = NULL; + } + while (NULL != server->hooks) { + Hook h = server->hooks; + + server->hooks = h->next; + hook_destroy(h); + } + queue_cleanup(&server->eval_queue); + log_close(&server->log); + } +} + +static void +sig_handler(int sig) { + if (NULL != the_server) { + shutdown_server(the_server); + } + // Use exit instead of rb_exit as rb_exit segfaults most of the time. + //rb_exit(0); + exit(0); +} + +static void +server_free(void *ptr) { + shutdown_server((Server)ptr); + // Commented out for now as it causes a segfault later. Some thread seems + //to be pointing at it even though they have exited so live with a memory + //leak that only shows up when the program exits. + //xfree(ptr); + the_server = NULL; +} + +static int +configure(Err err, Server s, int port, const char *root, VALUE options) { + s->port = port; + s->root = strdup(root); + s->thread_cnt = 1; + s->running = 0; + s->listen_thread = 0; + s->con_thread = 0; + s->log.cats = NULL; + log_cat_reg(&s->log, &s->error_cat, "ERROR", ERROR, RED, true); + log_cat_reg(&s->log, &s->warn_cat, "WARN", WARN, YELLOW, true); + log_cat_reg(&s->log, &s->info_cat, "INFO", INFO, GREEN, false); + log_cat_reg(&s->log, &s->debug_cat, "DEBUG", DEBUG, GRAY, false); + log_cat_reg(&s->log, &s->con_cat, "connect", INFO, GREEN, false); + log_cat_reg(&s->log, &s->req_cat, "request", INFO, CYAN, false); + log_cat_reg(&s->log, &s->resp_cat, "response", INFO, DARK_CYAN, false); + log_cat_reg(&s->log, &s->eval_cat, "eval", INFO, BLUE, false); + + if (ERR_OK != log_init(err, &s->log, options)) { + return err->code; + } + if (Qnil != options) { + VALUE v; + + if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("thread_count"))))) { + int tc = FIX2INT(v); + + if (1 <= tc || tc < 1000) { + s->thread_cnt = tc; + } else { + rb_raise(rb_eArgError, "thread_count must be between 1 and 1000."); + } + } + if (Qnil != (v = rb_hash_lookup(options, ID2SYM(rb_intern("pedantic"))))) { + s->pedantic = (Qtrue == v); + } + } + return ERR_OK; +} + +/* Document-method: new + * + * call-seq: new(port, root, options) + * + * Creates a new server that will listen on the designated _port_ and using + * the _root_ as the root of the static resources. Logging is feature based + * and not level based and the options reflect that approach. + * + * - *options* [_Hash_] server options + * + * - *:pedantic* [_true_|_false_] if true response header and status codes are check and an exception raised if they violate the rack spec at https://github.com/rack/rack/blob/master/SPEC, https://tools.ietf.org/html/rfc3875#section-4.1.18, or https://tools.ietf.org/html/rfc7230. + * + * + * - *:thread_count* [_Integer_] number of ruby worker threads. Defaults to one. If zero then the _start_ function will not return but instead will proess using the thread that called _start_. Usually the default is best unless the workers are making IO calls. + * + * - *:log_dir* [_String_] directory to place log files in. If nil or empty then no log files are written. + * + + * - *:log_console* [_true_|_false_] if true log entry are display on the console. + * + * - *:log_classic* [_true_|_false_] if true log entry follow a classic format. If false log entries are JSON. + * + * - *:log_colorize* [_true_|_false_] if true log entries are colorized. + * + * - *:log_states* [_Hash_] a map of logging categories and whether they should be on or off. Categories are: + * - *:ERROR* errors + * - *:WARN* warnings + * - *:INFO* infomational + * - *:DEBUG* debugging + * - *:connect* openning and closing of connections + * - *:request* requests + * - *:response* responses + * - *:eval* handler evaluationss + */ +static VALUE +server_new(int argc, VALUE *argv, VALUE self) { + Server s; + struct _Err err = ERR_INIT; + int port; + const char *root; + VALUE options = Qnil; + + if (argc < 2 || 3 < argc) { + rb_raise(rb_eArgError, "Wrong number of arguments to Agoo::Server.new."); + } + port = FIX2INT(argv[0]); + rb_check_type(argv[1], T_STRING); + root = StringValuePtr(argv[1]); + if (3 <= argc) { + options = argv[2]; + } + s = ALLOC(struct _Server); + memset(s, 0, sizeof(struct _Server)); + if (ERR_OK != configure(&err, s, port, root, options)) { + xfree(s); + // TBD raise Agoo specific exception + rb_raise(rb_eArgError, "%s", err.msg); + } + queue_multi_init(&s->con_queue, 256, false, false); + queue_multi_init(&s->eval_queue, 1024, false, true); + cache_init(&s->pages); + the_server = s; + + return Data_Wrap_Struct(server_class, NULL, server_free, s); +} + +static void* +listen_loop(void *x) { + Server server = (Server)x; + struct sockaddr_in addr; + int optval = 1; + struct pollfd pa[1]; + struct pollfd *fp = pa; + struct _Err err = ERR_INIT; + struct sockaddr_in client_addr; + int client_sock; + socklen_t alen = 0; + Con con; + int i; + uint64_t cnt = 0; + + atomic_fetch_add(&server->running, 1); + if (0 >= (pa->fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP))) { + log_cat(&server->error_cat, "Server failed to open server socket. %s.", strerror(errno)); + atomic_fetch_sub(&server->running, 1); + return NULL; + } +#ifdef OSX_OS + setsockopt(pa->fd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval)); +#endif + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(server->port); + setsockopt(pa->fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)); + setsockopt(pa->fd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval)); + if (0 > bind(fp->fd, (struct sockaddr*)&addr, sizeof(addr))) { + log_cat(&server->error_cat, "Server failed to bind server socket. %s.", strerror(errno)); + atomic_fetch_sub(&server->running, 1); + return NULL; + } + listen(pa->fd, 1000); + pa->events = POLLIN; + pa->revents = 0; + + memset(&client_addr, 0, sizeof(client_addr)); + while (server->active) { + if (0 > (i = poll(pa, 1, 100))) { + if (EAGAIN == errno) { + continue; + } + log_cat(&server->error_cat, "Server polling error. %s.", strerror(errno)); + // Either a signal or something bad like out of memory. Might as well exit. + break; + } + if (0 == i) { // nothing to read + continue; + } + if (0 != (pa->revents & POLLIN)) { + if (0 > (client_sock = accept(pa->fd, (struct sockaddr*)&client_addr, &alen))) { + log_cat(&server->error_cat, "Server accept connection failed. %s.", strerror(errno)); + } else if (NULL == (con = con_create(&err, server, client_sock, ++cnt))) { + log_cat(&server->error_cat, "Server accept connection failed. %s.", err.msg); + close(client_sock); + cnt--; + err_clear(&err); + } else { +#ifdef OSX_OS + setsockopt(client_sock, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval)); +#endif + fcntl(client_sock, F_SETFL, O_NONBLOCK); + setsockopt(client_sock, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)); + setsockopt(client_sock, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval)); + log_cat(&server->con_cat, "Server accepted connection %llu on port %d [%d]", (unsigned long long)cnt, server->port, con->sock); + queue_push(&server->con_queue, (void*)con); + } + } + if (0 != (pa->revents & (POLLERR | POLLHUP | POLLNVAL))) { + if (0 != (pa->revents & (POLLHUP | POLLNVAL))) { + log_cat(&server->error_cat, "Agoo server socket on port %d closed.", server->port); + } else { + log_cat(&server->error_cat, "Agoo server socket on port %d error.", server->port); + } + server->active = false; + } + pa->revents = 0; + } + close(pa->fd); + atomic_fetch_sub(&server->running, 1); + + return NULL; +} + +static const char bad500[] = "HTTP/1.1 500 Internal Error\r\nConnection: Close\r\nContent-Length: "; + +static VALUE +rescue_error(VALUE x) { + Req req = (Req)x; + volatile VALUE info = rb_errinfo(); + volatile VALUE msg = rb_funcall(info, rb_intern("message"), 0); + const char *classname = rb_obj_classname(info); + const char *ms = rb_string_value_ptr(&msg); + char buf[1024]; + int len = (int)(strlen(classname) + 2 + strlen(ms)); + int cnt; + Text message; + + if ((int)(sizeof(buf) - sizeof(bad500) + 7) <= len) { + len = sizeof(buf) - sizeof(bad500) + 7; + } + cnt = snprintf(buf, sizeof(buf), "HTTP/1.1 500 Internal Error\r\nConnection: Close\r\nContent-Length: %d\r\n\r\n%s: %s", len, classname, ms); + message = text_create(buf, cnt); + + req->res->close = true; + res_set_message(req->res, message); + queue_wakeup(&req->server->con_queue); + + return Qfalse; +} + +static VALUE +handle_base_inner(void *x) { + Req req = (Req)x; + volatile VALUE rr = request_wrap(req); + volatile VALUE rres = response_new(req->server); + + rb_funcall(req->handler, on_request_id, 2, rr, rres); + DATA_PTR(rr) = NULL; + + res_set_message(req->res, response_text(rres)); + queue_wakeup(&req->server->con_queue); + + return Qfalse; +} + +static void* +handle_base(void *x) { + rb_rescue2(handle_base_inner, (VALUE)x, rescue_error, (VALUE)x, rb_eException, 0); + + return NULL; +} + +static int +header_cb(VALUE key, VALUE value, Text *tp) { + const char *ks = StringValuePtr(key); + int klen = (int)RSTRING_LEN(key); + const char *vs = StringValuePtr(value); + int vlen = (int)RSTRING_LEN(value); + + http_header_ok(ks, klen, vs, vlen); + if (0 != strcasecmp("Content-Length", ks)) { + *tp = text_append(*tp, ks, klen); + *tp = text_append(*tp, ": ", 2); + *tp = text_append(*tp, vs, vlen); + *tp = text_append(*tp, "\r\n", 2); + } + return ST_CONTINUE; +} + +static VALUE +header_each_cb(VALUE kv, Text *tp) { + header_cb(rb_ary_entry(kv, 0), rb_ary_entry(kv, 1), tp); + + return Qnil; +} + +static VALUE +body_len_cb(VALUE v, int *sizep) { + *sizep += (int)RSTRING_LEN(v); + + return Qnil; +} + +static VALUE +body_append_cb(VALUE v, Text *tp) { + *tp = text_append(*tp, StringValuePtr(v), (int)RSTRING_LEN(v)); + + return Qnil; +} + +static VALUE +handle_rack_inner(void *x) { + Req req = (Req)x; + Text t; + volatile VALUE env = request_env(req); + volatile VALUE res = rb_funcall(req->handler, call_id, 1, env); + volatile VALUE hv; + volatile VALUE bv; + int code; + const char *status_msg; + int bsize = 0; + + rb_check_type(res, T_ARRAY); + if (3 != RARRAY_LEN(res)) { + rb_raise(rb_eArgError, "a rack call() response must be an array of 3 members."); + } + code = NUM2INT(rb_funcall(rb_ary_entry(res, 0), to_i_id, 0)); + status_msg = http_code_message(code); + if ('\0' == *status_msg) { + rb_raise(rb_eArgError, "invalid rack call() response status code (%d).", code); + } + hv = rb_ary_entry(res, 1); + if (!rb_respond_to(hv, each_id)) { + rb_raise(rb_eArgError, "invalid rack call() response headers does not respond to each."); + } + bv = rb_ary_entry(res, 2); + if (!rb_respond_to(bv, each_id)) { + rb_raise(rb_eArgError, "invalid rack call() response body does not respond to each."); + } + if (NULL == (t = text_allocate(1024))) { + rb_raise(rb_eArgError, "failed to allocate response."); + } + if (T_ARRAY != rb_type(bv)) { + int i; + int bcnt = RARRAY_LEN(bv); + + for (i = 0; i < bcnt; i++) { + bsize += (int)RSTRING_LEN(rb_ary_entry(bv, i)); + } + } else { + rb_iterate (rb_each, bv, body_len_cb, (VALUE)&bsize); + } + switch (code) { + case 100: + case 101: + case 102: + case 204: + case 205: + case 304: + // TBD Content-Type and Content-Length can not be present + t->len = sprintf(t->text, "HTTP/1.1 %d %s\r\n", code, status_msg); + break; + default: + t->len = sprintf(t->text, "HTTP/1.1 %d %s\r\nContent-Length: %d\r\n", code, status_msg, bsize); + break; + } + if (T_HASH == rb_type(hv)) { + rb_hash_foreach(hv, header_cb, (VALUE)&t); + } else { + rb_iterate (rb_each, hv, header_each_cb, (VALUE)&t); + } + t = text_append(t, "\r\n", 2); + if (0 < bsize) { + if (T_ARRAY == rb_type(bv)) { + VALUE v; + int i; + int bcnt = RARRAY_LEN(bv); + + for (i = 0; i < bcnt; i++) { + v = rb_ary_entry(bv, i); + t = text_append(t, StringValuePtr(v), (int)RSTRING_LEN(v)); + } + } else { + rb_iterate (rb_each, bv, body_append_cb, (VALUE)&t); + } + } + res_set_message(req->res, t); + queue_wakeup(&req->server->con_queue); + + return Qfalse; +} + +static void* +handle_rack(void *x) { + // Disable GC. The handle_rack function or rather the env seems to get + // collected even though it is volatile so for now turn off GC + // temporarily. + rb_gc_disable(); + rb_rescue2(handle_rack_inner, (VALUE)x, rescue_error, (VALUE)x, rb_eException, 0); + rb_gc_enable(); + + return NULL; +} + +static VALUE +handle_wab_inner(void *x) { + Req req = (Req)x; + volatile VALUE rr = request_wrap(req); + volatile VALUE rres = response_new(req->server); + + rb_funcall(req->handler, on_request_id, 2, rr, rres); + DATA_PTR(rr) = NULL; + + res_set_message(req->res, response_text(rres)); + queue_wakeup(&req->server->con_queue); + + return Qfalse; +} + +static void* +handle_wab(void *x) { + rb_rescue2(handle_wab_inner, (VALUE)x, rescue_error, (VALUE)x, rb_eException, 0); + + return NULL; +} +static void +handle_protected(Req req) { + + switch (req->handler_type) { + case BASE_HOOK: + rb_thread_call_with_gvl(handle_base, req); + break; + case RACK_HOOK: + rb_thread_call_with_gvl(handle_rack, req); + break; + case WAB_HOOK: + rb_thread_call_with_gvl(handle_wab, req); + break; + default: { + char buf[256]; + int cnt = snprintf(buf, sizeof(buf), "HTTP/1.1 500 Internal Error\r\nConnection: Close\r\nContent-Length: 0\r\n\r\n"); + Text message = text_create(buf, cnt); + + req->res->close = true; + res_set_message(req->res, message); + queue_wakeup(&req->server->con_queue); + break; + } + } +} + +static void* +process_loop(void *ptr) { + Server server = (Server)ptr; + Req req; + + atomic_fetch_add(&server->running, 1); + while (server->active) { + if (NULL != (req = (Req)queue_pop(&server->eval_queue, 0.1))) { + handle_protected(req); + } + } + atomic_fetch_sub(&server->running, 1); + + return NULL; +} + +static VALUE +wrap_process_loop(void *ptr) { + rb_thread_call_without_gvl(process_loop, ptr, RUBY_UBF_IO, NULL); + return Qnil; +} + +/* Document-method: start + * + * call-seq: start() + * + * Start the server. + */ +static VALUE +start(VALUE self) { + Server server = (Server)DATA_PTR(self); + VALUE *vp; + int i; + + server->active = true; + + pthread_create(&server->listen_thread, NULL, listen_loop, server); + pthread_create(&server->con_thread, NULL, con_loop, server); + + if (0 >= server->thread_cnt) { + Req req; + + while (server->active) { + if (NULL != (req = (Req)queue_pop(&server->eval_queue, 0.1))) { + switch (req->handler_type) { + case BASE_HOOK: + handle_base(req); + break; + case RACK_HOOK: + handle_rack(req); + break; + case WAB_HOOK: + handle_wab(req); + break; + default: { + char buf[256]; + int cnt = snprintf(buf, sizeof(buf), "HTTP/1.1 500 Internal Error\r\nConnection: Close\r\nContent-Length: 0\r\n\r\n"); + Text message = text_create(buf, cnt); + + req->res->close = true; + res_set_message(req->res, message); + queue_wakeup(&req->server->con_queue); + break; + } + } + } + } + } else { + server->eval_threads = ALLOC_N(VALUE, server->thread_cnt + 1); + for (i = server->thread_cnt, vp = server->eval_threads; 0 < i; i--, vp++) { + *vp = rb_thread_create(wrap_process_loop, server); + } + *vp = Qnil; + } + return Qnil; +} + +/* Document-method: shutdown + * + * call-seq: shutdown() + * + * Shutdown the server. Logs and queues are flushed before shutting down. + */ +static VALUE +server_shutdown(VALUE self) { + shutdown_server((Server)DATA_PTR(self)); + return Qnil; +} + +/* Document-method: error? + * + * call-seq: error?() + * + * Returns true is errors are being logged. + */ +static VALUE +log_errorp(VALUE self) { + return ((Server)DATA_PTR(self))->error_cat.on ? Qtrue : Qfalse; +} + +/* Document-method: warn? + * + * call-seq: warn?() + * + * Returns true is warnings are being logged. + */ +static VALUE +log_warnp(VALUE self) { + return ((Server)DATA_PTR(self))->warn_cat.on ? Qtrue : Qfalse; +} + +/* Document-method: info? + * + * call-seq: info?() + * + * Returns true is info entries are being logged. + */ +static VALUE +log_infop(VALUE self) { + return ((Server)DATA_PTR(self))->info_cat.on ? Qtrue : Qfalse; +} + +/* Document-method: debug? + * + * call-seq: debug?() + * + * Returns true is debug entries are being logged. + */ +static VALUE +log_debugp(VALUE self) { + return ((Server)DATA_PTR(self))->debug_cat.on ? Qtrue : Qfalse; +} + +/* Document-method: eval? + * + * call-seq: eval?() + * + * Returns true is handler evaluation entries are being logged. + */ +static VALUE +log_evalp(VALUE self) { + return ((Server)DATA_PTR(self))->eval_cat.on ? Qtrue : Qfalse; +} + +/* Document-method: error + * + * call-seq: error(msg) + * + * Log an error message. + */ +static VALUE +log_error(VALUE self, VALUE msg) { + log_cat(&((Server)DATA_PTR(self))->error_cat, "%s", StringValuePtr(msg)); + return Qnil; +} + +/* Document-method: warn + * + * call-seq: warn(msg) + * + * Log a warn message. + */ +static VALUE +log_warn(VALUE self, VALUE msg) { + log_cat(&((Server)DATA_PTR(self))->warn_cat, "%s", StringValuePtr(msg)); + return Qnil; +} + +/* Document-method: info + * + * call-seq: info(msg) + * + * Log an info message. + */ +static VALUE +log_info(VALUE self, VALUE msg) { + log_cat(&((Server)DATA_PTR(self))->info_cat, "%s", StringValuePtr(msg)); + return Qnil; +} + +/* Document-method: debug + * + * call-seq: debug(msg) + * + * Log a debug message. + */ +static VALUE +log_debug(VALUE self, VALUE msg) { + log_cat(&((Server)DATA_PTR(self))->debug_cat, "%s", StringValuePtr(msg)); + return Qnil; +} + +/* Document-method: log_eval + * + * call-seq: log_eval(msg) + * + * Log an eval message. + */ +static VALUE +log_eval(VALUE self, VALUE msg) { + log_cat(&((Server)DATA_PTR(self))->eval_cat, "%s", StringValuePtr(msg)); + return Qnil; +} + +/* Document-method: log_state + * + * call-seq: log_state(label) + * + * Return the logging state of the category identified by the _label_. + */ +static VALUE +log_state(VALUE self, VALUE label) { + Server server = (Server)DATA_PTR(self); + LogCat cat = log_cat_find(&server->log, StringValuePtr(label)); + + if (NULL == cat) { + rb_raise(rb_eArgError, "%s is not a valid log category.", StringValuePtr(label)); + } + return cat->on ? Qtrue : Qfalse; +} + +/* Document-method: set_log_state + * + * call-seq: set_log_state(label, state) + * + * Set the logging state of the category identified by the _label_. + */ +static VALUE +set_log_state(VALUE self, VALUE label, VALUE on) { + Server server = (Server)DATA_PTR(self); + LogCat cat = log_cat_find(&server->log, StringValuePtr(label)); + + if (NULL == cat) { + rb_raise(rb_eArgError, "%s is not a valid log category.", StringValuePtr(label)); + } + cat->on = (Qtrue == on); + + return Qnil; +} + +/* Document-method: log_flush + * + * call-seq: log_flush() + * + * Flush the log queue and write all entries to disk or the console. The call + * waits for the flush to complete. + */ +static VALUE +server_log_flush(VALUE self, VALUE to) { + double timeout = NUM2DBL(to); + + if (!log_flush(&((Server)DATA_PTR(self))->log, timeout)) { + rb_raise(rb_eStandardError, "timed out waiting for log flush."); + } + return Qnil; +} + +/* Document-method: handle + * + * call-seq: handle(method, pattern, handler) + * + * Registers a handler for the HTTP method and path pattern specified. The + * path pattern follows glob like rules in that a single * matches a single + * token bounded by the `/` character and a double ** matches all remaining. + */ +static VALUE +handle(VALUE self, VALUE method, VALUE pattern, VALUE handler) { + Server server = (Server)DATA_PTR(self); + Hook hook; + Method meth = ALL; + const char *pat; + + rb_check_type(pattern, T_STRING); + pat = StringValuePtr(pattern); + + if (connect_sym == method) { + meth = CONNECT; + } else if (delete_sym == method) { + meth = DELETE; + } else if (get_sym == method) { + meth = GET; + } else if (head_sym == method) { + meth = HEAD; + } else if (options_sym == method) { + meth = OPTIONS; + } else if (post_sym == method) { + meth = POST; + } else if (put_sym == method) { + meth = PUT; + } else if (Qnil == method) { + meth = ALL; + } else { + rb_raise(rb_eArgError, "invalid method"); + } + if (NULL == (hook = hook_create(meth, pat, handler))) { + rb_raise(rb_eStandardError, "out of memory."); + } else { + Hook h; + Hook prev = NULL; + + for (h = server->hooks; NULL != h; h = h->next) { + prev = h; + } + if (NULL != prev) { + prev->next = hook; + } else { + server->hooks = hook; + } + } + return Qnil; +} + +/* Document-class: Agoo::Server + * + * An HTTP server that support the rack API as well as some other optimized + * APIs for handling HTTP requests. + */ +void +server_init(VALUE mod) { + server_class = rb_define_class_under(mod, "Server", rb_cObject); + + rb_define_module_function(server_class, "new", server_new, -1); + rb_define_method(server_class, "start", start, 0); + rb_define_method(server_class, "shutdown", server_shutdown, 0); + + rb_define_method(server_class, "error?", log_errorp, 0); + rb_define_method(server_class, "warn?", log_warnp, 0); + rb_define_method(server_class, "info?", log_infop, 0); + rb_define_method(server_class, "debug?", log_debugp, 0); + rb_define_method(server_class, "log_eval?", log_evalp, 0); + rb_define_method(server_class, "error", log_error, 1); + rb_define_method(server_class, "warn", log_warn, 1); + rb_define_method(server_class, "info", log_info, 1); + rb_define_method(server_class, "debug", log_debug, 1); + rb_define_method(server_class, "log_eval", log_eval, 1); + + rb_define_method(server_class, "log_state", log_state, 1); + rb_define_method(server_class, "set_log_state", set_log_state, 2); + rb_define_method(server_class, "log_flush", server_log_flush, 1); + + rb_define_method(server_class, "handle", handle, 3); + + call_id = rb_intern("call"); + each_id = rb_intern("each"); + on_request_id = rb_intern("on_request"); + to_i_id = rb_intern("to_i"); + + connect_sym = ID2SYM(rb_intern("CONNECT")); rb_gc_register_address(&connect_sym); + delete_sym = ID2SYM(rb_intern("DELETE")); rb_gc_register_address(&delete_sym); + get_sym = ID2SYM(rb_intern("GET")); rb_gc_register_address(&get_sym); + head_sym = ID2SYM(rb_intern("HEAD")); rb_gc_register_address(&head_sym); + options_sym = ID2SYM(rb_intern("OPTIONS")); rb_gc_register_address(&options_sym); + post_sym = ID2SYM(rb_intern("POST")); rb_gc_register_address(&post_sym); + put_sym = ID2SYM(rb_intern("PUT")); rb_gc_register_address(&put_sym); + + http_init(); + + signal(SIGINT, sig_handler); + signal(SIGTERM, sig_handler); + signal(SIGPIPE, SIG_IGN); +} diff --git a/ext/agoo/server.h b/ext/agoo/server.h new file mode 100644 index 0000000..69ffdc5 --- /dev/null +++ b/ext/agoo/server.h @@ -0,0 +1,47 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_SERVER_H__ +#define __AGOO_SERVER_H__ + +#include +#include +#include + +#include + +#include "hook.h" +#include "log.h" +#include "page.h" +#include "queue.h" + +typedef struct _Server { + volatile bool active; + int thread_cnt; + int port; + bool pedantic; + char *root; + atomic_int running; + pthread_t listen_thread; + pthread_t con_thread; + struct _Log log; + struct _LogCat error_cat; + struct _LogCat warn_cat; + struct _LogCat info_cat; + struct _LogCat debug_cat; + struct _LogCat con_cat; + struct _LogCat req_cat; + struct _LogCat resp_cat; + struct _LogCat eval_cat; + + struct _Queue con_queue; + struct _Cache pages; + + Hook hooks; + struct _Queue eval_queue; + + VALUE *eval_threads; // Qnil terminated +} *Server; + +extern void server_init(VALUE mod); + +#endif // __AGOO_SERVER_H__ diff --git a/ext/agoo/text.c b/ext/agoo/text.c new file mode 100644 index 0000000..69303e0 --- /dev/null +++ b/ext/agoo/text.c @@ -0,0 +1,66 @@ +// Copyright 2016, 2018 by Peter Ohler, All Rights Reserved + +#include +#include + +#include "text.h" + +Text +text_create(const char *str, int len) { + Text t = (Text)malloc(sizeof(struct _Text) - TEXT_MIN_SIZE + len + 1); + + if (NULL != t) { + t->len = len; + t->alen = len; + atomic_init(&t->ref_cnt, 0); + memcpy(t->text, str, len); + t->text[len] = '\0'; + } + return t; +} + +Text +text_allocate(int len) { + Text t = (Text)malloc(sizeof(struct _Text) - TEXT_MIN_SIZE + len + 1); + + if (NULL != t) { + t->len = 0; + t->alen = len; + atomic_init(&t->ref_cnt, 0); + *t->text = '\0'; + } + return t; +} + +void +text_ref(Text t) { + atomic_fetch_add(&t->ref_cnt, 1); +} + +void +text_release(Text t) { + if (1 >= atomic_fetch_sub(&t->ref_cnt, 1)) { + free(t); + } +} + +Text +text_append(Text t, const char *s, int len) { + if (0 >= len) { + len = strlen(s); + } + if (t->alen <= t->len + len) { + long new_len = t->alen + t->alen / 2; + size_t size = sizeof(struct _Text) - TEXT_MIN_SIZE + new_len + 1; + + if (NULL == (t = (Text)realloc(t, size))) { + return NULL; + } + t->alen = new_len; + } + memcpy(t->text + t->len, s, len); + t->len += len; + t->text[t->len] = '\0'; + + return t; +} diff --git a/ext/agoo/text.h b/ext/agoo/text.h new file mode 100644 index 0000000..142b6cb --- /dev/null +++ b/ext/agoo/text.h @@ -0,0 +1,24 @@ +// Copyright 2016, 2018 by Peter Ohler, All Rights Reserved + +#ifndef __AGOO_TEXT_H__ +#define __AGOO_TEXT_H__ + +#include +#include + +#define TEXT_MIN_SIZE 8 + +typedef struct _Text { + long len; // length of valid text + long alen; // size of allocated text + atomic_int ref_cnt; + char text[TEXT_MIN_SIZE]; +} *Text; + +extern Text text_create(const char *str, int len); +extern Text text_allocate(int len); +extern void text_ref(Text t); +extern void text_release(Text t); +extern Text text_append(Text t, const char *s, int len); + +#endif /* __AGOO_TEXT_H__ */ diff --git a/ext/agoo/types.h b/ext/agoo/types.h new file mode 100644 index 0000000..b53e4d0 --- /dev/null +++ b/ext/agoo/types.h @@ -0,0 +1,18 @@ +// Copyright (c) 2018, Peter Ohler, All rights reserved. + +#ifndef __AGOO_TYPES_H__ +#define __AGOO_TYPES_H__ + +typedef enum { + CONNECT = 'C', + DELETE = 'D', + GET = 'G', + HEAD = 'H', + OPTIONS = 'O', + POST = 'P', + PUT = 'U', + ALL = 'A', + NONE = '\0', +} Method; + +#endif // __AGOO_TYPES_H__ diff --git a/lib/agoo.rb b/lib/agoo.rb new file mode 100644 index 0000000..4d66d87 --- /dev/null +++ b/lib/agoo.rb @@ -0,0 +1,9 @@ + +# Agoo is the module that includes an HTTP server. The word agoo is a Japanese +# word for a type of flying fish. +module Agoo +end + +require 'agoo/version' + +require 'agoo/agoo' # C extension diff --git a/lib/agoo/version.rb b/lib/agoo/version.rb new file mode 100644 index 0000000..8df93c8 --- /dev/null +++ b/lib/agoo/version.rb @@ -0,0 +1,5 @@ + +module Agoo + # Agoo version. + VERSION = '0.9.0' +end diff --git a/notes b/notes new file mode 100644 index 0000000..566c45e --- /dev/null +++ b/notes @@ -0,0 +1,23 @@ +;; -*- mode: outline; outline-regexp: " *[-\+]"; indent-tabs-mode: nil; fill-column: 120 -*- + +- refs + - http://www.rubydoc.info/github/rack/rack/master/file/SPEC + +- todo + + - wabur + - add call(env) + + - opo benchmarks on big + - test on mac + - open source it + - if travis is ok then merge to master + - new on opo + +handler + is_a?(WAB::Controller) + or create, read, update, and delete + + +- hijacking - is it needed? + - before and afters for call, pipeline diff --git a/test/base_handler_test.rb b/test/base_handler_test.rb new file mode 100755 index 0000000..8e85442 --- /dev/null +++ b/test/base_handler_test.rb @@ -0,0 +1,170 @@ +#!/usr/bin/env ruby + +$: << File.dirname(__FILE__) +$root_dir = File.dirname(File.expand_path(File.dirname(__FILE__))) +%w(lib ext).each do |dir| + $: << File.join($root_dir, dir) +end + +require 'minitest' +require 'minitest/autorun' +require 'net/http' + +require 'oj' + +require 'agoo' + +class BaseHandlerTest < Minitest::Test + + class TellMeHandler + def initialize + end + + def on_request(req, res) + if 'GET' == req.request_method + res.body = Oj.dump(req.to_h, mode: :null) + elsif 'POST' == req.request_method + res.code = 204 + elsif 'PUT' == req.request_method + res.code = 201 + res.body = req.body + end + end + end + + class WildHandler + def initialize(name) + @name = name + end + + def on_request(req, res) + res.body = "#{@name} - #{req.script_name}" + end + end + + def test_base_handler + begin + server = Agoo::Server.new(6464, 'root', + pedantic: false, + log_dir: '', + thread_count: 1, + log_console: true, + log_classic: true, + log_colorize: true, + log_states: { + INFO: false, + DEBUG: false, + connect: false, + request: false, + response: false, + eval: true, + }) + handler = TellMeHandler.new + server.handle(:GET, "/tellme", handler) + server.handle(:POST, "/makeme", handler) + server.handle(:PUT, "/makeme", handler) + server.handle(:GET, "/wild/*/one", WildHandler.new('one')) + server.handle(:GET, "/wild/all/**", WildHandler.new('all')) + server.start() + + #sleep(100) + eval_test + post_test + put_test + wild_one_test + wild_all_test + ensure + server.shutdown + end + end + + def eval_test + uri = URI('http://localhost:6464/tellme?a=1') + req = Net::HTTP::Get.new(uri) + # Set the headers the way we want them. + req['Accept-Encoding'] = '*' + req['Accept'] = 'application/json' + req['User-Agent'] = 'Ruby' + + res = Net::HTTP.start(uri.hostname, uri.port) { |h| + h.request(req) + } + content = res.body + obj = Oj.load(content, mode: :strict) + + expect = { + "HTTP_Accept" => "application/json", + "HTTP_Accept-Encoding" => "*", + "HTTP_User-Agent" => "Ruby", + "PATH_INFO" => "", + "QUERY_STRING" => "a=1", + "REQUEST_METHOD" => "GET", + "SCRIPT_NAME" => "/tellme", + "SERVER_NAME" => "localhost", + "SERVER_PORT" => "6464", + "rack.errors" => nil, + "rack.input" => nil, + "rack.multiprocess" => false, + "rack.multithread" => false, + "rack.run_once" => false, + "rack.url_scheme" => "http", + "rack.version" => "2.0.3", + } + expect.each_pair { |k,v| + if v.nil? + assert_nil(obj[k], k) + else + assert_equal(v, obj[k], k) + end + } + end + + def post_test + uri = URI('http://localhost:6464/makeme') + req = Net::HTTP::Post.new(uri) + # Set the headers the way we want them. + req['Accept-Encoding'] = '*' + req['Accept'] = 'application/json' + req['User-Agent'] = 'Ruby' + + res = Net::HTTP.start(uri.hostname, uri.port) { |h| + h.request(req) + } + assert_equal(Net::HTTPNoContent, res.class) + end + + def put_test + uri = URI('http://localhost:6464/makeme') + req = Net::HTTP::Put.new(uri) + # Set the headers the way we want them. + req['Accept-Encoding'] = '*' + req['Accept'] = 'application/json' + req['User-Agent'] = 'Ruby' + req.body = 'hello' + + res = Net::HTTP.start(uri.hostname, uri.port) { |h| + h.request(req) + } + assert_equal(Net::HTTPCreated, res.class) + assert_equal('hello', res.body) + end + + def wild_one_test + uri = URI('http://localhost:6464/wild/abc/one') + req = Net::HTTP::Get.new(uri) + res = Net::HTTP.start(uri.hostname, uri.port) { |h| + h.request(req) + } + assert_equal('one - /wild/abc/one', res.body) + end + + def wild_all_test + uri = URI('http://localhost:6464/wild/all/x/y') + req = Net::HTTP::Get.new(uri) + res = Net::HTTP.start(uri.hostname, uri.port) { |h| + h.request(req) + } + assert_equal('all - /wild/all/x/y', res.body) + end + +end diff --git a/test/log_test.rb b/test/log_test.rb new file mode 100755 index 0000000..8639b96 --- /dev/null +++ b/test/log_test.rb @@ -0,0 +1,269 @@ +#!/usr/bin/env ruby + +$: << File.dirname(__FILE__) +$root_dir = File.dirname(File.expand_path(File.dirname(__FILE__))) +%w(lib ext).each do |dir| + $: << File.join($root_dir, dir) +end + +require 'minitest' +require 'minitest/autorun' + +require 'agoo' + +class LogStateTest < Minitest::Test + + def test_log_state + begin + server = Agoo::Server.new(6464, '.') + + error_state_test(server) + warn_state_test(server) + info_state_test(server) + debug_state_test(server) + request_state_test(server) + response_state_test(server) + eval_state_test(server) + ensure + server.shutdown + end + end + + def error_state_test(server) + assert(server.error?) + server.set_log_state('ERROR', false) + refute(server.error?) + server.set_log_state('ERROR', true) + end + + def warn_state_test(server) + assert(server.warn?) + server.set_log_state('WARN', false) + refute(server.warn?) + server.set_log_state('WARN', true) + end + + def info_state_test(server) + refute(server.info?) + server.set_log_state('INFO', true) + assert(server.info?) + end + + def debug_state_test(server) + refute(server.debug?) + server.set_log_state('DEBUG', true) + assert(server.debug?) + server.set_log_state('DEBUG', false) + end + + def request_state_test(server) + refute(server.log_state('request')) + server.set_log_state('request', true) + assert(server.log_state('request')) + server.set_log_state('request', false) + end + + def response_state_test(server) + refute(server.log_state('response')) + server.set_log_state('response', true) + assert(server.log_state('response')) + server.set_log_state('response', false) + end + + def eval_state_test(server) + refute(server.log_state('eval')) + server.set_log_state('eval', true) + assert(server.log_state('eval')) + server.set_log_state('eval', false) + end + + def unknown_state_test(server) + assert_raises(ArgumentError) { + server.log_state('unknown') + } + assert_raises(ArgumentError) { + server.set_log_state('unknown', true) + } + end + +end + +class LogClassicTest < Minitest::Test + + def test_log_classic + `rm -rf log` + begin + server = Agoo::Server.new(6464, '.', + log_dir: 'log', + log_console: false, + log_classic: true, + log_colorize: false, + log_states: { + INFO: true, + DEBUG: true, + eval: true, + }) + error_test(server) + warn_test(server) + info_test(server) + debug_test(server) + log_eval_test(server) + ensure + server.shutdown + end + end + + def error_test(server) + server.error('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log') + assert_match(/ERROR: my message/, content) + end + + def warn_test(server) + server.warn('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log') + assert_match(/WARN: my message/, content) + end + + def info_test(server) + server.info('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log') + assert_match(/INFO: my message/, content) + end + + def debug_test(server) + server.debug('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log') + assert_match(/DEBUG: my message/, content) + end + + def log_eval_test(server) + server.log_eval('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log') + assert_match(/eval: my message/, content) + end + +end + +class LogJsonTest < Minitest::Test + + def test_log_json + `rm -rf log` + begin + server = Agoo::Server.new(6464, '.', + log_dir: 'log', + log_max_files: 2, + log_max_size: 10, + log_console: false, + log_classic: false, + log_colorize: false, + log_states: { + INFO: true, + DEBUG: true, + eval: true, + }) + error_test(server) + warn_test(server) + info_test(server) + debug_test(server) + eval_test(server) + ensure + server.shutdown + end + end + + def error_test(server) + server.error('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log.1') + assert_match(/"where":"ERROR"/, content) + assert_match(/"level":1/, content) + assert_match(/"what":"my message"/, content) + end + + def warn_test(server) + server.warn('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log.1') + assert_match(/"where":"WARN"/, content) + assert_match(/"level":2/, content) + assert_match(/"what":"my message"/, content) + end + + def info_test(server) + server.info('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log.1') + assert_match(/"where":"INFO"/, content) + assert_match(/"level":3/, content) + assert_match(/"what":"my message"/, content) + end + + def debug_test(server) + server.debug('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log.1') + assert_match(/"where":"DEBUG"/, content) + assert_match(/"level":4/, content) + assert_match(/"what":"my message"/, content) + end + + def eval_test(server) + server.log_eval('my message') + server.log_flush(1.0) + content = IO.read('log/agoo.log.1') + assert_match(/"where":"eval"/, content) + assert_match(/"level":3/, content) + assert_match(/"what":"my message"/, content) + end + +end + +class LogRollTest < Minitest::Test + + def setup + `rm -rf log` + begin + server = Agoo::Server.new(6464, '.', + log_dir: 'log', + log_max_files: 2, + log_max_size: 150, + log_console: false, + log_classic: true, + log_colorize: false, + log_states: { + INFO: true, + DEBUG: true, + eval: true, + }) + 10.times { |i| + @server.info("my #{i} message") + } + @server.log_flush(1.0) + content = IO.read('log/agoo.log') + assert_match(/INFO: my 9 message/, content) + + content = IO.read('log/agoo.log.1') + assert_match(/INFO: my 6 message/, content) + assert_match(/INFO: my 7 message/, content) + assert_match(/INFO: my 8 message/, content) + + content = IO.read('log/agoo.log.2') + assert_match(/INFO: my 3 message/, content) + assert_match(/INFO: my 4 message/, content) + assert_match(/INFO: my 5 message/, content) + + assert_raises() { + IO.read('log/agoo.log.3') + } + ensure + @server.shutdown + end + end + +end diff --git a/test/rack_handler_test.rb b/test/rack_handler_test.rb new file mode 100755 index 0000000..4f81e0f --- /dev/null +++ b/test/rack_handler_test.rb @@ -0,0 +1,147 @@ +#!/usr/bin/env ruby + +$: << File.dirname(__FILE__) +$root_dir = File.dirname(File.expand_path(File.dirname(__FILE__))) +%w(lib ext).each do |dir| + $: << File.join($root_dir, dir) +end + +require 'minitest' +require 'minitest/autorun' +require 'net/http' + +require 'oj' + +require 'agoo' + +class RackHandlerTest < Minitest::Test + + class TellMeHandler + def initialize + end + + def call(req) + if 'GET' == req['REQUEST_METHOD'] + [ 200, + { 'Content-Type' => 'application/json' }, + [ Oj.dump(req.to_h, mode: :null) ] + ] + elsif 'POST' == req['REQUEST_METHOD'] + [ 204, { }, [] ] + elsif 'PUT' == req['REQUEST_METHOD'] + [ 201, + { }, + [ req['rack.input'].read ] + ] + else + [ 404, {}, []] + end + + end + end + + def test_rack + begin + server = Agoo::Server.new(6465, 'root', + pedantic: false, + log_dir: '', + thread_count: 1, + log_console: true, + log_classic: true, + log_colorize: true, + log_states: { + INFO: false, + DEBUG: false, + connect: false, + request: false, + response: false, + eval: true, + }) + #sleep(100) + + handler = TellMeHandler.new + server.handle(:GET, "/tellme", handler) + server.handle(:POST, "/makeme", handler) + server.handle(:PUT, "/makeme", handler) + server.start() + + eval_test + post_test + put_test + ensure + server.shutdown + end + end + + def eval_test + uri = URI('http://localhost:6465/tellme?a=1') + req = Net::HTTP::Get.new(uri) + # Set the headers the way we want them. + req['Accept-Encoding'] = '*' + req['Accept'] = 'application/json' + req['User-Agent'] = 'Ruby' + + res = Net::HTTP.start(uri.hostname, uri.port) { |h| + h.request(req) + } + content = res.body + obj = Oj.load(content, mode: :strict) + + expect = { + "HTTP_Accept" => "application/json", + "HTTP_Accept-Encoding" => "*", + "HTTP_User-Agent" => "Ruby", + "PATH_INFO" => "", + "QUERY_STRING" => "a=1", + "REQUEST_METHOD" => "GET", + "SCRIPT_NAME" => "/tellme", + "SERVER_NAME" => "localhost", + "SERVER_PORT" => "6465", + "rack.errors" => nil, + "rack.input" => nil, + "rack.multiprocess" => false, + "rack.multithread" => false, + "rack.run_once" => false, + "rack.url_scheme" => "http", + "rack.version" => "2.0.3", + } + expect.each_pair { |k,v| + if v.nil? + assert_nil(obj[k], k) + else + assert_equal(v, obj[k], k) + end + } + end + + def post_test + uri = URI('http://localhost:6465/makeme') + req = Net::HTTP::Post.new(uri) + # Set the headers the way we want them. + req['Accept-Encoding'] = '*' + req['Accept'] = 'application/json' + req['User-Agent'] = 'Ruby' + + res = Net::HTTP.start(uri.hostname, uri.port) { |h| + h.request(req) + } + assert_equal(Net::HTTPNoContent, res.class) + end + + def put_test + uri = URI('http://localhost:6465/makeme') + req = Net::HTTP::Put.new(uri) + # Set the headers the way we want them. + req['Accept-Encoding'] = '*' + req['Accept'] = 'application/json' + req['User-Agent'] = 'Ruby' + req.body = 'hello' + + res = Net::HTTP.start(uri.hostname, uri.port) { |h| + h.request(req) + } + assert_equal(Net::HTTPCreated, res.class) + assert_equal('hello', res.body) + end + +end diff --git a/test/root/index.html b/test/root/index.html new file mode 100644 index 0000000..892cf82 --- /dev/null +++ b/test/root/index.html @@ -0,0 +1,5 @@ + + + Agoo Test + Agoo + diff --git a/test/root/nest/something.txt b/test/root/nest/something.txt new file mode 100644 index 0000000..8e4e7a2 --- /dev/null +++ b/test/root/nest/something.txt @@ -0,0 +1 @@ +Just some text. diff --git a/test/static_test.rb b/test/static_test.rb new file mode 100755 index 0000000..e7b5ad1 --- /dev/null +++ b/test/static_test.rb @@ -0,0 +1,81 @@ +#!/usr/bin/env ruby + +$: << File.dirname(__FILE__) +$root_dir = File.dirname(File.expand_path(File.dirname(__FILE__))) +%w(lib ext).each do |dir| + $: << File.join($root_dir, dir) +end + +require 'minitest' +require 'minitest/autorun' +require 'net/http' + +require 'agoo' + +class StaticTest < Minitest::Test + + # Run all the tests in one test to avoid creating the server multiple times. + def test_static + begin + server = Agoo::Server.new(6466, 'root', + log_dir: '', + log_console: true, + log_classic: true, + log_colorize: true, + log_states: { + INFO: false, + DEBUG: false, + connect: false, + request: false, + response: false, + eval: true, + }) + server.start() + fetch_index_test + fetch_auto_index_test + fetch_nested_test + fetch_not_found_test + ensure + server.shutdown + end + end + + + def fetch_index_test + uri = URI('http://localhost:6466/index.html') + content = Net::HTTP.get(uri) + expect = %| + + Agoo Test + Agoo + +| + assert_equal(expect, content) + end + + def fetch_auto_index_test + uri = URI('http://localhost:6466/') + content = Net::HTTP.get(uri) + expect = %| + + Agoo Test + Agoo + +| + assert_equal(expect, content) + end + + def fetch_nested_test + uri = URI('http://localhost:6466/nest/something.txt') + content = Net::HTTP.get(uri) + assert_equal('Just some text. +', content) + end + + def fetch_not_found_test + uri = URI('http://localhost:6466/bad.html') + res = Net::HTTP.get_response(uri) + assert_equal("404", res.code) + end + +end diff --git a/test/tests.rb b/test/tests.rb new file mode 100755 index 0000000..e3fdc4d --- /dev/null +++ b/test/tests.rb @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby + +$: << File.dirname(__FILE__) + +require 'log_test' +require 'static_test' +require 'base_handler_test' +require 'rack_handler_test'