This is an automated email from the ASF dual-hosted git repository. sorber pushed a commit to branch master in repository https://git-dual.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push: new f949aed TS-4320: Add ACME Plugin f949aed is described below commit f949aed3adbf92620b539e19062bc6a8a0ef54ef Author: Phil Sorber <sor...@apache.org> AuthorDate: Mon Apr 11 12:37:52 2016 -0600 TS-4320: Add ACME Plugin --- configure.ac | 1 + plugins/experimental/Makefile.am | 1 + plugins/experimental/{ => acme}/Makefile.am | 54 +---- plugins/experimental/acme/README | 5 + plugins/experimental/acme/acme.c | 349 ++++++++++++++++++++++++++++ 5 files changed, 360 insertions(+), 50 deletions(-) diff --git a/configure.ac b/configure.ac index d9d6049..3d38e7d 100644 --- a/configure.ac +++ b/configure.ac @@ -1973,6 +1973,7 @@ AC_CONFIG_FILES([ plugins/cacheurl/Makefile plugins/conf_remap/Makefile plugins/experimental/Makefile + plugins/experimental/acme/Makefile plugins/experimental/authproxy/Makefile plugins/experimental/background_fetch/Makefile plugins/experimental/balancer/Makefile diff --git a/plugins/experimental/Makefile.am b/plugins/experimental/Makefile.am index dd73193..a530a38 100644 --- a/plugins/experimental/Makefile.am +++ b/plugins/experimental/Makefile.am @@ -15,6 +15,7 @@ # limitations under the License. SUBDIRS = \ + acme \ authproxy \ background_fetch \ balancer \ diff --git a/plugins/experimental/Makefile.am b/plugins/experimental/acme/Makefile.am similarity index 52% copy from plugins/experimental/Makefile.am copy to plugins/experimental/acme/Makefile.am index dd73193..3cf70c5 100644 --- a/plugins/experimental/Makefile.am +++ b/plugins/experimental/acme/Makefile.am @@ -14,54 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -SUBDIRS = \ - authproxy \ - background_fetch \ - balancer \ - buffer_upload \ - cachekey \ - cache_range_requests \ - cache_promote \ - collapsed_connection \ - collapsed_forwarding \ - custom_redirect \ - epic \ - escalate \ - esi \ - generator \ - geoip_acl \ - header_normalize \ - hipes \ - inliner \ - metalink \ - multiplexer \ - memcache \ - memcached_remap \ - regex_revalidate \ - remap_stats \ - s3_auth \ - ssl_cert_loader \ - sslheaders \ - stale_while_revalidate \ - url_sig \ - xdebug \ - mp4 \ - stream_editor +include $(top_srcdir)/build/plugins.mk -if ENABLE_CPPAPI -if BUILD_WEBP_TRANSFORM_PLUGIN - SUBDIRS += webp_transform -endif -endif - -if HAS_MYSQL - SUBDIRS += mysql_remap -endif - -if BUILD_LUAJIT - SUBDIRS += ts_lua -endif - -if HAS_KYOTOCABINET - SUBDIRS += cache_key_genid -endif +pkglib_LTLIBRARIES = acme.la +acme_la_SOURCES = acme.c +acme_la_LDFLAGS = $(TS_PLUGIN_LDFLAGS) diff --git a/plugins/experimental/acme/README b/plugins/experimental/acme/README new file mode 100644 index 0000000..4e6536e --- /dev/null +++ b/plugins/experimental/acme/README @@ -0,0 +1,5 @@ +Plugin to support the ACME protocol + +https://github.com/ietf-wg-acme/acme + +https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment diff --git a/plugins/experimental/acme/acme.c b/plugins/experimental/acme/acme.c new file mode 100644 index 0000000..cb0244f --- /dev/null +++ b/plugins/experimental/acme/acme.c @@ -0,0 +1,349 @@ +/** @file + +@section license + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <ctype.h> +#include <string.h> +#include <sys/types.h> +#include <unistd.h> +#include <getopt.h> +#include <sys/stat.h> + +#include "ts/ts.h" +#include "ts/ink_platform.h" +#include "ts/ink_defs.h" + +static const char PLUGIN_NAME[] = "acme"; +static const char ACME_WK_PATH[] = ".well-known/acme-challenge/"; +static const char ACME_OK_RESP[] = "HTTP/1.1 200 OK\r\nContent-Type: application/jose\r\nCache-Control: no-cache\r\n"; +static const char ACME_DENIED_RESP[] = "HTTP/1.1 404 Not Found\r\nContent-Type: application/jose\r\nCache-Control: no-cache\r\n"; + +#define MAX_PATH_LEN 4096 + +/* This should hold all configurations going forward. */ +typedef struct AcmeConfig_t { + char *proof; +} AcmeConfig; + +static AcmeConfig gConfig; + +/* State used for the intercept plugin. ToDo: Can this be improved ? */ +typedef struct AcmeState_t { + TSVConn net_vc; + TSVIO read_vio; + TSVIO write_vio; + + TSIOBuffer req_buffer; + TSIOBuffer resp_buffer; + TSIOBufferReader resp_reader; + + int output_bytes; + int fd; + struct stat stat_buf; +} AcmeState; + +inline static AcmeState * +make_acme_state() +{ + AcmeState *state = (AcmeState *)TSmalloc(sizeof(AcmeState)); + + memset(state, 0, sizeof(AcmeState)); + state->fd = -1; + + return state; +} + +/* Create a safe pathname to the proof-type file, the destination must be sufficiently large. */ +static size_t +make_absolute_path(char *dest, int dest_len, const char *file, int file_len) +{ + int i; + + for (i = 0; i < file_len; ++i) { + char c = file[i]; + + /* Assure that only Base64-URL chracter are in the path */ + if (!(c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) { + TSDebug(PLUGIN_NAME, "Invalid Base64 character found, error"); + return 0; + } + } + + return snprintf(dest, dest_len - 1, "%s/%.*s", gConfig.proof, file_len, file); +} + +static void +open_acme_file(AcmeState *state, const char *file, int file_len) +{ + char fname[MAX_PATH_LEN]; + int len = make_absolute_path(fname, MAX_PATH_LEN - 1, file, file_len); + + /* 1. Make sure the filename is reasonable */ + if (!len || (len >= (MAX_PATH_LEN - 1))) { + TSDebug(PLUGIN_NAME, "invalid filename"); + return; + } + + /* 2. Open the file */ + state->fd = open(fname, O_RDONLY); + if (-1 == state->fd) { + TSDebug(PLUGIN_NAME, "can not open file %s (%s)", fname, strerror(errno)); + return; + } + + /* 3. stat() the file */ + if (fstat(state->fd, &state->stat_buf)) { + TSDebug(PLUGIN_NAME, "can not stat() file %s (%s)", fname, strerror(errno)); + close(state->fd); + state->fd = -1; + return; + } + + TSDebug(PLUGIN_NAME, "opened filename of %s for read()", fname); + return; +} + +/* Cleanup after intercept has completed */ +static void +cleanup(TSCont contp, AcmeState *my_state) +{ + if (my_state->req_buffer) { + TSIOBufferDestroy(my_state->req_buffer); + my_state->req_buffer = NULL; + } + + if (my_state->resp_buffer) { + TSIOBufferDestroy(my_state->resp_buffer); + my_state->resp_buffer = NULL; + } + + TSVConnClose(my_state->net_vc); + TSfree(my_state); + TSContDestroy(contp); +} + +/* Add data to the output */ +inline static int +add_data_to_resp(const char *buf, int len, AcmeState *my_state) +{ + TSIOBufferWrite(my_state->resp_buffer, buf, len); + return len; +} + +static int +add_file_to_resp(AcmeState *my_state) +{ + if (-1 == my_state->fd) { + return add_data_to_resp("\r\n", 2, my_state); + } else { + int ret = 0, len; + char buf[8192]; + + while (1) { + len = read(my_state->fd, buf, sizeof(buf)); + if ((0 == len) || ((-1 == len) && (errno != EAGAIN) && (errno != EINTR))) { + break; + } else { + TSIOBufferWrite(my_state->resp_buffer, buf, len); + ret += len; + } + } + close(my_state->fd); + my_state->fd = -1; + + return ret; + } +} + +/* Process a read event from the SM */ +static void +acme_process_read(TSCont contp, TSEvent event, AcmeState *my_state) +{ + if (event == TS_EVENT_VCONN_READ_READY) { + if (-1 == my_state->fd) { + my_state->output_bytes = add_data_to_resp(ACME_DENIED_RESP, strlen(ACME_DENIED_RESP), my_state); + } else { + my_state->output_bytes = add_data_to_resp(ACME_OK_RESP, strlen(ACME_OK_RESP), my_state); + } + TSVConnShutdown(my_state->net_vc, 1, 0); + my_state->write_vio = TSVConnWrite(my_state->net_vc, contp, my_state->resp_reader, INT64_MAX); + } else if (event == TS_EVENT_ERROR) { + TSError("[%s] acme_process_read: Received TS_EVENT_ERROR", PLUGIN_NAME); + } else if (event == TS_EVENT_VCONN_EOS) { + /* client may end the connection, simply return */ + return; + } else if (event == TS_EVENT_NET_ACCEPT_FAILED) { + TSError("[%s] acme_process_read: Received TS_EVENT_NET_ACCEPT_FAILED", PLUGIN_NAME); + } else { + TSReleaseAssert(!"Unexpected Event"); + } +} + +/* Process a write event from the SM */ +static void +acme_process_write(TSCont contp, TSEvent event, AcmeState *my_state) +{ + if (event == TS_EVENT_VCONN_WRITE_READY) { + char buf[64]; /* Plenty of space for CL: header */ + int len; + + len = snprintf(buf, sizeof(buf) - 1, "Content-Length: %zd\r\n\r\n", my_state->stat_buf.st_size); + my_state->output_bytes += add_data_to_resp(buf, len, my_state); + my_state->output_bytes += add_file_to_resp(my_state); + + TSVIONBytesSet(my_state->write_vio, my_state->output_bytes); + TSVIOReenable(my_state->write_vio); + } else if (TS_EVENT_VCONN_WRITE_COMPLETE) { + cleanup(contp, my_state); + } else if (event == TS_EVENT_ERROR) { + TSError("[%s] acme_process_write: Received TS_EVENT_ERROR", PLUGIN_NAME); + } else { + TSReleaseAssert(!"Unexpected Event"); + } +} + +/* Process the accept event from the SM */ +static void +acme_process_accept(TSCont contp, AcmeState *my_state) +{ + my_state->req_buffer = TSIOBufferCreate(); + my_state->resp_buffer = TSIOBufferCreate(); + my_state->resp_reader = TSIOBufferReaderAlloc(my_state->resp_buffer); + my_state->read_vio = TSVConnRead(my_state->net_vc, contp, my_state->req_buffer, INT64_MAX); +} + +/* Implement the server intercept */ +static int +acme_intercept(TSCont contp, TSEvent event, void *edata) +{ + AcmeState *my_state = TSContDataGet(contp); + + if (event == TS_EVENT_NET_ACCEPT) { + my_state->net_vc = (TSVConn)edata; + acme_process_accept(contp, my_state); + } else if (edata == my_state->read_vio) { /* All read events */ + acme_process_read(contp, event, my_state); + } else if (edata == my_state->write_vio) { /* All write events */ + acme_process_write(contp, event, my_state); + } else { + TSReleaseAssert(!"Unexpected Event"); + } + + return 0; +} + +/* Read-request header continuation, used to kick off the server intercept if necessary */ +static int +acme_hook(TSCont contp ATS_UNUSED, TSEvent event ATS_UNUSED, void *edata) +{ + TSMBuffer reqp; + TSMLoc hdr_loc = NULL, url_loc = NULL; + TSCont icontp; + AcmeState *my_state; + TSHttpTxn txnp = (TSHttpTxn)edata; + + TSDebug(PLUGIN_NAME, "kicking off ACME hook"); + + if ((TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &reqp, &hdr_loc)) && (TS_SUCCESS == TSHttpHdrUrlGet(reqp, hdr_loc, &url_loc))) { + int path_len = 0; + const char *path = TSUrlPathGet(reqp, url_loc, &path_len); + + /* Short circuit the / path, common case */ + if (!path || path_len < (strlen(ACME_WK_PATH) + 2) || *path != '.' || memcmp(path, ACME_WK_PATH, strlen(ACME_WK_PATH))) { + TSDebug(PLUGIN_NAME, "skipping URL path = %.*s", path_len, path); + goto cleanup; + } + + TSSkipRemappingSet(txnp, 1); /* not strictly necessary, but speed is everything these days */ + + /* This request is for us -- register our intercept */ + icontp = TSContCreate(acme_intercept, TSMutexCreate()); + + my_state = make_acme_state(); + open_acme_file(my_state, path + strlen(ACME_WK_PATH), path_len - strlen(ACME_WK_PATH)); + + TSContDataSet(icontp, my_state); + TSHttpTxnIntercept(icontp, txnp); + TSDebug(PLUGIN_NAME, "created intercept hook"); + } + +cleanup: + if (url_loc) { + TSHandleMLocRelease(reqp, hdr_loc, url_loc); + } + if (hdr_loc) { + TSHandleMLocRelease(reqp, TS_NULL_MLOC, hdr_loc); + } + + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); + + return 0; +} + +/* Initialize the plugin / global continuation hook */ +void +TSPluginInit(int argc, const char *argv[]) +{ + TSPluginRegistrationInfo info; + const char *proof = "acme"; + + static const struct option longopt[] = { + {(char *)"proof-directory", optional_argument, NULL, 'p'}, {NULL, no_argument, NULL, '\0'}, + }; + + memset(&gConfig, 0, sizeof(gConfig)); + optind = 0; + while (true) { + int opt = getopt_long(argc, (char *const *)argv, "", longopt, NULL); + + switch (opt) { + case 'p': + proof = optarg; + break; + } + + if (opt == -1) { + break; + } + } + + if ('/' != *proof) { + const char *confdir = TSConfigDirGet(); + int len = strlen(proof) + strlen(confdir) + 8; + + gConfig.proof = TSmalloc(len); + snprintf(gConfig.proof, len - 1, "%s/%s", confdir, proof); + TSDebug(PLUGIN_NAME, "base directory for proof-types is %s", gConfig.proof); + } else { + gConfig.proof = TSstrdup(proof); + } + + info.plugin_name = "acme"; + info.vendor_name = "Apache Software Foundation"; + info.support_email = "d...@trafficserver.apache.org"; + + if (TS_SUCCESS != TSPluginRegister(&info)) { + TSError("[%s] Plugin registration failed.", PLUGIN_NAME); + return; + } + + TSDebug(PLUGIN_NAME, "Started the %s plugin", PLUGIN_NAME); + TSDebug(PLUGIN_NAME, "\tproof-type dir = %s", gConfig.proof); + + TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, TSContCreate(acme_hook, NULL)); +} -- To stop receiving notification emails like this one, please contact ['"commits@trafficserver.apache.org" <commits@trafficserver.apache.org>'].