blob: 88dadd09c6ad815eaa9617e71261b250db79b03b [file] [log] [blame]
/*
* 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.
*/
// AuthProxy - An authorization plugin for Apache Traffic Server that delegates
// the authorization decision to a separate web service. The web service
// (which we refer to here as the Authorization Proxy) is expected to authorize
// the request (or not) by consulting some authoritative source.
//
// This plugin follows the pattern of the basic-auth sample code. We use the
// TS_HTTP_POST_REMAP_HOOK to perform the initial authorization, and
// the TS_HTTP_SEND_RESPONSE_HDR_HOOK to send an error response if necessary.
#include "utils.h"
#include <string>
#include <memory> // placement new
#include <limits>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <getopt.h>
#include <arpa/inet.h>
#include <sys/param.h>
#include <ts/remap.h>
using std::strlen;
struct AuthRequestContext;
using AuthRequestTransform = bool (*)(AuthRequestContext *);
const static int MAX_HOST_LENGTH = 4096;
// We can operate in global plugin mode or remap plugin mode. If we are in
// global mode, then we will authorize every request. In remap mode, we will
// only authorize tagged requests.
static int AuthTaggedRequestArg = -1;
static TSCont AuthOsDnsContinuation;
struct AuthOptions {
std::string hostname;
int hostport = -1;
AuthRequestTransform transform = nullptr;
bool force = false;
AuthOptions() = default;
~AuthOptions() = default;
};
// Global options; used when we are in global authorization mode.
static AuthOptions *AuthGlobalOptions;
// Generic state handler callback. This should handle the event, and return a
// new event. The return value controls the subsequent state transition:
// TS_EVENT_CONTINUE Continue the state machine, returning to the ATS event loop
// TS_EVENT_NONE Stop processing (because a nested dispatch occurred)
// Anything else Continue the state machine with this event
using StateHandler = TSEvent (*)(struct AuthRequestContext *, void *);
struct StateTransition {
TSEvent event;
StateHandler handler;
const StateTransition *next;
};
static TSEvent StateAuthProxyConnect(AuthRequestContext *, void *);
static TSEvent StateAuthProxyWriteComplete(AuthRequestContext *, void *);
static TSEvent StateUnauthorized(AuthRequestContext *, void *);
static TSEvent StateAuthorized(AuthRequestContext *, void *);
static TSEvent StateAuthProxyReadHeaders(AuthRequestContext *, void *);
static TSEvent StateAuthProxyCompleteHeaders(AuthRequestContext *, void *);
static TSEvent StateAuthProxyReadContent(AuthRequestContext *, void *);
static TSEvent StateAuthProxyCompleteContent(AuthRequestContext *, void *);
static TSEvent StateAuthProxySendResponse(AuthRequestContext *, void *);
// Trampoline state that just returns TS_EVENT_CONTINUE. We need this to be
// able to transition between state tables when we are in a loop.
static TSEvent
StateContinue(AuthRequestContext *, void *)
{
return TS_EVENT_CONTINUE;
}
// State table for sending the auth proxy response to the client.
static const StateTransition StateTableSendResponse[] = {{TS_EVENT_HTTP_SEND_RESPONSE_HDR, StateAuthProxySendResponse, nullptr},
{TS_EVENT_NONE, nullptr, nullptr}};
// State table for reading the proxy response body content.
static const StateTransition StateTableProxyReadContent[] = {
{TS_EVENT_VCONN_READ_READY, StateAuthProxyReadContent, StateTableProxyReadContent},
{TS_EVENT_VCONN_READ_COMPLETE, StateAuthProxyReadContent, StateTableProxyReadContent},
{TS_EVENT_VCONN_EOS, StateAuthProxyCompleteContent, StateTableProxyReadContent},
{TS_EVENT_HTTP_SEND_RESPONSE_HDR, StateContinue, StateTableSendResponse},
{TS_EVENT_ERROR, StateUnauthorized, nullptr},
{TS_EVENT_IMMEDIATE, StateAuthorized, nullptr},
{TS_EVENT_NONE, nullptr, nullptr}};
// State table for reading the auth proxy response header.
static const StateTransition StateTableProxyReadHeader[] = {
{TS_EVENT_VCONN_READ_READY, StateAuthProxyReadHeaders, StateTableProxyReadHeader},
{TS_EVENT_VCONN_READ_COMPLETE, StateAuthProxyReadHeaders, StateTableProxyReadHeader},
{TS_EVENT_HTTP_READ_REQUEST_HDR, StateAuthProxyCompleteHeaders, StateTableProxyReadHeader},
{TS_EVENT_HTTP_SEND_RESPONSE_HDR, StateContinue, StateTableSendResponse},
{TS_EVENT_HTTP_CONTINUE, StateAuthProxyReadContent, StateTableProxyReadContent},
{TS_EVENT_VCONN_EOS, StateUnauthorized, nullptr}, // XXX Should we check headers on EOS?
{TS_EVENT_ERROR, StateUnauthorized, nullptr},
{TS_EVENT_IMMEDIATE, StateAuthorized, nullptr},
{TS_EVENT_NONE, nullptr, nullptr}};
// State table for sending the request to the auth proxy.
static const StateTransition StateTableProxyRequest[] = {
{TS_EVENT_VCONN_WRITE_COMPLETE, StateAuthProxyWriteComplete, StateTableProxyReadHeader},
{TS_EVENT_ERROR, StateUnauthorized, nullptr},
{TS_EVENT_NONE, nullptr, nullptr}};
// Initial state table.
static const StateTransition StateTableInit[] = {{TS_EVENT_HTTP_POST_REMAP, StateAuthProxyConnect, StateTableProxyRequest},
{TS_EVENT_ERROR, StateUnauthorized, nullptr},
{TS_EVENT_NONE, nullptr, nullptr}};
struct AuthRequestContext {
TSHttpTxn txn = nullptr; // Original client transaction we are authorizing.
TSCont cont = nullptr; // Continuation for this state machine.
TSVConn vconn = nullptr; // Virtual connection to the auth proxy.
TSHttpParser hparser; // HTTP response header parser.
HttpHeader rheader; // HTTP response header.
HttpIoBuffer iobuf;
const char *method = nullptr; // Client request method (e.g. GET)
bool read_body = true;
const StateTransition *state = nullptr;
AuthRequestContext()
: cont(TSContCreate(dispatch, TSMutexCreate())), hparser(TSHttpParserCreate()), rheader(), iobuf(TS_IOBUFFER_SIZE_INDEX_4K)
{
TSContDataSet(this->cont, this);
}
~AuthRequestContext()
{
TSContDataSet(this->cont, nullptr);
TSContDestroy(this->cont);
TSHttpParserDestroy(this->hparser);
if (this->vconn) {
TSVConnClose(this->vconn);
}
}
const AuthOptions *
options() const
{
AuthOptions *opt;
opt = static_cast<AuthOptions *>(TSUserArgGet(this->txn, AuthTaggedRequestArg));
return opt ? opt : AuthGlobalOptions;
}
static AuthRequestContext *allocate();
static void destroy(AuthRequestContext *);
static int dispatch(TSCont, TSEvent, void *);
};
AuthRequestContext *
AuthRequestContext::allocate()
{
void *ptr = TSmalloc(sizeof(AuthRequestContext));
return new (ptr) AuthRequestContext();
}
void
AuthRequestContext::destroy(AuthRequestContext *auth)
{
if (auth) {
auth->~AuthRequestContext();
TSfree(auth);
}
}
int
AuthRequestContext::dispatch(TSCont cont, TSEvent event, void *edata)
{
AuthRequestContext *auth = static_cast<AuthRequestContext *>(TSContDataGet(cont));
const StateTransition *s;
pump:
for (s = auth->state; s && s->event; ++s) {
if (s->event == event) {
break;
}
}
// If we don't have a handler, the state machine is borked.
TSReleaseAssert(s != nullptr);
TSReleaseAssert(s->handler != nullptr);
// Move to the next state. We have to set this *before* invoking the
// handler because the handler itself can invoke the next handler.
auth->state = s->next;
event = s->handler(auth, edata);
// If the handler returns TS_EVENT_NONE, it means that a re-entrant event
// was dispatched. In this case, the state machine continues from the
// nested call to dispatch.
if (event == TS_EVENT_NONE) {
return TS_EVENT_NONE;
}
// If there are no more states, the state machine has terminated.
if (auth->state == nullptr) {
AuthRequestContext::destroy(auth);
return TS_EVENT_NONE;
}
// If the handler gave us an event, pump the it back into the current state
// table, otherwise we will return back to the ATS event loop.
if (event != TS_EVENT_CONTINUE) {
goto pump;
}
return TS_EVENT_NONE;
}
// Return whether the client request was a HEAD request.
const char *
AuthRequestGetMethod(TSHttpTxn txn)
{
TSMBuffer mbuf;
TSMLoc mhdr;
int len;
const char *method;
TSReleaseAssert(TSHttpTxnClientReqGet(txn, &mbuf, &mhdr) == TS_SUCCESS);
method = TSHttpHdrMethodGet(mbuf, mhdr, &len);
TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr);
return method;
}
// Chain the response header hook to send the proxy's authorization response.
static void
AuthChainAuthorizationResponse(AuthRequestContext *auth)
{
if (auth->vconn) {
TSVConnClose(auth->vconn);
auth->vconn = nullptr;
}
TSHttpTxnHookAdd(auth->txn, TS_HTTP_SEND_RESPONSE_HDR_HOOK, auth->cont);
TSHttpTxnReenable(auth->txn, TS_EVENT_HTTP_ERROR);
}
// Transform the client request into a HEAD request and write it out.
static bool
AuthWriteHeadRequest(AuthRequestContext *auth)
{
HttpHeader rq;
TSMBuffer mbuf;
TSMLoc mhdr;
TSReleaseAssert(TSHttpTxnClientReqGet(auth->txn, &mbuf, &mhdr) == TS_SUCCESS);
// First, copy the whole client request to our new auth proxy request.
TSReleaseAssert(TSHttpHdrCopy(rq.buffer, rq.header, mbuf, mhdr) == TS_SUCCESS);
// Next, we need to rewrite the client request URL to be a HEAD request.
TSReleaseAssert(TSHttpHdrMethodSet(rq.buffer, rq.header, TS_HTTP_METHOD_HEAD, -1) == TS_SUCCESS);
HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CONTENT_LENGTH, 0u);
HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CACHE_CONTROL, "no-cache");
HttpDebugHeader(rq.buffer, rq.header);
// Serialize the HTTP request to the write IO buffer.
TSHttpHdrPrint(rq.buffer, rq.header, auth->iobuf.buffer);
// We have to tell the auth context not to try to ready the response
// body (since HEAD can have a content-length but must not have any
// content).
auth->read_body = false;
TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr);
return true;
}
// Transform the client request into a GET Range: bytes=0-0 request. This is useful
// for example of the authentication service is a caching proxy which might not
// cache HEAD requests.
static bool
AuthWriteRangeRequest(AuthRequestContext *auth)
{
HttpHeader rq;
TSMBuffer mbuf;
TSMLoc mhdr;
TSReleaseAssert(TSHttpTxnClientReqGet(auth->txn, &mbuf, &mhdr) == TS_SUCCESS);
// First, copy the whole client request to our new auth proxy request.
TSReleaseAssert(TSHttpHdrCopy(rq.buffer, rq.header, mbuf, mhdr) == TS_SUCCESS);
// Next, assure that the request to the auth server is a GET request, since we'll send a Range:
if (TS_HTTP_METHOD_GET != auth->method) {
TSReleaseAssert(TSHttpHdrMethodSet(rq.buffer, rq.header, TS_HTTP_METHOD_GET, -1) == TS_SUCCESS);
}
HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CONTENT_LENGTH, 0u);
HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_RANGE, "bytes=0-0");
HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CACHE_CONTROL, "no-cache");
HttpDebugHeader(rq.buffer, rq.header);
// Serialize the HTTP request to the write IO buffer.
TSHttpHdrPrint(rq.buffer, rq.header, auth->iobuf.buffer);
// We have to tell the auth context not to try to ready the response
// body, since we'are asking for a zero length Range.
auth->read_body = false;
TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr);
return true;
}
// Transform the client request into a form that the auth proxy can consume and
// write it out.
static bool
AuthWriteRedirectedRequest(AuthRequestContext *auth)
{
const AuthOptions *options = auth->options();
HttpHeader rq;
TSMBuffer mbuf;
TSMLoc mhdr;
TSMLoc murl;
char hostbuf[MAX_HOST_LENGTH + 1];
TSReleaseAssert(TSHttpTxnClientReqGet(auth->txn, &mbuf, &mhdr) == TS_SUCCESS);
// First, copy the whole client request to our new auth proxy request.
TSReleaseAssert(TSHttpHdrCopy(rq.buffer, rq.header, mbuf, mhdr) == TS_SUCCESS);
// Next, we need to rewrite the client request URL so that the request goes to
// the auth proxy instead of the original request.
TSReleaseAssert(TSHttpHdrUrlGet(rq.buffer, rq.header, &murl) == TS_SUCCESS);
// XXX Possibly we should rewrite the URL to remove the host, port and
// scheme, forcing ATS to go to the Host header. I wonder how HTTPS would
// work in that case. At any rate, we should add a new header containing
// the original host so that the auth proxy can examine it.
TSUrlHostSet(rq.buffer, murl, options->hostname.c_str(), options->hostname.size());
if (-1 != options->hostport) {
snprintf(hostbuf, sizeof(hostbuf), "%s:%d", options->hostname.c_str(), options->hostport);
TSUrlPortSet(rq.buffer, murl, options->hostport);
} else {
snprintf(hostbuf, sizeof(hostbuf), "%s", options->hostname.c_str());
}
TSHandleMLocRelease(rq.buffer, rq.header, murl);
HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_HOST, hostbuf);
HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CONTENT_LENGTH, 0u);
HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CACHE_CONTROL, "no-cache");
HttpDebugHeader(rq.buffer, rq.header);
// Serialize the HTTP request to the write IO buffer.
TSHttpHdrPrint(rq.buffer, rq.header, auth->iobuf.buffer);
TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr);
TSHandleMLocRelease(rq.buffer, rq.header, murl);
return true;
}
static TSEvent
StateAuthProxyConnect(AuthRequestContext *auth, void * /* edata ATS_UNUSED */)
{
const AuthOptions *options = auth->options();
struct sockaddr const *ip = TSHttpTxnClientAddrGet(auth->txn);
TSReleaseAssert(ip); // We must have a client IP.
auth->method = AuthRequestGetMethod(auth->txn);
AuthLogDebug("client request %s a HEAD request", auth->method == TS_HTTP_METHOD_HEAD ? "is" : "is not");
auth->vconn = TSHttpConnect(ip);
if (auth->vconn == nullptr) {
return TS_EVENT_ERROR;
}
// Transform the client request into an auth proxy request and write it
// out to the auth proxy vconn.
if (!options->transform(auth)) {
return TS_EVENT_ERROR;
}
// Start a write and transition to WriteAuthProxyState.
TSVConnWrite(auth->vconn, auth->cont, auth->iobuf.reader, TSIOBufferReaderAvail(auth->iobuf.reader));
return TS_EVENT_CONTINUE;
}
static TSEvent
StateAuthProxyCompleteHeaders(AuthRequestContext *auth, void * /* edata ATS_UNUSED */)
{
TSHttpStatus status;
HttpDebugHeader(auth->rheader.buffer, auth->rheader.header);
status = TSHttpHdrStatusGet(auth->rheader.buffer, auth->rheader.header);
AuthLogDebug("authorization proxy returned status %d", (int)status);
// Authorize the original request on a 2xx response.
if (status >= 200 && status < 300) {
return TS_EVENT_IMMEDIATE;
}
if (auth->read_body) {
// We can't support sending the auth proxy response back to the client
// without writing a transform. Since that's more trouble than I want to
// deal with right now, let's just fail fast ...
if (HttpIsChunkedEncoding(auth->rheader.buffer, auth->rheader.header)) {
AuthLogDebug("ignoring chunked authorization proxy response");
} else {
// OK, we have a non-chunked response. If there's any content, let's go and
// buffer it so that we can send it on to the client.
unsigned nbytes = HttpGetContentLength(auth->rheader.buffer, auth->rheader.header);
if (nbytes > 0) {
AuthLogDebug("content length is %u", nbytes);
return TS_EVENT_HTTP_CONTINUE;
}
}
}
// We are going to reply with the auth proxy's response. The response body
// is empty in this case.
AuthChainAuthorizationResponse(auth);
return TS_EVENT_HTTP_SEND_RESPONSE_HDR;
}
static TSEvent
StateAuthProxySendResponse(AuthRequestContext *auth, void * /* edata ATS_UNUSED */)
{
TSMBuffer mbuf;
TSMLoc mhdr;
TSHttpStatus status;
char msg[128];
// The auth proxy denied this request. We need to copy the auth proxy
// response header to the client response header, then read any available
// body data and copy that as well.
// There's only a client response if the auth proxy sent one. There
TSReleaseAssert(TSHttpTxnClientRespGet(auth->txn, &mbuf, &mhdr) == TS_SUCCESS);
TSReleaseAssert(TSHttpHdrCopy(mbuf, mhdr, auth->rheader.buffer, auth->rheader.header) == TS_SUCCESS);
status = TSHttpHdrStatusGet(mbuf, mhdr);
snprintf(msg, sizeof(msg), "%d %s\n", status, TSHttpHdrReasonLookup(status));
TSHttpTxnErrorBodySet(auth->txn, TSstrdup(msg), strlen(msg), TSstrdup("text/plain"));
// We must not whack the content length for HEAD responses, since the
// client already knows that there is no body. Forcing content length to
// zero breaks hdiutil(1) on macOS
if (TS_HTTP_METHOD_HEAD != auth->method) {
HttpSetMimeHeader(mbuf, mhdr, TS_MIME_FIELD_CONTENT_LENGTH, 0u);
}
AuthLogDebug("sending auth proxy response for status %d", status);
TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr);
TSHttpTxnReenable(auth->txn, TS_EVENT_HTTP_CONTINUE);
return TS_EVENT_CONTINUE;
}
static TSEvent
StateAuthProxyReadHeaders(AuthRequestContext *auth, void * /* edata ATS_UNUSED */)
{
TSIOBufferBlock blk;
ssize_t consumed = 0;
bool complete = false;
AuthLogDebug("reading header data, %u bytes available", (unsigned)TSIOBufferReaderAvail(auth->iobuf.reader));
for (blk = TSIOBufferReaderStart(auth->iobuf.reader); blk; blk = TSIOBufferBlockNext(blk)) {
const char *ptr;
const char *end;
int64_t nbytes;
TSParseResult result;
ptr = TSIOBufferBlockReadStart(blk, auth->iobuf.reader, &nbytes);
if (ptr == nullptr || nbytes == 0) {
continue;
}
end = ptr + nbytes;
result = TSHttpHdrParseResp(auth->hparser, auth->rheader.buffer, auth->rheader.header, &ptr, end);
switch (result) {
case TS_PARSE_ERROR:
return TS_EVENT_ERROR;
case TS_PARSE_DONE:
// We consumed the buffer we got minus the remainder.
consumed += (nbytes - std::distance(ptr, end));
complete = true;
break;
case TS_PARSE_CONT:
consumed += (nbytes - std::distance(ptr, end));
break;
}
if (complete) {
break;
}
}
AuthLogDebug("consuming %u bytes, %u remain", (unsigned)consumed, (unsigned)TSIOBufferReaderAvail(auth->iobuf.reader));
TSIOBufferReaderConsume(auth->iobuf.reader, consumed);
// If the headers are complete, send a completion event.
return complete ? TS_EVENT_HTTP_READ_REQUEST_HDR : TS_EVENT_CONTINUE;
}
static TSEvent
StateAuthProxyWriteComplete(AuthRequestContext *auth, void * /* edata ATS_UNUSED */)
{
// We finished writing the auth proxy request. Kick off a read to get the response.
auth->iobuf.reset();
TSVConnRead(auth->vconn, auth->cont, auth->iobuf.buffer, std::numeric_limits<int64_t>::max());
// XXX Do we need to keep the read and write VIOs and close them?
return TS_EVENT_CONTINUE;
}
static TSEvent
StateAuthProxyReadContent(AuthRequestContext *auth, void * /* edata ATS_UNUSED */)
{
unsigned needed;
int64_t avail = 0;
avail = TSIOBufferReaderAvail(auth->iobuf.reader);
needed = HttpGetContentLength(auth->rheader.buffer, auth->rheader.header);
AuthLogDebug("we have %u of %u needed bytes", (unsigned)avail, needed);
if (avail >= needed) {
// OK, we have what we need. Let's respond to the client request.
AuthChainAuthorizationResponse(auth);
return TS_EVENT_HTTP_SEND_RESPONSE_HDR;
}
return TS_EVENT_CONTINUE;
}
static TSEvent
StateAuthProxyCompleteContent(AuthRequestContext *auth, void * /* edata ATS_UNUSED */)
{
unsigned needed;
int64_t avail;
avail = TSIOBufferReaderAvail(auth->iobuf.reader);
needed = HttpGetContentLength(auth->rheader.buffer, auth->rheader.header);
AuthLogDebug("we have %u of %u needed bytes", (unsigned)avail, needed);
if (avail >= needed) {
// OK, we have what we need. Let's respond to the client request.
AuthChainAuthorizationResponse(auth);
return TS_EVENT_HTTP_SEND_RESPONSE_HDR;
}
// We got EOS before reading all the content we expected.
return TS_EVENT_ERROR;
}
// Terminal state. Force a 403 Forbidden response.
static TSEvent
StateUnauthorized(AuthRequestContext *auth, void *)
{
static const char msg[] = "authorization denied\n";
TSHttpTxnStatusSet(auth->txn, TS_HTTP_STATUS_FORBIDDEN);
TSHttpTxnErrorBodySet(auth->txn, TSstrdup(msg), sizeof(msg) - 1, TSstrdup("text/plain"));
TSHttpTxnReenable(auth->txn, TS_EVENT_HTTP_ERROR);
return TS_EVENT_CONTINUE;
}
// Terminal state. Allow the original request to proceed.
static TSEvent
StateAuthorized(AuthRequestContext *auth, void *)
{
const AuthOptions *options = auth->options();
AuthLogDebug("request authorized");
// Since the original request might have authentication headers, we may
// need to force ATS to ignore those in order to make it cacheable.
if (options->force) {
TSHttpTxnConfigIntSet(auth->txn, TS_CONFIG_HTTP_CACHE_IGNORE_AUTHENTICATION, 1);
}
TSHttpTxnReenable(auth->txn, TS_EVENT_HTTP_CONTINUE);
return TS_EVENT_CONTINUE;
}
// Return true if the given request was tagged by a remap rule as needing
// authorization.
static bool
AuthRequestIsTagged(TSHttpTxn txn)
{
return AuthTaggedRequestArg != -1 && TSUserArgGet(txn, AuthTaggedRequestArg) != nullptr;
}
static int
AuthProxyGlobalHook(TSCont /* cont ATS_UNUSED */, TSEvent event, void *edata)
{
TSHttpTxn txn = static_cast<TSHttpTxn>(edata);
AuthLogDebug("handling event=%d edata=%p", (int)event, edata);
switch (event) {
case TS_EVENT_HTTP_POST_REMAP:
// Ignore internal requests since we generated them.
if (TSHttpTxnIsInternal(txn)) {
// All our internal requests *must* hit the origin since it is the
// agent that needs to make the authorization decision. We can't
// allow that to be cached. Note that this only affects the remap
// rule that this plugin is instantiated for, *unless* you are using
// it as a global plugin (not highly recommended). Also remember that
// the HEAD auth request might trip a different remap rule, particularly
// if you do not have pristine host-headers enabled.
TSHttpTxnConfigIntSet(txn, TS_CONFIG_HTTP_CACHE_HTTP, 0);
AuthLogDebug("re-enabling internal transaction");
TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
return TS_EVENT_NONE;
}
// Hook this request if we are in global authorization mode or if a
// remap rule tagged it.
if (AuthGlobalOptions != nullptr || AuthRequestIsTagged(txn)) {
AuthRequestContext *auth = AuthRequestContext::allocate();
auth->state = StateTableInit;
auth->txn = txn;
return AuthRequestContext::dispatch(auth->cont, event, edata);
}
// fallthrough
default:
return TS_EVENT_NONE;
}
}
static AuthOptions *
AuthParseOptions(int argc, const char **argv)
{
// The const_cast<> here is magic to work around a flaw in the definition of struct option
// on some platforms (e.g. Solaris / Illumos). On sane platforms (e.g. linux), it'll get
// automatically casted back to the const char*, as the struct is defined in <getopt.h>.
static const struct option longopt[] = {
{const_cast<char *>("auth-host"), required_argument, nullptr, 'h'},
{const_cast<char *>("auth-port"), required_argument, nullptr, 'p'},
{const_cast<char *>("auth-transform"), required_argument, nullptr, 't'},
{const_cast<char *>("force-cacheability"), no_argument, nullptr, 'c'},
{nullptr, 0, nullptr, 0},
};
AuthOptions *options = AuthNew<AuthOptions>();
options->transform = AuthWriteRedirectedRequest;
for (;;) {
int opt;
opt = getopt_long(argc, const_cast<char *const *>(argv), "", longopt, nullptr);
switch (opt) {
case 'h':
options->hostname = optarg;
break;
case 'p':
options->hostport = std::atoi(optarg);
break;
case 'c':
options->force = true;
break;
case 't':
if (strcasecmp(optarg, "redirect") == 0) {
options->transform = AuthWriteRedirectedRequest;
} else if (strcasecmp(optarg, "head") == 0) {
options->transform = AuthWriteHeadRequest;
} else if (strcasecmp(optarg, "range") == 0) {
options->transform = AuthWriteRangeRequest;
} else {
AuthLogError("invalid authorization transform '%s'", optarg);
// XXX make this a fatal error?
}
break;
}
if (opt == -1) {
break;
}
}
if (options->hostname.empty()) {
options->hostname = "127.0.0.1";
}
return options;
}
#undef LONGOPT_OPTION_CAST
void
TSPluginInit(int argc, const char *argv[])
{
TSPluginRegistrationInfo info;
info.plugin_name = (char *)"authproxy";
info.vendor_name = (char *)"Apache Software Foundation";
info.support_email = (char *)"dev@trafficserver.apache.org";
if (TSPluginRegister(&info) != TS_SUCCESS) {
AuthLogError("plugin registration failed");
}
TSReleaseAssert(TSUserArgIndexReserve(TS_USER_ARGS_TXN, "AuthProxy", "AuthProxy authorization tag", &AuthTaggedRequestArg) ==
TS_SUCCESS);
AuthOsDnsContinuation = TSContCreate(AuthProxyGlobalHook, nullptr);
AuthGlobalOptions = AuthParseOptions(argc, argv);
AuthLogDebug("using authorization proxy at %s:%d", AuthGlobalOptions->hostname.c_str(), AuthGlobalOptions->hostport);
// Use the appropriate hook for consistent auth checks.
TSHttpHookAdd(TS_HTTP_POST_REMAP_HOOK, AuthOsDnsContinuation);
}
TSReturnCode
TSRemapInit(TSRemapInterface * /* api ATS_UNUSED */, char * /* err ATS_UNUSED */, int /* errsz ATS_UNUSED */)
{
TSReleaseAssert(TSUserArgIndexReserve(TS_USER_ARGS_TXN, "AuthProxy", "AuthProxy authorization tag", &AuthTaggedRequestArg) ==
TS_SUCCESS);
AuthOsDnsContinuation = TSContCreate(AuthProxyGlobalHook, nullptr);
return TS_SUCCESS;
}
TSReturnCode
TSRemapNewInstance(int argc, char *argv[], void **instance, char * /* err ATS_UNUSED */, int /* errsz ATS_UNUSED */)
{
AuthOptions *options;
AuthLogDebug("using authorization proxy for remapping %s -> %s", argv[0], argv[1]);
// The first two arguments are the "from" and "to" URL string. We need to
// skip them, but we also require that there be an option to masquerade as
// argv[0], so we increment the argument indexes by 1 rather than by 2.
argc--;
argv++;
options = AuthParseOptions(argc, (const char **)argv);
*instance = options;
return TS_SUCCESS;
}
void
TSRemapDeleteInstance(void *instance)
{
AuthOptions *options = static_cast<AuthOptions *>(instance);
AuthDelete(options);
}
TSRemapStatus
TSRemapDoRemap(void *instance, TSHttpTxn txn, TSRemapRequestInfo * /* rri ATS_UNUSED */)
{
AuthOptions *options = static_cast<AuthOptions *>(instance);
TSUserArgSet(txn, AuthTaggedRequestArg, options);
TSHttpTxnHookAdd(txn, TS_HTTP_POST_REMAP_HOOK, AuthOsDnsContinuation);
return TSREMAP_NO_REMAP;
}
// vim: set ts=4 sw=4 et :