Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelogs/current/new_features/lua__added-base64-decode.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Added the :ref:`base64Decode()
<config_http_filters_lua_stream_handle_api_base64_decode>` method to the HTTP Lua filter's stream
handle, the inverse of the existing ``base64Escape()``. It returns ``nil`` when the input is not
valid base64.
31 changes: 31 additions & 0 deletions docs/root/configuration/http/http_filters/lua_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,37 @@ which means the signature is verified; otherwise, the second element will store

Encodes the input string as base64. This can be useful for escaping binary data.

.. _config_http_filters_lua_stream_handle_api_base64_decode:

``base64Decode()``
^^^^^^^^^^^^^^^^^^

.. code-block:: lua

local decoded = handle:base64Decode("aW5wdXQgc3RyaW5n")

Decodes a base64 encoded string, the inverse of :ref:`base64Escape()
<config_http_filters_lua_stream_handle_api_base64_escape>`. Returns ``nil`` if the input is not
valid base64, so a value taken from a header or an upstream body can be checked rather than
having to be trusted:

.. code-block:: lua

function envoy_on_request(request_handle)
local claim = request_handle:headers():get("x-encoded-claim")
if claim ~= nil then
local decoded = request_handle:base64Decode(claim)
if decoded == nil then
request_handle:respond({[":status"] = "400"}, "malformed claim")
return
end
request_handle:headers():add("x-decoded-claim", decoded)
end
end

The decoded value may contain NUL bytes, since base64 carries arbitrary binary data; Lua strings
are length-counted, so this is preserved.

``timestamp()``
^^^^^^^^^^^^^^^

Expand Down
14 changes: 14 additions & 0 deletions source/extensions/filters/http/lua/lua_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,20 @@ int StreamHandleWrapper::luaBase64Escape(lua_State* state) {
return 1;
}

int StreamHandleWrapper::luaBase64Decode(lua_State* state) {
absl::string_view input = Filters::Common::Lua::getStringViewFromLuaString(state, 2);
std::string output;
if (!absl::Base64Unescape(input, &output)) {
// Returning nil rather than raising keeps a malformed value recoverable by the script, which
// is the common case when the input came from a header or an upstream response body.
lua_pushnil(state);
return 1;
}
lua_pushlstring(state, output.data(), output.size());

return 1;
}

int StreamHandleWrapper::luaTimestamp(lua_State* state) {
auto now = time_source_.systemTime().time_since_epoch();

Expand Down
8 changes: 8 additions & 0 deletions source/extensions/filters/http/lua/lua_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ class StreamHandleWrapper : public Filters::Common::Lua::BaseLuaObject<StreamHan
{"importPublicKey", static_luaImportPublicKey},
{"verifySignature", static_luaVerifySignature},
{"base64Escape", static_luaBase64Escape},
{"base64Decode", static_luaBase64Decode},
{"timestamp", static_luaTimestamp},
{"timestampString", static_luaTimestampString},
{"connectionStreamInfo", static_luaConnectionStreamInfo},
Expand Down Expand Up @@ -336,6 +337,13 @@ class StreamHandleWrapper : public Filters::Common::Lua::BaseLuaObject<StreamHan
*/
DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaBase64Escape);

/**
* Base64 decode a string.
* @param1 (string) base64 encoded string to be decoded.
* @return (string) the decoded string, or nil if the input is not valid base64.
*/
DECLARE_LUA_FUNCTION(StreamHandleWrapper, luaBase64Decode);

/**
* Timestamp.
* @param1 (string) optional format (e.g. milliseconds_from_epoch, nanoseconds_from_epoch).
Expand Down
45 changes: 45 additions & 0 deletions test/extensions/filters/http/lua/lua_filter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3413,6 +3413,51 @@ TEST_F(LuaHttpFilterTest, LuaFilterBase64Escape) {
});
}

TEST_F(LuaHttpFilterTest, LuaFilterBase64Decode) {
const std::string SCRIPT{R"EOF(
function envoy_on_request(request_handle)
request_handle:logTrace(request_handle:base64Decode("Zm9vYmFy"))

-- Round trips with base64Escape.
request_handle:logTrace(request_handle:base64Decode(request_handle:base64Escape("round trip")))

-- Binary data survives, including embedded NULs: Lua strings are length counted, so the
-- length is the observable property rather than the content.
local nuls = request_handle:base64Decode("AGEA")
request_handle:logTrace("nul length " .. #nuls)

-- The empty string is valid base64 and decodes to the empty string, not nil.
local empty = request_handle:base64Decode("")
request_handle:logTrace("empty is nil: " .. tostring(empty == nil) .. " length " .. #empty)
end

function envoy_on_response(response_handle)
-- Invalid base64 yields nil rather than raising.
response_handle:logTrace("bad chars: " .. tostring(response_handle:base64Decode("!!!!")))
response_handle:logTrace("bad length: " .. tostring(response_handle:base64Decode("a")))
end
)EOF"};

InSequence s;
setup(SCRIPT);

Http::TestRequestHeaderMapImpl request_headers{{":path", "/"}};

EXPECT_LOG_CONTAINS_ALL_OF(
Envoy::ExpectedLogMessages({{"trace", "foobar"},
{"trace", "round trip"},
{"trace", "nul length 3"},
{"trace", "empty is nil: false length 0"}}),
EXPECT_EQ(Http::FilterHeadersStatus::Continue,
filter_->decodeHeaders(request_headers, true)));

Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}};
EXPECT_LOG_CONTAINS_ALL_OF(
Envoy::ExpectedLogMessages({{"trace", "bad chars: nil"}, {"trace", "bad length: nil"}}),
EXPECT_EQ(Http::FilterHeadersStatus::Continue,
filter_->encodeHeaders(response_headers, true)));
}

TEST_F(LuaHttpFilterTest, Timestamp_ReturnsFormatSet) {
const std::string SCRIPT{R"EOF(
function envoy_on_request(request_handle)
Expand Down
Loading