lib/handlers/acs.ex

defmodule ACS.Handlers.ACS do
  @moduledoc """
  Handles CWMP requests coming in over the CWMP southbound interface.

  Requests are parsed as SOAP CWMP envelopes and then used for the basis
  of the current provisioning tree. There are several possible request types
  as defined in the TR-069 specification. These are all just handled by
  the custom logic module.

  Some are part of basic ACS functionality and are handled here.

  This is the main ACS server loop corresponding to the old acs3.pm
  """
  require Logger
  use Plug.Builder
  use Plug.ErrorHandler

  plug(Plug.Parsers, parsers: [ACS.CWMP.Parser])
  plug(:dispatch)

  def dispatch(conn, _params) do
    cookies = fetch_cookies(conn).req_cookies

    session_id =
      cond do
        Map.has_key?(cookies, "session") ->
          Logger.debug("session cookie in the request, must be decoded into did")
          session_id = cookies["session"]
          # verify the session. Do we have a session with this ID, and does the did therein
          # match the IP of this request?
          cond do
            ACS.Session.verify_session(session_id, to_string(:inet_parse.ntoa(conn.remote_ip))) ->
              session_id

            true ->
              Logger.debug("session does not verify - sending Bad Request")
              ""
          end

        # verify_Session
        true ->
          Logger.debug("no cookie set in the request - should be an Inform then")

          case conn.body_params do
            %{cwmp_version: _cwmp_ver, entries: entries, header: _header} ->
              # Something that was parseable by cwmp_ex.
              case hd(
                     Enum.map(entries, fn entry ->
                       if(Map.has_key?(entry, :device_id), do: entry.device_id)
                     end)
                   ) do
                nil ->
                  Logger.debug("Cant find device_id in request nor cookie, bogus!")
                  ""

                didstruct ->
                  Logger.debug("device_id in the body - must be Inform, start session")
                  session_id = UUID.uuid4(:hex)

                  extended_deviceid =
                    Map.merge(Map.from_struct(didstruct), %{
                      ip: to_string(:inet_parse.ntoa(conn.remote_ip))
                    })

                  ACS.Session.Supervisor.start_session(
                    session_id,
                    extended_deviceid,
                    conn.body_params
                  )

                  session_id
              end

            # has device_id
            _ ->
              # unparseable body and no cookie, bogus
              Logger.debug("Cant find cwmp output in request nor cookie, bogus!")
              ""
          end

          # conn.body_params
      end

    # fetch_cookies

    {code, resp, session_id} =
      case session_id do
        "" ->
          {400, "Bad request", ""}

        {:reject, reason} ->
          Logger.error("Request rejected: #{inspect(reason)}")
          {404, "Not found", ""}

        _ ->
          {c, r} = ACS.Session.process_message(session_id, conn.body_params)
          {c, r, session_id}
      end

    if resp == "", do: ACS.Session.Supervisor.end_session(session_id)

    conn
    |> put_resp_content_type("text/xml")
    |> put_resp_cookie("session", session_id)
    |> send_resp(code, resp)
  end

  @doc """
  When our cwmp parser thows, we end up here. So when some CPE sends bogus
  xml, or half a record or whatever else they might do, this is where we capture
  that parser error from CWMP.Parser.
  """
  def handle_errors(conn, %{kind: _kind, reason: reason, stack: _stack}) do
    error =
      case reason do
        %Plug.Parsers.ParseError{
          exception: %CWMP.Protocol.Parser.ParseError{message: err_message}
        } ->
          "CWMP.Parser error: #{err_message}"

        %Plug.Parsers.ParseError{exception: _some_other_exception} ->
          "Plug.Parsers.ParseError raised"

        _ ->
          "Exception raised by Plug.Parsers"
      end

    Logger.error("Plug error: #{error}")
    send_resp(conn, conn.status, error)
  end
end