| /* | |
| * 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::string_view; | |
| 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; | |
| std::string forward_header_prefix; | |
| int hostport = -1; | |
| AuthRequestTransform transform = nullptr; | |
| bool force = false; | |
| bool cache_internal_requests = false; | |
| string_view forwardHeaderPrefix; | |
| 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 StateAuthProxyWriteReady(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_READY, StateAuthProxyWriteReady, 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); | |
| // This sub-request is bodyless (HEAD + Content-Length: 0), but the copied | |
| // client request may carry request-body framing (e.g. a chunked POST or | |
| // Expect: 100-continue). Left in place it is self-contradictory: ATS sets up | |
| // a request-body tunnel for a body that never arrives (stalling the probe | |
| // until timeout), and proxy.config.http.reject_head_with_content rejects a | |
| // HEAD that declares content. Strip the framing when forcing Content-Length: 0. | |
| HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_TRANSFER_ENCODING); | |
| HttpRemoveMimeHeader(rq.buffer, rq.header, "Trailer"); | |
| HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_EXPECT); | |
| 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); | |
| } | |
| // The body is dropped (Content-Length: 0), so strip any request-body framing | |
| // (Transfer-Encoding/Trailer/Expect) copied from the client request to keep | |
| // the sub-request well-formed. | |
| HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_TRANSFER_ENCODING); | |
| HttpRemoveMimeHeader(rq.buffer, rq.header, "Trailer"); | |
| HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_EXPECT); | |
| 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); | |
| // The body is dropped (Content-Length: 0), so strip any request-body framing | |
| // (Transfer-Encoding/Trailer/Expect) copied from the client request to keep | |
| // the sub-request well-formed. | |
| HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_TRANSFER_ENCODING); | |
| HttpRemoveMimeHeader(rq.buffer, rq.header, "Trailer"); | |
| HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_EXPECT); | |
| 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 | |
| StateAuthProxyWriteReady(AuthRequestContext *auth, void * /* edata ATS_UNUSED */) | |
| { | |
| TSVConnWrite(auth->vconn, auth->cont, auth->iobuf.reader, TSIOBufferReaderAvail(auth->iobuf.reader)); | |
| return 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, "authproxy"); | |
| 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); | |
| } | |
| if (!options->forward_header_prefix.empty()) { | |
| // Copy headers with configured prefix in the authentication response to the original request | |
| TSMLoc field_loc; | |
| TSMLoc next_field_loc; | |
| TSMBuffer request_bufp; | |
| TSMLoc request_hdr; | |
| TSReleaseAssert(TSHttpTxnClientReqGet(auth->txn, &request_bufp, &request_hdr) == TS_SUCCESS); | |
| field_loc = TSMimeHdrFieldGet(auth->rheader.buffer, auth->rheader.header, 0); | |
| while (field_loc) { | |
| int key_len = 0; | |
| int val_len = 0; | |
| char *key = const_cast<char *>(TSMimeHdrFieldNameGet(auth->rheader.buffer, auth->rheader.header, field_loc, &key_len)); | |
| char *val = | |
| const_cast<char *>(TSMimeHdrFieldValueStringGet(auth->rheader.buffer, auth->rheader.header, field_loc, -1, &val_len)); | |
| if (key && val && ContainsPrefix(string_view(key, key_len), options->forward_header_prefix)) { | |
| // Append the matched header key/val to the original request | |
| HttpSetMimeHeader(request_bufp, request_hdr, string_view(key, key_len), string_view(val, val_len)); | |
| } | |
| // Validate the next header field | |
| next_field_loc = TSMimeHdrFieldNext(auth->rheader.buffer, auth->rheader.header, field_loc); | |
| TSHandleMLocRelease(auth->rheader.buffer, auth->rheader.header, field_loc); | |
| field_loc = next_field_loc; | |
| } | |
| TSHandleMLocRelease(request_bufp, TS_NULL_MLOC, request_hdr); | |
| } | |
| // Proceed with the modified request | |
| 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; | |
| } | |
| // Return true if the internal requests can be cached. | |
| static bool | |
| CacheInternalRequests(TSHttpTxn txn) | |
| { | |
| AuthOptions *opt = static_cast<AuthOptions *>(TSUserArgGet(txn, AuthTaggedRequestArg)); | |
| return opt ? opt->cache_internal_requests : false; | |
| } | |
| 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. | |
| if (!CacheInternalRequests(txn)) | |
| 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 platform. 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'}, | |
| {const_cast<char *>("cache-internal"), no_argument, nullptr, 'i'}, | |
| {const_cast<char *>("forward-header-prefix"), required_argument, nullptr, 'f'}, | |
| {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 'i': | |
| options->cache_internal_requests = true; | |
| break; | |
| case 'f': | |
| options->forward_header_prefix = optarg; | |
| 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 : |