1 /** @file
2 
3 @section license
4 
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8 
9 http://www.apache.org/licenses/LICENSE-2.0
10 
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16  */
17 
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <ctype.h>
21 #include <string.h>
22 #include <sys/types.h>
23 #include <unistd.h>
24 #include <getopt.h>
25 #include <sys/stat.h>
26 
27 #include "ts/ts.h"
28 #include "tscore/ink_platform.h"
29 #include "tscore/ink_defs.h"
30 
31 static const char PLUGIN_NAME[]      = "acme";
32 static const char ACME_WK_PATH[]     = ".well-known/acme-challenge/";
33 static const char ACME_OK_RESP[]     = "HTTP/1.1 200 OK\r\nContent-Type: application/jose\r\nCache-Control: no-cache\r\n";
34 static const char ACME_DENIED_RESP[] = "HTTP/1.1 404 Not Found\r\nContent-Type: application/jose\r\nCache-Control: no-cache\r\n";
35 
36 #define MAX_PATH_LEN 4096
37 
38 /* This should hold all configurations going forward. */
39 typedef struct AcmeConfig_t {
40   char *proof;
41 } AcmeConfig;
42 
43 static AcmeConfig gConfig;
44 
45 /* State used for the intercept plugin. ToDo: Can this be improved ? */
46 typedef struct AcmeState_t {
47   TSVConn net_vc;
48   TSVIO read_vio;
49   TSVIO write_vio;
50 
51   TSIOBuffer req_buffer;
52   TSIOBuffer resp_buffer;
53   TSIOBufferReader resp_reader;
54 
55   int output_bytes;
56   int fd;
57   struct stat stat_buf;
58 } AcmeState;
59 
60 inline static AcmeState *
make_acme_state()61 make_acme_state()
62 {
63   AcmeState *state = (AcmeState *)TSmalloc(sizeof(AcmeState));
64 
65   memset(state, 0, sizeof(AcmeState));
66   state->fd = -1;
67 
68   return state;
69 }
70 
71 /* Create a safe pathname to the proof-type file, the destination must be sufficiently large. */
72 static size_t
make_absolute_path(char * dest,int dest_len,const char * file,int file_len)73 make_absolute_path(char *dest, int dest_len, const char *file, int file_len)
74 {
75   int i;
76 
77   for (i = 0; i < file_len; ++i) {
78     char c = file[i];
79 
80     /* Assure that only Base64-URL character are in the path */
81     if (!(c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) {
82       TSDebug(PLUGIN_NAME, "Invalid Base64 character found, error");
83       return 0;
84     }
85   }
86 
87   return snprintf(dest, dest_len, "%s/%.*s", gConfig.proof, file_len, file);
88 }
89 
90 static void
open_acme_file(AcmeState * state,const char * file,int file_len)91 open_acme_file(AcmeState *state, const char *file, int file_len)
92 {
93   char fname[MAX_PATH_LEN];
94   int len = make_absolute_path(fname, MAX_PATH_LEN - 1, file, file_len);
95 
96   /* 1. Make sure the filename is reasonable */
97   if (!len || (len >= (MAX_PATH_LEN - 1))) {
98     TSDebug(PLUGIN_NAME, "invalid filename");
99     return;
100   }
101 
102   /* 2. Open the file */
103   state->fd = open(fname, O_RDONLY);
104   if (-1 == state->fd) {
105     TSDebug(PLUGIN_NAME, "can not open file %s (%s)", fname, strerror(errno));
106     return;
107   }
108 
109   /* 3. stat() the file */
110   if (fstat(state->fd, &state->stat_buf)) {
111     TSDebug(PLUGIN_NAME, "can not stat() file %s (%s)", fname, strerror(errno));
112     close(state->fd);
113     state->fd = -1;
114     return;
115   }
116 
117   TSDebug(PLUGIN_NAME, "opened filename of %s for read()", fname);
118   return;
119 }
120 
121 /* Cleanup after intercept has completed */
122 static void
cleanup(TSCont contp,AcmeState * my_state)123 cleanup(TSCont contp, AcmeState *my_state)
124 {
125   if (my_state->req_buffer) {
126     TSIOBufferDestroy(my_state->req_buffer);
127     my_state->req_buffer = NULL;
128   }
129 
130   if (my_state->resp_buffer) {
131     TSIOBufferDestroy(my_state->resp_buffer);
132     my_state->resp_buffer = NULL;
133   }
134 
135   TSVConnClose(my_state->net_vc);
136   TSfree(my_state);
137   TSContDestroy(contp);
138 }
139 
140 /* Add data to the output */
141 inline static int
add_data_to_resp(const char * buf,int len,AcmeState * my_state)142 add_data_to_resp(const char *buf, int len, AcmeState *my_state)
143 {
144   TSIOBufferWrite(my_state->resp_buffer, buf, len);
145   return len;
146 }
147 
148 static int
add_file_to_resp(AcmeState * my_state)149 add_file_to_resp(AcmeState *my_state)
150 {
151   if (-1 == my_state->fd) {
152     return add_data_to_resp("\r\n", 2, my_state);
153   } else {
154     int ret = 0, len;
155     char buf[8192];
156 
157     while (1) {
158       len = read(my_state->fd, buf, sizeof(buf));
159       if ((0 == len) || ((-1 == len) && (errno != EAGAIN) && (errno != EINTR))) {
160         break;
161       } else {
162         TSIOBufferWrite(my_state->resp_buffer, buf, len);
163         ret += len;
164       }
165     }
166     close(my_state->fd);
167     my_state->fd = -1;
168 
169     return ret;
170   }
171 }
172 
173 /* Process a read event from the SM */
174 static void
acme_process_read(TSCont contp,TSEvent event,AcmeState * my_state)175 acme_process_read(TSCont contp, TSEvent event, AcmeState *my_state)
176 {
177   if (event == TS_EVENT_VCONN_READ_READY) {
178     if (-1 == my_state->fd) {
179       my_state->output_bytes = add_data_to_resp(ACME_DENIED_RESP, strlen(ACME_DENIED_RESP), my_state);
180     } else {
181       my_state->output_bytes = add_data_to_resp(ACME_OK_RESP, strlen(ACME_OK_RESP), my_state);
182     }
183     TSVConnShutdown(my_state->net_vc, 1, 0);
184     my_state->write_vio = TSVConnWrite(my_state->net_vc, contp, my_state->resp_reader, INT64_MAX);
185   } else if (event == TS_EVENT_ERROR) {
186     TSError("[%s] acme_process_read: Received TS_EVENT_ERROR", PLUGIN_NAME);
187   } else if (event == TS_EVENT_VCONN_EOS) {
188     /* client may end the connection, simply return */
189     return;
190   } else if (event == TS_EVENT_NET_ACCEPT_FAILED) {
191     TSError("[%s] acme_process_read: Received TS_EVENT_NET_ACCEPT_FAILED", PLUGIN_NAME);
192   } else {
193     TSReleaseAssert(!"Unexpected Event");
194   }
195 }
196 
197 /* Process a write event from the SM */
198 static void
acme_process_write(TSCont contp,TSEvent event,AcmeState * my_state)199 acme_process_write(TSCont contp, TSEvent event, AcmeState *my_state)
200 {
201   if (event == TS_EVENT_VCONN_WRITE_READY) {
202     char buf[64]; /* Plenty of space for CL: header */
203     int len;
204 
205     len = snprintf(buf, sizeof(buf), "Content-Length: %zd\r\n\r\n", (size_t)my_state->stat_buf.st_size);
206     my_state->output_bytes += add_data_to_resp(buf, len, my_state);
207     my_state->output_bytes += add_file_to_resp(my_state);
208 
209     TSVIONBytesSet(my_state->write_vio, my_state->output_bytes);
210     TSVIOReenable(my_state->write_vio);
211   } else if (event == TS_EVENT_VCONN_WRITE_COMPLETE) {
212     cleanup(contp, my_state);
213   } else if (event == TS_EVENT_ERROR) {
214     TSError("[%s] acme_process_write: Received TS_EVENT_ERROR", PLUGIN_NAME);
215   } else {
216     TSReleaseAssert(!"Unexpected Event");
217   }
218 }
219 
220 /* Process the accept event from the SM */
221 static void
acme_process_accept(TSCont contp,AcmeState * my_state)222 acme_process_accept(TSCont contp, AcmeState *my_state)
223 {
224   my_state->req_buffer  = TSIOBufferCreate();
225   my_state->resp_buffer = TSIOBufferCreate();
226   my_state->resp_reader = TSIOBufferReaderAlloc(my_state->resp_buffer);
227   my_state->read_vio    = TSVConnRead(my_state->net_vc, contp, my_state->req_buffer, INT64_MAX);
228 }
229 
230 /* Implement the server intercept */
231 static int
acme_intercept(TSCont contp,TSEvent event,void * edata)232 acme_intercept(TSCont contp, TSEvent event, void *edata)
233 {
234   AcmeState *my_state = TSContDataGet(contp);
235 
236   if (event == TS_EVENT_NET_ACCEPT) {
237     my_state->net_vc = (TSVConn)edata;
238     acme_process_accept(contp, my_state);
239   } else if (edata == my_state->read_vio) { /* All read events */
240     acme_process_read(contp, event, my_state);
241   } else if (edata == my_state->write_vio) { /* All write events */
242     acme_process_write(contp, event, my_state);
243   } else {
244     TSReleaseAssert(!"Unexpected Event");
245   }
246 
247   return 0;
248 }
249 
250 /* Read-request header continuation, used to kick off the server intercept if necessary */
251 static int
acme_hook(TSCont contp ATS_UNUSED,TSEvent event ATS_UNUSED,void * edata)252 acme_hook(TSCont contp ATS_UNUSED, TSEvent event ATS_UNUSED, void *edata)
253 {
254   TSMBuffer reqp;
255   TSMLoc hdr_loc = NULL, url_loc = NULL;
256   TSCont icontp;
257   AcmeState *my_state;
258   TSHttpTxn txnp = (TSHttpTxn)edata;
259 
260   TSDebug(PLUGIN_NAME, "kicking off ACME hook");
261 
262   if ((TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &reqp, &hdr_loc)) && (TS_SUCCESS == TSHttpHdrUrlGet(reqp, hdr_loc, &url_loc))) {
263     int path_len     = 0;
264     const char *path = TSUrlPathGet(reqp, url_loc, &path_len);
265 
266     /* Short circuit the / path, common case */
267     if (!path || path_len < (int)(strlen(ACME_WK_PATH) + 2) || *path != '.' || memcmp(path, ACME_WK_PATH, strlen(ACME_WK_PATH))) {
268       TSDebug(PLUGIN_NAME, "skipping URL path = %.*s", path_len, path);
269       goto cleanup;
270     }
271 
272     TSSkipRemappingSet(txnp, 1); /* not strictly necessary, but speed is everything these days */
273 
274     /* This request is for us -- register our intercept */
275     icontp = TSContCreate(acme_intercept, TSMutexCreate());
276 
277     my_state = make_acme_state();
278     open_acme_file(my_state, path + strlen(ACME_WK_PATH), path_len - strlen(ACME_WK_PATH));
279 
280     TSContDataSet(icontp, my_state);
281     TSHttpTxnIntercept(icontp, txnp);
282     TSDebug(PLUGIN_NAME, "created intercept hook");
283   }
284 
285 cleanup:
286   if (url_loc) {
287     TSHandleMLocRelease(reqp, hdr_loc, url_loc);
288   }
289   if (hdr_loc) {
290     TSHandleMLocRelease(reqp, TS_NULL_MLOC, hdr_loc);
291   }
292 
293   TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
294 
295   return 0;
296 }
297 
298 /* Initialize the plugin / global continuation hook */
299 void
TSPluginInit(int argc,const char * argv[])300 TSPluginInit(int argc, const char *argv[])
301 {
302   TSPluginRegistrationInfo info;
303   const char *proof = "acme";
304 
305   static const struct option longopt[] = {
306     {(char *)"proof-directory", optional_argument, NULL, 'p'},
307     {NULL, no_argument, NULL, '\0'},
308   };
309 
310   memset(&gConfig, 0, sizeof(gConfig));
311   while (true) {
312     int opt = getopt_long(argc, (char *const *)argv, "", longopt, NULL);
313 
314     switch (opt) {
315     case 'p':
316       proof = optarg;
317       break;
318     }
319 
320     if (opt == -1) {
321       break;
322     }
323   }
324 
325   if ('/' != *proof) {
326     const char *confdir = TSConfigDirGet();
327     int len             = strlen(proof) + strlen(confdir) + 8;
328 
329     gConfig.proof = TSmalloc(len);
330     snprintf(gConfig.proof, len, "%s/%s", confdir, proof);
331     TSDebug(PLUGIN_NAME, "base directory for proof-types is %s", gConfig.proof);
332   } else {
333     gConfig.proof = TSstrdup(proof);
334   }
335 
336   info.plugin_name   = "acme";
337   info.vendor_name   = "Apache Software Foundation";
338   info.support_email = "dev@trafficserver.apache.org";
339 
340   if (TS_SUCCESS != TSPluginRegister(&info)) {
341     TSError("[%s] Plugin registration failed", PLUGIN_NAME);
342     return;
343   }
344 
345   TSDebug(PLUGIN_NAME, "Started the %s plugin", PLUGIN_NAME);
346   TSDebug(PLUGIN_NAME, "\tproof-type dir = %s", gConfig.proof);
347 
348   TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, TSContCreate(acme_hook, NULL));
349 }
350