blob: 3f3f7cb7ff47e08421ec6221088286354ee8d00e [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.
*/
#include <ts/ts.h>
#include <ts/remap.h>
#include <string.h>
#include "lapi.h"
#include "lutil.h"
#include "hook.h"
#include "state.h"
#include <memory> // placement new
#include "ink_config.h"
#include "ink_defs.h"
const char *
HttpHookName(TSHttpHookID hookid)
{
static const char * names[TS_HTTP_LAST_HOOK] = {
"HTTP_READ_REQUEST_HDR_HOOK",
"HTTP_OS_DNS_HOOK",
"HTTP_SEND_REQUEST_HDR_HOOK",
"HTTP_READ_CACHE_HDR_HOOK",
"HTTP_READ_RESPONSE_HDR_HOOK",
"HTTP_SEND_RESPONSE_HDR_HOOK",
NULL, // XXX TS_HTTP_REQUEST_TRANSFORM_HOOK
NULL, // XXX TS_HTTP_RESPONSE_TRANSFORM_HOOK
NULL, // XXX HTTP_SELECT_ALT_HOOK
"HTTP_TXN_START_HOOK",
"HTTP_TXN_CLOSE_HOOK",
"HTTP_SSN_START_HOOK",
"HTTP_SSN_CLOSE_HOOK",
"HTTP_CACHE_LOOKUP_COMPLETE_HOOK",
"HTTP_PRE_REMAP_HOOK",
"HTTP_POST_REMAP_HOOK",
};
if (hookid >= 0 && hookid < static_cast<TSHttpHookID>(countof(names))) {
return names[hookid];
}
return NULL;
}
static bool
HookIsValid(int hookid)
{
if (hookid == TS_HTTP_REQUEST_TRANSFORM_HOOK || hookid == TS_HTTP_RESPONSE_TRANSFORM_HOOK) {
return false;
}
return hookid >= 0 && hookid < TS_HTTP_LAST_HOOK;
}
static void
LuaPushEventData(lua_State * lua, TSEvent event, void * edata)
{
switch (event) {
case TS_EVENT_HTTP_READ_REQUEST_HDR:
case TS_EVENT_HTTP_OS_DNS:
case TS_EVENT_HTTP_SEND_REQUEST_HDR:
case TS_EVENT_HTTP_READ_CACHE_HDR:
case TS_EVENT_HTTP_READ_RESPONSE_HDR:
case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
case TS_EVENT_HTTP_SELECT_ALT:
case TS_EVENT_HTTP_TXN_START:
case TS_EVENT_HTTP_TXN_CLOSE:
case TS_EVENT_CACHE_LOOKUP_COMPLETE:
case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE:
case TS_EVENT_HTTP_PRE_REMAP:
case TS_EVENT_HTTP_POST_REMAP:
LuaPushHttpTransaction(lua, (TSHttpTxn)edata);
break;
case TS_EVENT_HTTP_SSN_START:
case TS_EVENT_HTTP_SSN_CLOSE:
LuaPushHttpSession(lua, (TSHttpSsn)edata);
break;
default:
lua_pushnil(lua);
}
}
#if defined(INLINE_LUA_HOOK_REFERENCE)
typedef char __size_check[sizeof(this_type) == sizeof(void *) ? 0 : -1];
#endif
// For 64-bit pointers, we can inline the LuaHookReference, otherwise we need an extra malloc.
//
#if SIZEOF_VOID_POINTER >= 8
#define INLINE_LUA_HOOK_REFERENCE 1
#else
#undef INLINE_LUA_HOOK_REFERENCE
#endif
template <typename t1, typename t2>
struct inline_tuple
{
typedef t1 first_type;
typedef t2 second_type;
typedef inline_tuple<first_type, second_type> this_type;
union {
struct {
first_type first;
second_type second;
} s;
void * ptr;
} storage;
first_type& first() { return storage.s.first; }
second_type& second() { return storage.s.second; }
static void * allocate(const first_type first, const second_type second) {
#if defined(INLINE_LUA_HOOK_REFERENCE)
this_type obj;
obj.first() = first;
obj.second() = second;
return obj.storage.ptr;
#else
this_type * ptr = (this_type *)TSmalloc(sizeof(this_type));
ptr->first() = first;
ptr->second() = second;
return ptr;
#endif
}
static void free(void *ptr ATS_UNUSED) {
#if defined(INLINE_LUA_HOOK_REFERENCE)
// Nothing to do, because we never allocated.
#else
TSfree(ptr);
#endif
}
static this_type get(void * ptr) {
#if defined(INLINE_LUA_HOOK_REFERENCE)
this_type obj;
obj.storage.ptr = ptr;
return obj;
#else
return ptr ? *(this_type *)ptr : this_type();
#endif
}
};
// The per-ssn and per-txn argument mechanism stores a pointer, so it's NULL when not set. Unfortunately, 0 is a
// legitimate Lua reference value (all values except LUA_NOREF are legitimate), so we can't distinguish NULL from a 0
// reference. In 64-bit mode we have some extra bits and we can maintain the state, but in 32-bit mode, we need to
// allocate the LuaHookReference to have enough space to store the state.
typedef inline_tuple<int, bool> LuaHookReference;
static void *
LuaHttpObjectArgGet(TSHttpSsn ssn)
{
return TSHttpSsnArgGet(ssn, LuaHttpArgIndex);
}
static void *
LuaHttpObjectArgGet(TSHttpTxn txn)
{
return TSHttpTxnArgGet(txn, LuaHttpArgIndex);
}
static void
LuaHttpObjectArgSet(TSHttpSsn ssn, void * ptr)
{
return TSHttpSsnArgSet(ssn, LuaHttpArgIndex, ptr);
}
static void
LuaHttpObjectArgSet(TSHttpTxn txn, void * ptr)
{
return TSHttpTxnArgSet(txn, LuaHttpArgIndex, ptr);
}
template<typename T> static int
LuaGetArgReference(T ptr)
{
LuaHookReference href(LuaHookReference::get(LuaHttpObjectArgGet(ptr)));
// Only return the Lua ref if it was previously set.
return href.second() ? href.first() : LUA_NOREF;
}
template <typename T> void
LuaSetArgReference(T ptr, int ref)
{
LuaHookReference::free(LuaHttpObjectArgGet(ptr));
LuaHttpObjectArgSet(ptr, LuaHookReference::allocate(ref, true));
}
template <typename T> static void
LuaClearArgReference(T ptr)
{
LuaHookReference::free(LuaHttpObjectArgGet(ptr));
LuaHttpObjectArgSet(ptr, NULL);
}
// Force template instantiation of LuaSetArgReference().
template void LuaSetArgReference<TSHttpSsn>(TSHttpSsn ssn, int ref);
template void LuaSetArgReference<TSHttpTxn>(TSHttpTxn txn, int ref);
static void
LuaDemuxInvokeCallback(lua_State * lua, TSHttpHookID hookid, TSEvent event, void * edata, int ref)
{
int nitems = lua_gettop(lua);
// Push the callback table onto the top of the stack.
lua_rawgeti(lua, LUA_REGISTRYINDEX, ref);
// XXX If this is a global hook, we have a function reference. If it's a ssn or txn hook then we
// have a callback table reference. We need to make these the same, but not rught now ...
switch (lua_type(lua, -1)) {
case LUA_TFUNCTION:
// Nothing to do, the function we want to invoke is already on top of the stack.
break;
case LUA_TTABLE:
// Push the hookid onto the stack so we can use it to index the table (that is now at -2).
lua_pushinteger(lua, hookid);
TSAssert(lua_isnumber(lua, -1));
TSAssert(lua_istable(lua, -2));
// Index the callback table with the hookid to get the callback function for this hook.
lua_gettable(lua, -2);
break;
default:
LuaLogError("invalid callback reference type %s", ltypeof(lua, -1));
TSReleaseAssert(0);
}
// The item on the top of the stack *ought* to be the callback function. However when we register a
// cleanup function to release the callback reference (because the ssn or txn closes), then we won't
// have a function because there's nothing to do here.
if (!lua_isnil(lua, -1)) {
TSAssert(lua_isfunction(lua, -1));
lua_pushinteger(lua, event);
LuaPushEventData(lua, event, edata);
if (lua_pcall(lua, 2 /* nargs */, 0, 0) != 0) {
LuaLogDebug("hook callback failed: %s", lua_tostring(lua, -1));
lua_pop(lua, 1); // pop the error message
}
}
// If we left anything on the stack, pop it.
lua_pop(lua, lua_gettop(lua) - nitems);
}
int
LuaDemuxGlobalHook(TSHttpHookID hookid, TSCont cont, TSEvent event, void * edata)
{
instanceid_t instanceid = (uintptr_t)TSContDataGet(cont);
ScopedLuaState lstate(instanceid);
int ref = lstate->hookrefs[hookid];
LuaLogDebug("%u/%p %s event=%d edata=%p, ref=%d",
instanceid, lstate->lua,
HttpHookName(hookid), event, edata, ref);
if (ref == LUA_NOREF) {
LuaLogError("no Lua callback for hook %s", HttpHookName(hookid));
return TS_EVENT_ERROR;
}
LuaDemuxInvokeCallback(lstate->lua, hookid, event, edata, ref);
return TS_EVENT_NONE;
}
int
LuaDemuxTxnHook(TSHttpHookID hookid, TSCont cont, TSEvent event, void * edata)
{
int ref = LuaGetArgReference((TSHttpTxn)edata);
instanceid_t instanceid = (uintptr_t)TSContDataGet(cont);
ScopedLuaState lstate(instanceid);
LuaLogDebug("%s(%s) instanceid=%u event=%d edata=%p",
__func__, HttpHookName(hookid), instanceid, event, edata);
if (ref == LUA_NOREF) {
LuaLogError("no Lua callback for hook %s", HttpHookName(hookid));
return TS_EVENT_ERROR;
}
LuaDemuxInvokeCallback(lstate->lua, hookid, event, edata, ref);
if (event == TS_EVENT_HTTP_TXN_CLOSE) {
LuaLogDebug("unref event handler %d", ref);
luaL_unref(lstate->lua, LUA_REGISTRYINDEX, ref);
LuaClearArgReference((TSHttpTxn)edata);
}
return TS_EVENT_NONE;
}
int
LuaDemuxSsnHook(TSHttpHookID hookid, TSCont cont, TSEvent event, void * edata)
{
instanceid_t instanceid = (uintptr_t)TSContDataGet(cont);
ScopedLuaState lstate(instanceid);
TSHttpSsn ssn;
int ref;
// The edata might be a Txn or a Ssn, depending on the event type. If we get here, it's because we
// registered a callback on the Ssn, so we need to get back to the Ssn object in order to the the
// callback table reference ...
switch (event) {
case TS_EVENT_HTTP_SSN_START:
case TS_EVENT_HTTP_SSN_CLOSE:
ssn = (TSHttpSsn)edata;
break;
default:
ssn = TSHttpTxnSsnGet((TSHttpTxn)edata);
}
LuaLogDebug("%s(%s) instanceid=%u event=%d edata=%p",
__func__, HttpHookName(hookid), instanceid, event, edata);
ref = LuaGetArgReference(ssn);
if (ref == LUA_NOREF) {
LuaLogError("no Lua callback for hook %s", HttpHookName(hookid));
return TS_EVENT_ERROR;
}
LuaDemuxInvokeCallback(lstate->lua, hookid, event, edata, ref);
if (event == TS_EVENT_HTTP_SSN_CLOSE) {
LuaLogDebug("unref event handler %d", ref);
luaL_unref(lstate->lua, LUA_REGISTRYINDEX, ref);
LuaClearArgReference((TSHttpSsn)edata);
}
return TS_EVENT_NONE;
}
bool
LuaRegisterHttpHooks(lua_State * lua, void * obj, LuaHookAddFunction add, int hooks)
{
bool hooked_close = false;
const TSHttpHookID closehook = (add == LuaHttpSsnHookAdd ? TS_HTTP_SSN_CLOSE_HOOK : TS_HTTP_TXN_CLOSE_HOOK);
TSAssert(add == LuaHttpSsnHookAdd || add == LuaHttpTxnHookAdd);
// Push the hooks reference back onto the stack.
lua_rawgeti(lua, LUA_REGISTRYINDEX, hooks);
// The value on the top of the stack (index -1) MUST be the callback table.
TSAssert(lua_istable(lua, lua_gettop(lua)));
// Now we need our LuaThreadState to access the hook tables.
ScopedLuaState lstate(lua);
// Walk the table and register the hook for each entry.
lua_pushnil(lua); // Push the first key, makes the callback table index -2.
while (lua_next(lua, -2) != 0) {
TSHttpHookID hookid;
// uses 'key' (at index -2) and 'value' (at index -1).
// LuaLogDebug("key=%s value=%s\n", ltypeof(lua, -2), ltypeof(lua, -1));
// Now the key (index -2) and value (index -1) got pushed onto the stack. The key must be a hook ID and
// the value must be a callback function.
luaL_checktype(lua, -1, LUA_TFUNCTION);
hookid = (TSHttpHookID)luaL_checkint(lua, -2);
if (!HookIsValid(hookid)) {
LuaLogError("invalid Hook ID %d", hookid);
goto next;
}
if (hookid == closehook) {
hooked_close = true;
}
// At demux time, we need the hook ID and the table (or function) ref.
add(obj, lstate.instance(), hookid);
LuaLogDebug("registered callback table %d for event %s on object %p",
hooks, HttpHookName(hookid), obj);
next:
// Pop the value (index -1), leaving key as the new top (index -1).
lua_pop(lua, 1);
}
// we always need to hook the close because we keep a reference to the callback table and we need to
// release that reference when the object's lifetime ends.
if (!hooked_close) {
add(obj, lstate.instance(), closehook);
}
return true;
}
void
LuaHttpSsnHookAdd(void * ssn, const LuaPluginInstance * instance, TSHttpHookID hookid)
{
TSHttpSsnHookAdd((TSHttpSsn)ssn, hookid, instance->demux.ssn[hookid]);
}
void
LuaHttpTxnHookAdd(void * txn, const LuaPluginInstance * instance, TSHttpHookID hookid)
{
TSHttpTxnHookAdd((TSHttpTxn)txn, hookid, instance->demux.txn[hookid]);
}
static int
TSLuaHttpHookRegister(lua_State * lua)
{
TSHttpHookID hookid;
hookid = (TSHttpHookID)luaL_checkint(lua, 1);
luaL_checktype(lua, 2, LUA_TFUNCTION);
LuaLogDebug("registering hook %s (%d)", HttpHookName(hookid), (int)hookid);
if (hookid < 0 || hookid >= TS_HTTP_LAST_HOOK) {
LuaLogDebug("hook ID %d out of range", hookid);
return -1;
}
ScopedLuaState lstate(lua);
TSReleaseAssert(lstate);
// The lstate must match the current Lua state or something is seriously wrong.
TSReleaseAssert(lstate->lua == lua);
// Global hooks can only be registered once, but we load the Lua scripts in every thread. Check whether
// the hook has already been registered and ignore any double-registrations.
if (lstate->hookrefs[hookid] != LUA_NOREF) {
LuaLogDebug("ignoring double registration for %s hook", HttpHookName(hookid));
return 0;
}
// The callback function for the hook should be on the top of the stack now. Keep a reference
// to the callback function in the registry so we can pop it out later.
TSAssert(lua_type(lua, lua_gettop(lua)) == LUA_TFUNCTION);
lstate->hookrefs[hookid] = luaL_ref(lua, LUA_REGISTRYINDEX);
LuaLogDebug("%u/%p added hook ref %d for %s",
lstate->instance->instanceid, lua, lstate->hookrefs[hookid], HttpHookName(hookid));
// We need to atomically install this global hook. We snaffle the high bit to mark whether or
// not it has been installed.
if (((uintptr_t)lstate->instance->demux.global[hookid] & 0x01u) == 0) {
TSCont cont = (TSCont)((uintptr_t)lstate->instance->demux.global[hookid] | 0x01u);
if (__sync_bool_compare_and_swap(&lstate->instance->demux.global[hookid],
lstate->instance->demux.global[hookid], cont)) {
LuaLogDebug("installed continuation for %s", HttpHookName(hookid));
TSHttpHookAdd(hookid, (TSCont)((uintptr_t)cont & ~0x01u));
} else {
LuaLogDebug("lost hook creation race for %s", HttpHookName(hookid));
}
}
return 0;
}
static const luaL_Reg LUAEXPORTS[] =
{
{ "register", TSLuaHttpHookRegister },
{ NULL, NULL}
};
int
LuaHookApiInit(lua_State * lua)
{
LuaLogDebug("initializing TS Hook API");
lua_newtable(lua);
// Register functions in the "ts.hook" module.
luaL_register(lua, NULL, LUAEXPORTS);
for (unsigned i = 0; i < TS_HTTP_LAST_HOOK; ++i) {
if (HttpHookName((TSHttpHookID)i) != NULL) {
// Register named constants for each hook ID.
LuaSetConstantField(lua, HttpHookName((TSHttpHookID)i), i);
}
}
return 1;
}