apiVersion: consul.hashicorp.com/v1alpha1
kind: ServiceDefaults
metadata:
  name: api-gateway
  namespace: consul
spec:
  protocol: http
  envoyExtensions:
    - name: builtin/lua
      required: true
      arguments:
        proxyType: "api-gateway"
        listener: "outbound"
        mutualTLSMode: ""
        script: |
          -- Appsentinels Envoy Filter in Lua

          -- Needed for visibility, pls update this to your filter instance name
          local INSTANCE_NAME = "consul-api-gateway"
          -- End of visibility

          -- Config section start
          local ENFORCEMENT = false
          local AUTHZ_TIMEOUT = 100
          local ACCESSLOG_TIMEOUT = 100
          local FAILOPEN = true
          local CONTROLLER_CLUSTER_NAME = "ext_authz-http-service"
          local SKIP_METHODS = {"HEAD", "OPTIONS"}
          local LOGGING_CONTENT_TYPES = {"json", "form", "xml", "graphql"}
          local MAX_PAYLOAD_SIZE = 131072  --- 128k
          local AS_DEBUG = false
          -- Config section end

          local HOSTNAME = os.getenv("HOSTNAME")

          -- Helper functions start --
          function _bypass_method(method)
            if method == nil then
              return true
            end
            for _, v in ipairs(SKIP_METHODS) do
              --- do a substring match to check if v is in method
              ---
              --- This is a safe way to check if v is in SKIP_METHODS
              ---
              if string.find(v, method) then
                return true
              end
            end
            return false
          end

          function _valid_content_type(content_type)
            if content_type == nil then
              return true
            end

            for _, v in ipairs(LOGGING_CONTENT_TYPES) do
              if string.find(content_type, v) then
                return true
              end
            end
            return false
          end

          function _content_too_large(payload_size)
            if payload_size == nil then
              return false
            end

            if tonumber(payload_size) >= MAX_PAYLOAD_SIZE then
              return true
            else
              return false
            end
          end

          -- Helper functions end --

          function appsentinels_on_request(request_handle)
              request_handle:logErr("sagarrrrrrrrrrrrrr In request: ")
              -- Create a new table to store headers for httpCall
              local httpCallHeaders = {}

              -- Get the HTTP method and URI from the request headers
              local method = request_handle:headers():get(":method")
              if _bypass_method(method) then
                if AS_DEBUG then request_handle:logInfo("as: bypassing unsupported method: " .. method) end
                -- Pass this message over to response handler so it can bypass this log as well
                request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.skip", true)
                return
              end

              request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.method", method)

              -- Get all headers from request_handle
              for name, value in pairs(request_handle:headers()) do
                httpCallHeaders[name] = value
              end

              httpCallHeaders["X-As-Error"] = ""

              -- Pass some info must for resp via metadata
              local metadata = request_handle:streamInfo():dynamicMetadata()
              local rid = httpCallHeaders["x-request-id"]
              if rid ~= nil then
                request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.x-request-id", rid)
              else
                httpCallHeaders["X-As-Error"] = "Missing X-Request-Id;"
              end

              local path = request_handle:headers():get(":path")
              if path ~= nil then
                request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.path", path)
              else
                httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing path;"
              end

              local host = request_handle:headers():get(":authority")
              if host ~= nil then
                request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.authority", host)
              else
                httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing host;"
              end

              local _start_time = request_handle:timestamp(EnvoyTimestampResolution.MILLISECOND)
              httpCallHeaders["X-As-Req-Timestamp"] = _start_time
              request_handle:streamInfo():dynamicMetadata():set("envoy.filters.http.lua", "as.req-time", _start_time)

              -- No error case
              if httpCallHeaders["X-As-Error"] == "" then
                -- delete this key
                httpCallHeaders["X-As-Error"] = nil
              end

              httpCallHeaders["connection"] = nil
              httpCallHeaders["keep-alive"] = nil

              -- Cannot send transfer-encoding header, this is relevant only for upstream to downstream connection
              if httpCallHeaders["transfer-encoding"] ~= nil then
                httpCallHeaders["X-As-TE"] = httpCallHeaders["transfer-encoding"]
                httpCallHeaders["transfer-encoding"] = nil
              end

              local request_body = ""
              local _req_payload_size = request_handle:headers():get("content-length")
              if _content_too_large(_req_payload_size) or not _valid_content_type(httpCallHeaders["content-type"]) then
                if AS_DEBUG then request_handle:logInfo("as: request body too large or unsupported content type") end

                -- Setting this will ensure edge controller understands the payload being too large
                httpCallHeaders["x-as-req-truncated-body"] = "true"

                -- pass the actual content length if possible
                if _req_payload_size ~= nil then
                  httpCallHeaders["x-as-content-length"] = _req_payload_size
                end
              else
                -- Due to https://github.com/envoyproxy/envoy/issues/8785 request_handle:bodyChunks()
                -- cause probles in enforcement, therefore, reading the complete body in enforcement
                -- mode (Please note that it will result into buffering of the body in memory)
                if ENFORCEMENT then
                  local body_obj = request_handle:body()
                  if body_obj and body_obj:length() > 0 then
                    if body_obj:length() > MAX_PAYLOAD_SIZE then
                      if AS_DEBUG then request_handle:logInfo("as: request body too large") end
                      httpCallHeaders["x-as-req-truncated-body"] = "true"
                    else
                      request_body = body_obj:getBytes(0, body_obj:length())
                    end
                  end
                else
                  local _total_len = 0
                  local _body_chunks = {}
                  for chunk in request_handle:bodyChunks() do
                    local len = chunk:length()
                    if len > 0 then
                      local result = chunk:getBytes(0, len)
                      table.insert(_body_chunks, result)
                      _total_len = _total_len + len
                    end

                    if _total_len > MAX_PAYLOAD_SIZE then
                      if AS_DEBUG then request_handle:logInfo("as: request body too large") end
                      _body_chunks = {}
                      httpCallHeaders["x-as-req-truncated-body"] = "true"
                      break
                    end
                  end

                  if _total_len > 0 and #_body_chunks > 0 then
                    request_body = table.concat(_body_chunks)
                  end
                end
              end

              -- dont do asynch for enforcement mode
              local asynchronous = not ENFORCEMENT

              -- https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter
              local authz_headers, authz_body = request_handle:httpCall(
                "controller.default.dc1.internal.85c3ce6d-cc9e-db03-8944-2733bef37e23.consul",
                httpCallHeaders,
                request_body,
                AUTHZ_TIMEOUT,
                asynchronous)

              if ENFORCEMENT then
                local authz_status = authz_headers[":status"]
                authz_status = tonumber(authz_status)
                if FAILOPEN then
                  -- 4xx for failopen case only
                  if authz_status and (authz_status > 399 and authz_status < 500) then
                    request_handle:respond(authz_headers, authz_body)
                    return
                  end
                else
                  if authz_status and (authz_status > 399 and authz_status < 600) then
                    request_handle:respond(authz_headers, authz_body)
                    return
                  end
                end
              end

          -- end of function appsentinels_on_request
          end

          local function append_query_param(existing_path, param_name, param_value)
              if string.find(existing_path, "?") then
                  -- If there's already a query, append with &
                  return string.format("%s&%s=%s", existing_path, param_name, param_value)
              else
                  -- If there's no query yet, start with ?
                  return string.format("%s?%s=%s", existing_path, param_name, param_value)
              end
          end

          function appsentinels_on_response(response_handle)
              response_handle:logErr("sagarrrrrrrrrrrrrr In response: ")
              local _skip = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.skip"]
              if _skip ~= nil and _skip == true then
                if AS_DEBUG then response_handle:logDebug("as: Skipping resp logging, because req bypassed") end
                return
              end

              -- Create a new table to store headers for httpCall
              local httpCallHeaders = {}

              -- Get all headers from response_handle
              for name, value in pairs(response_handle:headers()) do
                -- skip headers starting with : , example :status
                if string.sub(name, 1, 1) == ":" then
                  -- response_handle:logErr("as: resp header not logging " .. name .. ": " .. value)
                else
                  httpCallHeaders[name] = value
                  -- response_handle:logErr("as: resp header name " .. name .. ": " .. value)
                end
              end

              local rid = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.x-request-id"]
              if rid ~= nil then
                httpCallHeaders["x-request-id"] = rid
              else
                if AS_DEBUG then response_handle:logErr("as: Resp: Missing x-request-id") end
              end

              -- For httpCall to work, we need :method, :path and "authority are must", in error cases, we must populate something
              httpCallHeaders["X-As-Error"] = ""
              local method = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.method"]
              if method ~= nil then
                  httpCallHeaders[":method"] = method
              else
                  httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing method;"
              end

              local path = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.path"]
              if path ~= nil then
                  -- path can contain query
                  local full_path = append_query_param(path, "sensor", "envoy-lua")
                  full_path = append_query_param(full_path, "cap", "nohb")
                  httpCallHeaders[":path"] = full_path
              else
                  httpCallHeaders[":path"] = "/"
                  httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing path;"
              end

              local host = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.authority"]
              if host ~= nil then
                  httpCallHeaders[":authority"] = host
              else
                  httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing host;"
              end

              httpCallHeaders["X-As-Req-Timestamp"] = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["as.req-time"]
              httpCallHeaders["X-As-Resp-Timestamp"] = response_handle:timestamp(EnvoyTimestampResolution.MILLISECOND)

              local status = response_handle:headers():get(":status")
              if status ~= nil then
                  httpCallHeaders["X-As-Status"] = status
              else
                  httpCallHeaders["X-As-Error"] = httpCallHeaders["X-As-Error"] .. "Missing status;"
                  -- What else to put here?
                  httpCallHeaders["X-As-Status"] = "200"
              end

              httpCallHeaders["X-As-Sensor"] = "envoy-http-access"
              httpCallHeaders["X-As-Sensor-Instance"] = INSTANCE_NAME
              httpCallHeaders["X-As-Sensor-Hostname"] = HOSTNAME

              -- No error case
              if httpCallHeaders["X-As-Error"] == "" then
                  -- delete this key
                  httpCallHeaders["X-As-Error"] = nil
              end

              httpCallHeaders["connection"] = nil
              httpCallHeaders["keep-alive"] = nil

              -- Cannot send transfer-encoding header, this is relevant only for upstream to downstream connection
              if httpCallHeaders["transfer-encoding"] ~= nil then
                  httpCallHeaders["X-As-TE"] = httpCallHeaders["transfer-encoding"]
                  httpCallHeaders["transfer-encoding"] = nil
              end

              -- The issue https://github.com/envoyproxy/envoy/issues/8785 does impact reading response
              -- body chunks, a synchronous call is not needed in this case, so not buffering the body
              -- in memory
              local _body_chunks = {}
              local _resp_payload_size = response_handle:headers():get("content-length")
              if _content_too_large(_resp_payload_size) or not _valid_content_type(httpCallHeaders["content-type"]) then
                if AS_DEBUG then response_handle:logInfo("as: resp body too large or unsupported content type") end
                httpCallHeaders["x-as-resp-truncated-body"] = "true"

                -- pass the actual content length if possible
                if _resp_payload_size ~= nil then
                  httpCallHeaders["x-as-content-length"] = _resp_payload_size
                end
              else
                local _total_len = 0
                for chunk in response_handle:bodyChunks() do
                  local len = chunk:length()
                  if len > 0 then
                    local result = chunk:getBytes(0, len)
                    table.insert(_body_chunks, result)
                    _total_len = _total_len + len
                  end

                  if _total_len > MAX_PAYLOAD_SIZE then
                    if AS_DEBUG then response_handle:logInfo("as: resp body too large") end
                    _body_chunks = {}
                    httpCallHeaders["x-as-resp-truncated-body"] = "true"
                    break
                  end
                end
              end

              local authz_headers, authz_body = response_handle:httpCall(
                "controller.default.dc1.internal.85c3ce6d-cc9e-db03-8944-2733bef37e23.consul",
                httpCallHeaders,
                table.concat(_body_chunks),
                ACCESSLOG_TIMEOUT,
                true)
          end

          function envoy_on_request(request_handle)
              appsentinels_on_request(request_handle)
          end
          function envoy_on_response(response_handle)
              appsentinels_on_response(response_handle)
          end
