%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:
%%%
%%%------------------------------------------------------------------------
%%% @doc
%%% ==Erlang syslog Client Interface==
%%% A minimal syslog client interface that supports both RFC3164 and RFC5424.
%%% @end
%%%
%%% MIT License
%%%
%%% Copyright (c) 2016-2018 Michael Truog <mjtruog at protonmail dot com>
%%%
%%% Permission is hereby granted, free of charge, to any person obtaining a
%%% copy of this software and associated documentation files (the "Software"),
%%% to deal in the Software without restriction, including without limitation
%%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%%% and/or sell copies of the Software, and to permit persons to whom the
%%% Software is furnished to do so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included in
%%% all copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%%% DEALINGS IN THE SOFTWARE.
%%%
%%% @author Michael Truog <mjtruog at protonmail dot com>
%%% @copyright 2016-2018 Michael Truog
%%% @version 1.7.3 {@date} {@time}
%%%------------------------------------------------------------------------
-module(syslog_socket).
-author('mjtruog at protonmail dot com').

-behaviour(gen_server).

%% external interface
-export([start_link/1,
         start_monitor/1,
         stop_link/1,
         stop_link/2,
         stop_monitor/1,
         stop_monitor/2,
         send/3,
         send/4,
         send/5,
         facility/1,
         facility_valid/1,
         severity/1,
         severity_valid/1]).

%% gen_server callbacks
-export([init/1,
         handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

-define(TRANSPORT_DEFAULT, local).
-define(PROTOCOL_DEFAULT, rfc3164).
-define(UTF8_DEFAULT, true).
-define(FACILITY_DEFAULT, local0).
-define(TIMEOUT_DEFAULT, 5000). % milliseconds
-define(TIMEOUT_MAX_ERLANG, 4294967295). % milliseconds
-define(SP, $\s).

-type transport() :: local | udp | tcp | tls.
-type protocol() :: rfc3164 | rfc5424.
-type timeout_milliseconds() :: 1..?TIMEOUT_MAX_ERLANG.
-type app_name() :: nonempty_string().
-type facility() :: kernel | user | mail | daemon | auth0 | syslog |
                    print | news | uucp | clock0 | auth1 | ftp | ntp |
                    auth2 | auth3 | clock1 | local0 | local1 | local2 |
                    local3 | local4 | local5 | local6 | local7 |
                    non_neg_integer() |
                    % common aliases
                    auth | authpriv | cron | kern | lpr | security.
-type severity() :: emergency | alert | critical | error | warning |
                    notice | informational | debug |
                    0..7 |
                    % common aliases
                    emerg | panic | crit | err | warn | info.
-type message_id() :: string() | binary().
-type options() :: list({transport, transport()} |
                        {transport_options, list()} |
                        {protocol, protocol()} |
                        {utf8, boolean()} |
                        {facility, facility()} |
                        {app_name, app_name()} |
                        {path, nonempty_string()} |
                        {host, inet:ip_address() | inet:hostname()} |
                        {port, undefined | inet:port_number()} |
                        {timeout, timeout_milliseconds()}).
-export_type([transport/0,
              protocol/0,
              timeout_milliseconds/0,
              app_name/0,
              facility/0,
              severity/0,
              message_id/0,
              options/0]).

-record(state,
        {
            transport :: transport(),
            transport_options :: list(),
            protocol :: protocol(),
            utf8 :: boolean(),
            utf8_bom :: binary(),
            facility :: non_neg_integer(),
            app_name :: app_name(),
            path :: nonempty_string(),
            host :: inet:ip_address() | inet:hostname(),
            port :: inet:port_number(),
            timeout :: pos_integer(),
            socket = undefined :: undefined | inet:socket() | ssl:sslsocket(),
            hostname = undefined :: undefined | string(),
            os_pid = undefined :: undefined | string()
        }).

%%%------------------------------------------------------------------------
%%% External interface functions
%%%------------------------------------------------------------------------

%%-------------------------------------------------------------------------
%% @doc
%% ===Start a syslog socket with a link.===
%% Connection is immediate within the new Erlang process.
%% @end
%%-------------------------------------------------------------------------

-spec start_link(Options :: options()) ->
    {ok, pid()} | {error, any()}.

start_link(Options) ->
    case start(Options) of
        {ok, Pid} = Success ->
            true = erlang:link(Pid),
            Success;
        {error, _} = Error ->
            Error
    end.

%%-------------------------------------------------------------------------
%% @doc
%% ===Start a syslog socket with a monitor.===
%% Connection is immediate within the new Erlang process.
%% @end
%%-------------------------------------------------------------------------

-spec start_monitor(Options :: options()) ->
    {ok, pid()} | {error, any()}.

start_monitor(Options) ->
    case start(Options) of
        {ok, Pid} = Success ->
            _ = erlang:monitor(process, Pid),
            Success;
        {error, _} = Error ->
            Error
    end.

%%-------------------------------------------------------------------------
%% @doc
%% ===Stop an existing syslog socket link asynchronously.===
%% @end
%%-------------------------------------------------------------------------

-spec stop_link(Pid :: pid()) ->
    ok.

stop_link(Pid) ->
    gen_server:cast(Pid, stop).

%%-------------------------------------------------------------------------
%% @doc
%% ===Stop an existing syslog socket link synchronously.===
%% @end
%%-------------------------------------------------------------------------

-spec stop_link(Pid :: pid(),
                Timeout :: timeout_milliseconds()) ->
    ok.

stop_link(Pid, Timeout) ->
    gen_server:call(Pid, stop, Timeout).

%%-------------------------------------------------------------------------
%% @doc
%% ===Stop an existing syslog socket monitor asynchronously.===
%% The parent process will still receive {'DOWN', _, process, Pid, Info}.
%% @end
%%-------------------------------------------------------------------------

-spec stop_monitor(Pid :: pid()) ->
    ok.

stop_monitor(Pid) ->
    gen_server:cast(Pid, stop).

%%-------------------------------------------------------------------------
%% @doc
%% ===Stop an existing syslog socket monitor synchronously.===
%% @end
%%-------------------------------------------------------------------------

-spec stop_monitor(Pid :: pid(),
                   Timeout :: timeout_milliseconds()) ->
    ok.

stop_monitor(Pid, Timeout) ->
    ok = gen_server:call(Pid, stop, Timeout),
    receive
        {'DOWN', _, process, Pid, normal} ->
            ok;
        {'DOWN', _, process, Pid, Info} ->
            erlang:exit(Info)
    after
        Timeout ->
            erlang:exit(timeout)
    end.

%%-------------------------------------------------------------------------
%% @doc
%% ===Send data to syslog without a timestamp or message_id.===
%% @end
%%-------------------------------------------------------------------------

-spec send(Pid :: pid(),
           Severity :: severity(),
           Data :: iodata()) ->
    ok.

send(Pid, Severity, Data) ->
    send(Pid, Severity, undefined, "", Data).

%%-------------------------------------------------------------------------
%% @doc
%% ===Send data to syslog without a message_id.===
%% @end
%%-------------------------------------------------------------------------

-spec send(Pid :: pid(),
           Severity :: severity(),
           Timestamp :: undefined | erlang:timestamp(),
           Data :: iodata()) ->
    ok.

send(Pid, Severity, Timestamp, Data) ->
    send(Pid, Severity, Timestamp, "", Data).

%%-------------------------------------------------------------------------
%% @doc
%% ===Send data to syslog.===
%% @end
%%-------------------------------------------------------------------------

-spec send(Pid :: pid(),
           Severity :: severity(),
           Timestamp :: undefined | erlang:timestamp(),
           MessageId :: message_id(),
           Data :: iodata()) ->
    ok.

send(Pid, Severity, Timestamp, MessageId, Data) ->
    gen_server:cast(Pid, {send, Severity, Timestamp, MessageId, Data}).

%%%------------------------------------------------------------------------
%%% facility and severity values are based on
%%% https://tools.ietf.org/html/rfc3164#section-4.1.1
%%% https://tools.ietf.org/html/rfc5424#section-6.2.1
%%%------------------------------------------------------------------------

%%-------------------------------------------------------------------------
%% @doc
%% ===Get the syslog facility numerical value.===
%% @end
%%-------------------------------------------------------------------------

-spec facility(Facility :: facility()) ->
    non_neg_integer().

facility(kernel) ->         0; % kernel messages
facility(user) ->           1; % user-level messages
facility(mail) ->           2; % mail system
facility(daemon) ->         3; % system daemons
facility(auth0) ->          4; % security/authorization messages 0
facility(syslog) ->         5; % messages generated internally by syslogd
facility(print) ->          6; % line printer subsystem
facility(news) ->           7; % network news subsystem
facility(uucp) ->           8; % UUCP subsystem
facility(clock0) ->         9; % clock daemon 0
facility(auth1) ->         10; % security/authorization messages 1
facility(ftp) ->           11; % FTP daemon
facility(ntp) ->           12; % NTP subsystem
facility(auth2) ->         13; % log audit (security/authorization 2)
facility(auth3) ->         14; % log alert (security/authorization 3)
facility(clock1) ->        15; % clock daemon 1
facility(local0) ->        16; % local use 0
facility(local1) ->        17; % local use 1
facility(local2) ->        18; % local use 2
facility(local3) ->        19; % local use 3
facility(local4) ->        20; % local use 4
facility(local5) ->        21; % local use 5
facility(local6) ->        22; % local use 6
facility(local7) ->        23; % local use 7
facility(auth) ->          facility(auth0);
facility(authpriv) ->      facility(auth1);
facility(cron) ->          facility(clock0);
facility(kern) ->          facility(kernel);
facility(lpr) ->           facility(print);
facility(security) ->      facility(auth0);
facility(Facility) when is_integer(Facility), Facility >= 0 ->
    Facility.

%%-------------------------------------------------------------------------
%% @doc
%% ===Test the validity of a syslog facility value.===
%% @end
%%-------------------------------------------------------------------------

-spec facility_valid(Facility :: facility()) ->
    boolean().

facility_valid(Facility) ->
    try facility(Facility) of
        _ -> true
    catch
        _:_ -> false
    end.

%%-------------------------------------------------------------------------
%% @doc
%% ===Get the syslog severity numerical value.===
%% @end
%%-------------------------------------------------------------------------

-spec severity(Severity :: severity()) ->
    0..7.

severity(emergency) ->      0; % system is unusable
severity(alert) ->          1; % action must be taken immediately
severity(critical) ->       2; % critical conditions
severity(error) ->          3; % error conditions
severity(warning) ->        4; % warning conditions
severity(notice) ->         5; % normal but significant condition
severity(informational) ->  6; % informational messages
severity(debug) ->          7; % debug-level messages
severity(emerg) ->          severity(emergency);
severity(panic) ->          severity(emergency);
severity(crit) ->           severity(critical);
severity(err) ->            severity(error);
severity(warn) ->           severity(warning);
severity(info) ->           severity(informational);
severity(Severity) when is_integer(Severity), Severity >= 0, Severity =< 7 ->
    Severity.

%%-------------------------------------------------------------------------
%% @doc
%% ===Test the validity of a syslog severity value.===
%% @end
%%-------------------------------------------------------------------------

-spec severity_valid(Severity :: severity()) ->
    boolean().

severity_valid(Severity) ->
    try severity(Severity) of
        _ -> true
    catch
        _:_ -> false
    end.

%%%------------------------------------------------------------------------
%%% Callback functions from gen_server
%%%------------------------------------------------------------------------

init([Transport, TransportOptions, Protocol,
      UTF8, Facility, AppName,
      Path, Host, Port, Timeout]) ->
    State0 = #state{transport = Transport,
                    transport_options = TransportOptions,
                    protocol = Protocol,
                    utf8 = UTF8,
                    utf8_bom = unicode:encoding_to_bom(utf8),
                    facility = Facility,
                    app_name = AppName,
                    path = Path,
                    host = Host,
                    port = Port,
                    timeout = Timeout},
    case transport_open(State0) of
        {ok, State1} ->
            StateN = protocol_init(State1),
            case transport_send(<<>>, StateN) of
                ok ->
                    {ok, StateN};
                {error, Reason} ->
                    {stop, Reason}
            end;
        {error, Reason} ->
            {stop, Reason}
    end.

handle_call(stop, _, State) ->
    {stop, normal, ok, State};
handle_call(Request, _, State) ->
    {stop, {call, Request}, State}.

handle_cast({send, Severity, Timestamp0, MessageId, Data}, State) ->
    TimestampN = case Timestamp0 of
        undefined ->
            os:timestamp();
        {_, _, _} ->
            Timestamp0
    end,
    HEADER = protocol_header(Severity, TimestampN, MessageId, State),
    DATA = protocol_data(State),
    MSG = protocol_msg(Data, State),
    case transport_send([HEADER, DATA, MSG], State) of
        ok ->
            {noreply, State};
        {error, Reason} ->
            {stop, Reason, State}
    end;
handle_cast(stop, State) ->
    {stop, normal, State};
handle_cast(Request, State) ->
    {stop, {cast, Request}, State}.

handle_info({Reason, Socket}, #state{socket = Socket} = State)
    when Reason =:= udp_closed;
         Reason =:= tcp_closed;
         Reason =:= ssl_closed ->
    {stop, Reason, State#state{socket = undefined}};
handle_info({Reason, Socket}, #state{socket = Socket} = State)
    when Reason =:= udp_error;
         Reason =:= tcp_error;
         Reason =:= ssl_error ->
    {stop, Reason, State};
handle_info(Request, State) ->
    {stop, {info, Request}, State}.

terminate(_, State) ->
    ok = transport_close(State),
    ok.

code_change(_, State, _) ->
    {ok, State}.

%%%------------------------------------------------------------------------
%%% Private functions
%%%------------------------------------------------------------------------

start(Options) when is_list(Options) ->
    Defaults = [{transport, ?TRANSPORT_DEFAULT},
                {transport_options, undefined},
                {protocol, ?PROTOCOL_DEFAULT},
                {utf8, ?UTF8_DEFAULT},
                {facility, ?FACILITY_DEFAULT},
                {app_name, undefined},
                {path, "/dev/log"},
                {host, {127,0,0,1}},
                {port, undefined},
                {timeout, ?TIMEOUT_DEFAULT}],
    [Transport, TransportOptionsValue, Protocol,
     UTF8, FacilityValue, AppName,
     Path, Host, PortValue, Timeout] = take_values(Defaults, Options),
    true = is_list(AppName) andalso is_integer(hd(AppName)), % required
    if
        Protocol =:= rfc3164 ->
            ok;
        Protocol =:= rfc5424 ->
            true = (length(AppName) =< 48)
    end,
    true = is_boolean(UTF8),
    Facility = facility(FacilityValue),
    true = is_integer(Timeout) andalso
           (Timeout > 0) andalso (Timeout =< ?TIMEOUT_MAX_ERLANG),
    TransportOptions = if
        Transport =:= local ->
            if
                TransportOptionsValue =:= undefined ->
                    [local];
                is_list(TransportOptionsValue) ->
                    [local | TransportOptionsValue]
            end;
        Transport =:= udp ->
            if
                TransportOptionsValue =:= undefined ->
                    [];
                is_list(TransportOptionsValue) ->
                    TransportOptionsValue
            end;
        Transport =:= tcp ->
            if
                TransportOptionsValue =:= undefined ->
                    [{send_timeout, Timeout},
                     {send_timeout_close, true},
                     {keepalive, true},
                     {reuseaddr, true}];
                is_list(TransportOptionsValue) ->
                    TransportOptionsValue
            end;
        Transport =:= tls ->
            true = is_list(TransportOptionsValue) andalso
                   (length(TransportOptionsValue) > 0),
            TransportOptionsValue
    end,
    true = is_list(Path) andalso is_integer(hd(Path)),
    Port = if
        PortValue =:= undefined ->
            if
                Transport =:= local ->
                    0;
                Transport =:= udp ->
                    514;
                Transport =:= tcp ->
                    601;
                Transport =:= tls ->
                    6514
            end;
        is_integer(PortValue), PortValue > 0 ->
            PortValue
    end,
    gen_server:start(?MODULE,
                     [Transport, TransportOptions, Protocol,
                      UTF8, Facility, AppName,
                      Path, Host, Port, Timeout], []).

protocol_init(#state{protocol = rfc3164} = State) ->
    HostnameOnly = lists:takewhile(fun(C) -> C /= $. end, hostname()),
    State#state{hostname = HostnameOnly,
                os_pid = os:getpid()};
protocol_init(#state{protocol = rfc5424} = State) ->
    State#state{hostname = hostname(),
                os_pid = os:getpid()}.

protocol_header(Severity, Timestamp, MessageId,
                #state{transport = Transport,
                       protocol = rfc3164,
                       facility = Facility,
                       app_name = APP_NAME,
                       hostname = Hostname,
                       os_pid = PROCID}) ->
    PRIVAL = (Facility bsl 3) + severity(Severity),
    TIMESTAMP = timestamp_rfc3164(Timestamp),
    HOSTNAME_SP = if
        Transport =:= local ->
            [];
        true ->
            [Hostname, ?SP]
    end,
    SP_MSGID = case MessageId of
        [] ->
            [];
        <<>> ->
            [];
        _ ->
            [?SP, MessageId]
    end,
    [$<, int_to_dec_list(PRIVAL), $>,
     TIMESTAMP, ?SP,
     HOSTNAME_SP,
     % extra information as MSG prefix
     APP_NAME, $[, PROCID, $], $:, SP_MSGID];
protocol_header(Severity, Timestamp, MessageId,
                #state{protocol = rfc5424,
                       facility = Facility,
                       app_name = APP_NAME,
                       hostname = HOSTNAME,
                       os_pid = PROCID}) ->
    PRIVAL = (Facility bsl 3) + severity(Severity),
    VERSION = $1,
    TIMESTAMP = timestamp_rfc5424(Timestamp),
    MSGID = case MessageId of
        [] ->
            $-;
        <<>> ->
            $-;
        _ ->
            truncate(MessageId, 32)
    end,
    [$<, int_to_dec_list(PRIVAL), $>,
     VERSION, ?SP,
     TIMESTAMP, ?SP,
     truncate(HOSTNAME, 255), ?SP,
     truncate(APP_NAME, 48), ?SP,
     PROCID, ?SP,
     MSGID].

protocol_data(#state{protocol = rfc3164}) ->
    [];
protocol_data(#state{protocol = rfc5424}) ->
    STRUCTURED_DATA = $-,
    [?SP, STRUCTURED_DATA].

protocol_msg(Data,
             #state{utf8 = UTF8,
                    utf8_bom = UTF8BOM}) ->
    if
        Data == <<>>; Data == [] ->
            [];
        UTF8 =:= true ->
            [?SP, UTF8BOM, Data];
        UTF8 =:= false ->
            [?SP, Data] % ASCII
    end.

timestamp_rfc3164({_, _, _} = Timestamp) ->
    {{_, DateMM, DateDD},
     {TimeHH, TimeMM, TimeSS}} = calendar:now_to_local_time(Timestamp),
    [DateMM0, DateMM1, DateMM2] = if
        DateMM ==  1 -> "Jan";
        DateMM ==  2 -> "Feb";
        DateMM ==  3 -> "Mar";
        DateMM ==  4 -> "Apr";
        DateMM ==  5 -> "May";
        DateMM ==  6 -> "Jun";
        DateMM ==  7 -> "Jul";
        DateMM ==  8 -> "Aug";
        DateMM ==  9 -> "Sep";
        DateMM == 10 -> "Oct";
        DateMM == 11 -> "Nov";
        DateMM == 12 -> "Dec"
    end,
    [DateDD0, DateDD1] = int_to_dec_list(DateDD, 2, $\s),
    [TimeHH0, TimeHH1] = int_to_dec_list(TimeHH, 2, $0),
    [TimeMM0, TimeMM1] = int_to_dec_list(TimeMM, 2, $0),
    [TimeSS0, TimeSS1] = int_to_dec_list(TimeSS, 2, $0),
    [DateMM0, DateMM1, DateMM2, $\s,
     DateDD0, DateDD1, $\s,
     TimeHH0, TimeHH1, $:, TimeMM0, TimeMM1, $:, TimeSS0, TimeSS1].

timestamp_rfc5424({_, _, MicroSeconds} = Timestamp) ->
    {{DateYYYY, DateMM, DateDD},
     {TimeHH, TimeMM, TimeSS}} = calendar:now_to_universal_time(Timestamp),
    [DateYYYY0, DateYYYY1,
     DateYYYY2, DateYYYY3] = int_to_dec_list(DateYYYY, 4, $0),
    [DateMM0, DateMM1] = int_to_dec_list(DateMM, 2, $0),
    [DateDD0, DateDD1] = int_to_dec_list(DateDD, 2, $0),
    [TimeHH0, TimeHH1] = int_to_dec_list(TimeHH, 2, $0),
    [TimeMM0, TimeMM1] = int_to_dec_list(TimeMM, 2, $0),
    [TimeSS0, TimeSS1] = int_to_dec_list(TimeSS, 2, $0),
    [MicroSeconds0, MicroSeconds1,
     MicroSeconds2, MicroSeconds3,
     MicroSeconds4, MicroSeconds5] = int_to_dec_list(MicroSeconds, 6, $0),
    [DateYYYY0, DateYYYY1, DateYYYY2, DateYYYY3, $-,
     DateMM0, DateMM1, $-, DateDD0, DateDD1, $T,
     TimeHH0, TimeHH1, $:, TimeMM0, TimeMM1, $:, TimeSS0, TimeSS1, $.,
     MicroSeconds0, MicroSeconds1,
     MicroSeconds2, MicroSeconds3,
     MicroSeconds4, MicroSeconds5, $Z].

transport_open(#state{transport = Transport,
                      transport_options = TransportOptions} = State)
    when Transport =:= local; Transport =:= udp ->
    try gen_udp:open(0, TransportOptions) of
        {ok, Socket} ->
            {ok, State#state{socket = Socket}};
        {error, _} = Error ->
            Error
    catch
        ErrorType:ErrorReason ->
            {error, {ErrorType, ErrorReason}}
    end;
transport_open(#state{transport = tcp,
                      transport_options = TransportOptions,
                      host = Host,
                      port = Port,
                      timeout = Timeout} = State) ->
    try gen_tcp:connect(Host, Port, TransportOptions, Timeout) of
        {ok, Socket} ->
            {ok, State#state{socket = Socket}};
        {error, _} = Error ->
            Error
    catch
        ErrorType:ErrorReason ->
            {error, {ErrorType, ErrorReason}}
    end;
transport_open(#state{transport = tls,
                      transport_options = TransportOptions,
                      host = Host,
                      port = Port,
                      timeout = Timeout} = State) ->
    try ssl:connect(Host, Port, TransportOptions, Timeout) of
        {ok, Socket} ->
            {ok, State#state{socket = Socket}};
        {error, _} = Error ->
            Error
    catch
        ErrorType:ErrorReason ->
            {error, {ErrorType, ErrorReason}}
    end.

transport_send(Data, #state{transport = local,
                            path = Path,
                            port = Port,
                            socket = Socket}) ->
    gen_udp:send(Socket, {local, Path}, Port, Data);
transport_send(Data, #state{transport = udp,
                            host = Host,
                            port = Port,
                            socket = Socket}) ->
    gen_udp:send(Socket, Host, Port, Data);
transport_send(Data, #state{transport = tcp,
                            socket = Socket}) ->
    gen_tcp:send(Socket, Data);
transport_send(Data, #state{transport = tls,
                            socket = Socket}) ->
    ssl:send(Socket, Data).

transport_close(#state{socket = undefined}) ->
    ok;
transport_close(#state{transport = Transport,
                       socket = Socket})
    when Transport =:= local; Transport =:= udp ->
    _ = (catch gen_udp:close(Socket)),
    ok;
transport_close(#state{transport = tcp,
                       socket = Socket}) ->
    _ = (catch gen_tcp:close(Socket)),
    ok;
transport_close(#state{transport = tls,
                       socket = Socket}) ->
    _ = (catch ssl:close(Socket)),
    ok.

hostname() ->
    case lists:dropwhile(fun(C) -> C /= $@ end,
                         erlang:atom_to_list(node())) of
        [$@ | "nohost"] ->
            {ok, Hostname} = inet:gethostname(),
            Hostname;
        [$@ | FQDN] ->
            FQDN
    end.

truncate(Value, N) when is_list(Value) ->
    lists:sublist(Value, N);
truncate(Value, N) when is_binary(Value) ->
    case Value of
        <<ValuePrefix:N/binary-unit:8, _/binary>> ->
            ValuePrefix;
        _ ->
            Value
    end.

take_values(DefaultList, List)
    when is_list(DefaultList), is_list(List) ->
    take_values([], DefaultList, List).
take_values(Result, [], List) ->
    lists:reverse(Result) ++ List;
take_values(Result, [{Key, Default} | DefaultList], List) ->
    case lists:keytake(Key, 1, List) of
        false ->
            take_values([Default | Result], DefaultList, List);
        {value, {Key, Value}, RemainingList} ->
            take_values([Value | Result], DefaultList, RemainingList)
    end.

int_to_dec_list(I) when is_integer(I), I >= 0 ->
    int_to_dec_list([], I).

int_to_dec_list(L, I)
    when I < 10 ->
    [int_to_dec(I) | L];
int_to_dec_list(L, I) ->
    int_to_dec_list([int_to_dec(I rem 10) | L], I div 10).

int_to_dec_list(I, N, Char) when is_integer(I), I >= 0 ->
    int_to_dec_list([], I, 1, N, Char).

int_to_dec_list(L, I, Count, N, Char)
    when I < 10 ->
    int_to_list_pad([int_to_dec(I) | L], N - Count, Char);
int_to_dec_list(L, I, Count, N, Char) ->
    int_to_dec_list([int_to_dec(I rem 10) | L], I div 10, Count + 1, N, Char).

int_to_list_pad(L, 0, _) ->
    L;
int_to_list_pad(L, Count, Char) ->
    int_to_list_pad([Char | L], Count - 1, Char).

int_to_dec(I) when 0 =< I, I =< 9 ->
    I + $0.

