Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions plugins/experimental/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# limitations under the License.

SUBDIRS = \
acme \
authproxy \
background_fetch \
balancer \
Expand Down
21 changes: 21 additions & 0 deletions plugins/experimental/acme/Makefile.am
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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 $(top_srcdir)/build/plugins.mk

pkglib_LTLIBRARIES = acme.la
acme_la_SOURCES = acme.c
acme_la_LDFLAGS = $(TS_PLUGIN_LDFLAGS)
5 changes: 5 additions & 0 deletions plugins/experimental/acme/README
Original file line number Diff line number Diff line change
@@ -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
349 changes: 349 additions & 0 deletions plugins/experimental/acme/acme.c
Original file line number Diff line number Diff line change
@@ -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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-cache (https://tools.ietf.org/html/rfc7234#section-5.2.1.4) or no-store (https://tools.ietf.org/html/rfc7234#section-5.2.1.5)? Eg, cache it, but validate before serving from cache, or don't cache it at all?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, lets make it CC: no-store,private. This likely is in several other plugins that this is stole^h^h^h borrowed from. Perhaps file a Jira on fixing those as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, we should make a separate JIRA and get all these at once. I'll leave this be for now.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

struct AcemeState

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;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Close the file descriptor?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What file descriptor? It should have been closed elsewhere, no ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeh as I read further I saw where it was closed. Since this is the destructor, I was expecting to see it here ... maybe add an assertion if you don't want to verify the close?

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference would be to not abbreviate, i.e. add_data_to_response().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bike shedding.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

{
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a TODO to use the TSAIO API?

if ((0 == len) || ((-1 == len) && (errno != EAGAIN) && (errno != EINTR))) {
break;
} else {
TSIOBufferWrite(my_state->resp_buffer, buf, len);
ret += len;
}
}
close(my_state->fd);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my_state->fd = -1;

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))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can replace the strlen with COUNTOF(ACME_WK_PATH) - 1.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strlen() seems more C-like, and the compiler will optimize this to a static (and, we avoid the -1).

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 = "dev@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));
}