-- vim: st=4 sts=4 sw=4 et:
--- Network backend using the [lua-nginx-module](https://github.com/openresty/lua-nginx-module) cosocket API.
-- This module can be used with the raw Lua module for nginx or the OpenResty
-- bundle.
--
-- As socket API is not available in all contexts, the messages are queued and
-- processed in a timer when necessary. This can be forced for all messages too
-- in order to not wait for the message to be sent (see the `async` filed).
--
-- It will require the `lua_ssl_trusted_certificate` to be set correctly when
-- reporting to a HTTPS endpoint.
--
-- @module raven.senders.ngx
-- @copyright 2014-2017 CloudFlare, Inc.
-- @license BSD 3-clause (see LICENSE file)

-- luacheck: globals ngx

local util = require 'raven.util'

local ngx_socket = ngx.socket
local ngx_get_phase = ngx.get_phase
local string_format = string.format
local table_remove = table.remove
local parse_dsn = util.parse_dsn
local generate_auth_header = util.generate_auth_header
local _VERSION = util._VERSION
local _M = {}

-- provide a more sensible implementation of the error log function
function util.errlog(...)
    ngx.log(ngx.ERR, 'raven-lua failure: ', ...)
end

-- as we don't want to use an external HTTP library, just send the HTTP request
-- directly using cosocket API
local HTTP_REQUEST = string.gsub([[POST %s HTTP/1.0
Host: %s
Connection: close
Content-Type: application/json
Content-Length: %d
User-Agent: %s
X-Sentry-Auth: %s

%s
]], '\r?\n', '\r\n')

local CALLBACK_DEFAULT_ERRMSG =
    "failed to configure socket (custom callback did not returned a value)"

local function send_msg(self, msg)
    local ok
    local sock, err = ngx_socket.tcp()
    if not sock then
        return nil, err
    end

    if self.configure_socket then
        ok, err = self.configure_socket(self, sock)
        if not ok then
            return nil, err or CALLBACK_DEFAULT_ERRMSG
        end
    end

    ok, err = sock:connect(self.target, self.port)
    if not ok then
        return nil, err
    end

    if self.protocol == 'https' then
        -- TODO: session resumption?
        ok, err = sock:sslhandshake(false, self.host, self.verify_ssl)
        if not ok then
            sock:close()
            return nil, err
        end
    end

    ok, err = sock:send(msg)
    if not ok then
        sock:close()
        return nil, err
    end

    local resp, partial
    resp, err, partial = sock:receive('*a')
    if err then
        -- If the connection was forcibly reset by the server after sending the
        -- response, it will look like an error. To cover for this, take the
        -- partial data as normal response and try to parse it
        -- See: https://github.com/cloudflare/raven-lua/issues/30
        if partial and partial ~= "" then
            resp = partial
        else
            return nil, err
        end
    end

    local status = resp:match("HTTP/%d%.%d (%d%d%d) %w+")
    if status ~= "200" then
        return nil, "Server response status not 200:" .. (status or "nil")
    end

    return resp:match("\r\n\r\n(.*)") or ""
end

-- async task pushing: sometimes the socket API is not available, in this case,
-- messages are pushed into a queue and sent in a timer. Only one timer at most
-- is running per instance (and per worker): timers are a scarce resource, we
-- don't want to exhaust the timer pool during message storms.

local function send_task(premature, self)
    if premature then
        return
    end

    local ok, err = xpcall(function()
        local send_queue = self.send_queue
        while #send_queue > 0 do
            local msg = send_queue[1]
            -- do not remove the message yet as an empty queue is the signal to
            -- re-start the task
            local ok, err = send_msg(self, msg)
            if not ok then
                ngx.log(ngx.ERR, 'raven: failed to send message asyncronously: ',
                    err, '. Drop the message.')
            end
            table_remove(send_queue, 1)
        end
    end, debug.traceback)

    if not ok then
        ngx.log(ngx.ERR, 'raven: failed to run the async sender task: ', err)
        -- TODO: restart the task here? requires a extra counter as we don't want
        -- to fail in loop indefinitely.
    end
    self.task_running = false
end


local mt = {}
mt.__index = mt

function mt:send(json_str)
    local auth = generate_auth_header(self)
    local msg = string_format(HTTP_REQUEST, self.request_uri, self.host, #json_str,
        "raven-lua-ngx/" .. _VERSION, auth, json_str)
    local phase = ngx_get_phase()
    -- rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*
    if (not self.async) and (
        phase == 'rewrite' or
        phase == 'access' or
        phase == 'content' or
        phase == 'timer' or
        phase == 'ssl_cert' or
        phase == 'ssl_session_fetch'
    ) then
        -- socket is available
        return send_msg(self, msg)
    else
        -- cannot use socket: push the message in the async queue
        local send_queue = self.send_queue
        local queue_size = #send_queue
        if queue_size <= self.queue_limit then
            send_queue[queue_size + 1] = msg
            if not self.task_running then
                local ok, err = ngx.timer.at(0, send_task, self)
                if not ok then
                    return nil, 'failed to schedule async sender task: ' .. err
                end

                -- assume the task is already running, as other messages might
                -- be reported before it is actually scheduled and run.
                self.task_running = true
            end
        else
            return nil, 'failed to send message asyncronously: queue is full'
        end
        return true
    end
end

--- Configuration table for the nginx sender.
-- @field dsn DSN string
-- @field verify_ssl Whether or not the SSL certificate is checked (boolean,
--  defaults to false)
-- @field configure_socket Callback used to configure the created socket (e.g.
--  adjusting the timeout). Called with the sender object and socket as arguments.
--  Must return `true` or `nil, error_message`.
-- @field queue_limit Maximum number of message in the asynchronous sending queue
--  (default: 50)
-- @field async Always send message asynchronously, even when it can be sent
--  right away. This is to prevent to slow down processing while contacting the
--  Sentry server. (default: false)
-- @table sender_conf

--- Create a new sender object for the given DSN
-- @param conf Configuration table, see @{sender_conf}
-- @return A sender object
function _M.new(conf)
    local obj, err = parse_dsn(conf.dsn)
    if not obj then
        return nil, err
    end

    obj.target = conf.target or obj.host
    obj.verify_ssl = conf.verify_ssl
    obj.configure_socket = conf.configure_socket
    obj.queue_limit = conf.queue_limit or 50
    obj.async = conf.async or false
    obj.send_queue = {}
    obj.task_running = false

    return setmetatable(obj, mt)
end

--- Returns the value of the `server_name` variable if possible.
-- Otherwise (wrong phase), this will return "undefined".
--
-- It is intended to be used as a `get_server_name` override on the main raven
-- instance.
--
-- @usage
-- local raven_ngx = require("raven.senders.ngx")
-- local rvn = raven.new(...)
-- rvn.get_server_name = raven_ngx.get_server_name
function _M.get_server_name()
    local phase = ngx.get_phase()
    -- the ngx.var.* API is not available in all contexts
    if phase == "set" or
        phase == "rewrite" or
        phase == "access" or
        phase == "content" or
        phase == "header_filter" or
        phase == "body_filter" or
        phase == "log"
    then
        return ngx.var.server_name
    end
    return "undefined"
end

return _M
