| /* |
| * 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_OS_DNS_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> |
| #include <ink_config.h> |
| |
| using std::strlen; |
| |
| struct AuthRequestContext; |
| |
| typedef bool (*AuthRequestTransform)(AuthRequestContext * auth, const sockaddr * saddr); |
| |
| // 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 bool AuthTaggedRequestOnly = false; |
| static int AuthTaggedRequestArg = -1; |
| |
| static TSCont AuthOsDnsContinuation; |
| |
| struct AuthOptions |
| { |
| char * hostname; |
| int hostport; |
| bool force; |
| AuthRequestTransform transform; |
| |
| AuthOptions() : hostname(NULL), hostport(8080), force(false), transform(NULL) { |
| } |
| |
| ~AuthOptions() { |
| TSfree(hostname); |
| } |
| }; |
| |
| // 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 |
| typedef TSEvent (*StateHandler)(struct AuthRequestContext *, void * edata); |
| |
| struct StateTransition { |
| TSEvent event; |
| StateHandler handler; |
| const StateTransition * next; |
| }; |
| |
| static TSEvent StateAuthProxyConnect(AuthRequestContext *, void *); |
| static TSEvent StateAuthProxyResolve(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, NULL }, |
| { TS_EVENT_NONE, NULL, NULL } |
| }; |
| |
| // 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, NULL }, |
| { TS_EVENT_IMMEDIATE, StateAuthorized, NULL }, |
| { TS_EVENT_NONE, NULL, NULL } |
| }; |
| |
| // 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, NULL }, // XXX Should we check headers on EOS? |
| { TS_EVENT_ERROR, StateUnauthorized, NULL }, |
| { TS_EVENT_IMMEDIATE, StateAuthorized, NULL }, |
| { TS_EVENT_NONE, NULL, NULL } |
| }; |
| |
| // State table for sending the request to the auth proxy. |
| static const StateTransition StateTableProxyRequest[] = |
| { |
| { TS_EVENT_HOST_LOOKUP, StateAuthProxyConnect, StateTableProxyRequest }, |
| { TS_EVENT_VCONN_WRITE_COMPLETE, StateAuthProxyWriteComplete, StateTableProxyReadHeader }, |
| { TS_EVENT_ERROR, StateUnauthorized, NULL }, |
| { TS_EVENT_NONE, NULL, NULL } |
| }; |
| |
| // Initial state table. |
| static const StateTransition StateTableInit[] = |
| { |
| { TS_EVENT_HTTP_OS_DNS, StateAuthProxyResolve, StateTableProxyRequest }, |
| { TS_EVENT_ERROR, StateUnauthorized, NULL }, |
| { TS_EVENT_NONE, NULL, NULL } |
| }; |
| |
| struct AuthRequestContext |
| { |
| TSHttpTxn txn; // Original client transaction we are authorizing. |
| TSCont cont; // Continuation for this state machine. |
| TSVConn vconn; // Virtual connection to the auth proxy. |
| TSHttpParser hparser;// HTTP response header parser. |
| HttpHeader rheader;// HTTP response header. |
| HttpIoBuffer iobuf; |
| bool is_head;// This is a HEAD request |
| bool read_body; |
| |
| const StateTransition * state; |
| |
| AuthRequestContext() |
| : txn(NULL), cont(NULL), vconn(NULL), hparser(TSHttpParserCreate()), |
| rheader(), iobuf(TS_IOBUFFER_SIZE_INDEX_4K), is_head(false), read_body(true), state(NULL) { |
| this->cont = TSContCreate(dispatch, TSMutexCreate()); |
| TSContDataSet(this->cont, this); |
| } |
| |
| ~AuthRequestContext() { |
| TSContDataSet(this->cont, NULL); |
| TSContDestroy(this->cont); |
| TSHttpParserDestroy(this->hparser); |
| if (this->vconn) { |
| TSVConnClose(this->vconn); |
| } |
| } |
| |
| const AuthOptions * options() const { |
| AuthOptions * opt; |
| |
| opt = (AuthOptions *)TSHttpTxnArgGet(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 = (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->handler != NULL); |
| |
| // 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 == NULL) { |
| AuthRequestContext::destroy(auth); |
| } |
| |
| // 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. |
| static bool |
| AuthRequestIsHead(TSHttpTxn txn) |
| { |
| TSMBuffer mbuf; |
| TSMLoc mhdr; |
| int len; |
| bool is_head; |
| |
| TSReleaseAssert( |
| TSHttpTxnClientReqGet(txn, &mbuf, &mhdr) == TS_SUCCESS |
| ); |
| |
| is_head = (TSHttpHdrMethodGet(mbuf, mhdr, &len) == TS_HTTP_METHOD_HEAD); |
| |
| TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr); |
| return is_head; |
| } |
| |
| // 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 = NULL; |
| } |
| |
| 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, const sockaddr * /* saddr ATS_UNUSED */) |
| { |
| 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 form that the auth proxy can consume and |
| // write it out. |
| static bool |
| AuthWriteRedirectedRequest(AuthRequestContext * auth, const sockaddr * saddr) |
| { |
| HttpHeader rq; |
| TSMBuffer mbuf; |
| TSMLoc mhdr; |
| TSMLoc murl; |
| |
| char addrbuf[INET6_ADDRSTRLEN]; |
| char hostbuf[INET6_ADDRSTRLEN + sizeof("[]") + sizeof(":65536")]; |
| uint16_t hostport; |
| |
| 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 |
| ); |
| |
| hostport = SockaddrGetPort(saddr); |
| inet_ntop(saddr->sa_family, SockaddrGetAddress(saddr), addrbuf, sizeof(addrbuf)); |
| if (saddr->sa_family == PF_INET6) { |
| snprintf(hostbuf, sizeof(hostbuf), "[%s]:%d", addrbuf, hostport); |
| } else { |
| snprintf(hostbuf, sizeof(hostbuf), "%s:%d", addrbuf, hostport); |
| } |
| |
| // 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, addrbuf, -1); |
| TSUrlPortSet(rq.buffer, murl, hostport); |
| 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 |
| StateAuthProxyResolve(AuthRequestContext * auth, void *) |
| { |
| TSAction lookup; |
| const AuthOptions * options = auth->options(); |
| |
| // If we are authorizing with a HEAD request we want to send that to the |
| // origin; other requests we want to send to the authorization proxy. |
| if (options->transform == AuthWriteHeadRequest) { |
| char hostname[TS_MAX_HOST_NAME_LEN * 2]; |
| TSMBuffer mbuf; |
| TSMLoc mhdr; |
| |
| TSReleaseAssert( |
| TSHttpTxnClientReqGet(auth->txn, &mbuf, &mhdr) == TS_SUCCESS |
| ); |
| |
| if (HttpGetOriginHost(mbuf, mhdr, hostname, sizeof(hostname))) { |
| AuthLogDebug("resolving authorization host %s", hostname); |
| lookup = TSHostLookup(auth->cont, hostname, strlen(hostname)); |
| TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr); |
| } else { |
| AuthLogError("failed to extract origin host name from client request"); |
| TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr); |
| return TS_EVENT_ERROR; |
| } |
| |
| } else { |
| AuthLogDebug("resolving authorization proxy host %s", options->hostname); |
| lookup = TSHostLookup(auth->cont, options->hostname, strlen(options->hostname)); |
| } |
| |
| if (TSActionDone(lookup)) { |
| AuthLogDebug("host lookup was executed in line"); |
| return TS_EVENT_NONE; |
| } |
| |
| return TS_EVENT_CONTINUE; |
| } |
| |
| static TSEvent |
| StateAuthProxyConnect(AuthRequestContext * auth, void * edata) |
| { |
| const AuthOptions * options = auth->options(); |
| TSHostLookupResult dns; |
| const sockaddr * saddr; |
| |
| union { |
| sockaddr sa; |
| sockaddr_in sin; |
| sockaddr_in6 sin6; |
| sockaddr_storage storage; |
| } addr; |
| |
| dns = (TSHostLookupResult)edata; |
| if (dns == NULL) { |
| AuthLogError("failed to resolve authorization proxy at %s", options->hostname); |
| return TS_EVENT_ERROR; |
| } |
| |
| // Copy the resolved address and add the port. |
| saddr = TSHostLookupResultAddrGet(dns); |
| switch (saddr->sa_family) { |
| case PF_INET: |
| memcpy(&addr.sin, saddr, sizeof(sockaddr_in)); |
| addr.sin.sin_port = options->hostport; |
| break; |
| case PF_INET6: |
| memcpy(&addr.sin6, saddr, sizeof(sockaddr_in6)); |
| addr.sin6.sin6_port = options->hostport; |
| break; |
| } |
| |
| auth->is_head = AuthRequestIsHead(auth->txn); |
| AuthLogDebug("client request %s a HEAD request", auth->is_head ? "is" : "is not"); |
| |
| auth->vconn = TSHttpConnect(&addr.sa); |
| if (auth->vconn == NULL) { |
| 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, &addr.sa)) { |
| 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; |
| unsigned nbytes; |
| |
| 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. |
| 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), NULL); |
| |
| // 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 Mac OS X. |
| if (!auth->is_head) { |
| HttpSetMimeHeader(mbuf, mhdr, TS_MIME_FIELD_CONTENT_LENGTH, 0u); |
| } |
| |
| AuthLogDebug("sending auth proxy response for status %d", status); |
| |
| TSHttpTxnReenable(auth->txn, TS_EVENT_HTTP_CONTINUE); |
| TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr); |
| 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 == NULL || 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: |
| case TS_PARSE_OK: |
| // 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"; |
| |
| TSHttpTxnSetHttpRetStatus(auth->txn, TS_HTTP_STATUS_FORBIDDEN); |
| TSHttpTxnErrorBodySet(auth->txn, TSstrdup(msg), sizeof(msg) - 1, NULL); |
| |
| 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 && |
| TSHttpTxnArgGet(txn, AuthTaggedRequestArg) != NULL; |
| } |
| |
| static int |
| AuthProxyGlobalHook(TSCont /* cont ATS_UNUSED */, TSEvent event, void * edata) |
| { |
| AuthRequestContext * auth; |
| union { |
| TSHostLookupResult dns; |
| TSHttpTxn txn; |
| void * edata; |
| } ptr; |
| |
| ptr.edata = edata; |
| AuthLogDebug("handling event=%d edata=%p", (int)event, edata); |
| |
| switch (event) { |
| case TS_EVENT_HTTP_OS_DNS: |
| // Ignore internal requests since we generated them. |
| if (TSHttpIsInternalRequest(ptr.txn) == TS_SUCCESS) { |
| // 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. |
| TSHttpTxnReqCacheableSet(ptr.txn, 0); |
| |
| AuthLogDebug("re-enabling internal transaction"); |
| TSHttpTxnReenable(ptr.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 != NULL || AuthRequestIsTagged(ptr.txn)) { |
| auth = AuthRequestContext::allocate(); |
| auth->state = StateTableInit; |
| auth->txn = ptr.txn; |
| return AuthRequestContext::dispatch(auth->cont, event, edata); |
| } |
| |
| // fallthru |
| |
| 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, 0, 'h' }, |
| { const_cast<char *>("auth-port"), required_argument, 0, 'p' }, |
| { const_cast<char *>("auth-transform"), required_argument, 0, 't' }, |
| { const_cast<char *>("force-cacheability"), no_argument, 0, 'c' }, |
| {0, 0, 0, 0 } |
| }; |
| |
| AuthOptions * options = AuthNew<AuthOptions>(); |
| |
| options->transform = AuthWriteRedirectedRequest; |
| options->hostname = TSstrdup("127.0.0.1"); |
| |
| // We might parse arguments multiple times if we are loaded as a global |
| // plugin and a remap plugin. Reset optind so that getopt_long() does the |
| // right thing (ie. work instead of crash). |
| optind = 0; |
| |
| for (;;) { |
| int opt; |
| |
| opt = getopt_long(argc, (char * const *)argv, "", longopt, NULL); |
| switch (opt) { |
| case 'h': |
| TSfree(options->hostname); |
| options->hostname = TSstrdup(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 { |
| AuthLogError("invalid authorization transform '%s'", optarg); |
| // XXX make this a fatal error? |
| } |
| |
| break; |
| } |
| |
| if (opt == -1) { |
| break; |
| } |
| } |
| |
| 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(TS_SDK_VERSION_3_0, &info) != TS_SUCCESS) { |
| AuthLogError("plugin registration failed"); |
| } |
| |
| TSReleaseAssert( |
| TSHttpArgIndexReserve("AuthProxy", "AuthProxy authorization tag", &AuthTaggedRequestArg) == TS_SUCCESS |
| ); |
| |
| // We are in global mode. Authorize all requests. |
| AuthTaggedRequestOnly = false; |
| |
| AuthOsDnsContinuation = TSContCreate(AuthProxyGlobalHook, NULL); |
| AuthGlobalOptions = AuthParseOptions(argc, argv); |
| AuthLogDebug("using authorization proxy at %s:%d", AuthGlobalOptions->hostname, AuthGlobalOptions->hostport); |
| |
| // Catch the DNS hook. This triggers after reading the headers and |
| // resolving the requested host, but before performing any cache lookups. |
| TSHttpHookAdd(TS_HTTP_OS_DNS_HOOK, AuthOsDnsContinuation); |
| } |
| |
| TSReturnCode |
| TSRemapInit(TSRemapInterface * api, char * err, int errsz) |
| { |
| (void)api; |
| (void)err; |
| (void)errsz; |
| |
| TSReleaseAssert( |
| TSHttpArgIndexReserve("AuthProxy", "AuthProxy authorization tag", &AuthTaggedRequestArg) == TS_SUCCESS |
| ); |
| |
| // We are in remap mode. Only authorize tagged requests. |
| AuthTaggedRequestOnly = true; |
| AuthOsDnsContinuation = TSContCreate(AuthProxyGlobalHook, NULL); |
| 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 = (AuthOptions *)instance; |
| AuthDelete(options); |
| } |
| |
| TSRemapStatus |
| TSRemapDoRemap(void * instance, TSHttpTxn txn, TSRemapRequestInfo * /* rri ATS_UNUSED */) |
| { |
| AuthOptions * options = (AuthOptions *)instance; |
| |
| TSHttpTxnArgSet(txn, AuthTaggedRequestArg, options); |
| TSHttpTxnHookAdd(txn, TS_HTTP_OS_DNS_HOOK, AuthOsDnsContinuation); |
| return TSREMAP_NO_REMAP; |
| } |
| |
| // vim: set ts=4 sw=4 et : |