defmodule ACS.Session do use GenServer use Prometheus.Metric require Logger @moduledoc """ The actual ACS-CPE session is handled here. A session is initiated when an Inform arrives, therefore the init function takes a parsed Inform request as an argument. We let the supervisor handle the main session handler, and then we register new sessions with the session_begins method """ @doc """ For the supervisor. Ths supervisors starts the sessions from start_child """ def start_link(session_module, session_id: session_id, device_id: device_id, message: message, fun: fun ) do Logger.debug( "ACS.Session start_link(#{inspect(session_id)},#{inspect(device_id)}) with function" ) Gauge.inc(name: :acs_ex_nof_sessions, labels: [device_id.product_class]) GenServer.start_link(__MODULE__, [session_module, session_id, device_id, message, fun]) end def start_link(session_module, session_id: session_id, device_id: device_id, message: message ) do Logger.debug("ACS.Session start_link(#{inspect(session_id)},#{inspect(device_id)})") Gauge.inc(name: :acs_ex_nof_sessions, labels: [device_id.product_class]) GenServer.start_link(__MODULE__, [session_module, session_id, device_id, message, nil]) end # API @doc """ when stuff is sent into this session, like CWMP messages or other stuff. """ def process_message(session_id, message) do try do timeout = case Application.fetch_env(:acs_ex, :session_timeout) do {:ok, to} -> to :error -> 30000 end GenServer.call(via_tuple(session_id), {:process_message, [session_id, message]}, timeout) catch # timeout comes as :exit, reason. :exit, reason -> case reason do # Generate fault response? Or maybee just end the session by returning "" {:timeout, _} -> # Will it depend on the state of things? msg = hd(message) CWMP.Protocol.Generator.generate( %CWMP.Protocol.Messages.Header{id: msg.header.id}, %CWMP.Protocol.Messages.Fault{ faultcode: "Server", faultstring: "CWMP fault", detail: %CWMP.Protocol.Messages.FaultStruct{ code: "8002", string: "Internal error" } } ) {what, ever} -> {what, ever} end end end @doc """ When something non-Inform'ish is sent into the session we need to find and verify the session. """ def verify_session(session_id, remote_host) do try do timeout = case Application.fetch_env(:acs_ex, :session_timeout) do {:ok, to} -> to :error -> 30000 end case GenServer.call(via_tuple(session_id), {:verify_remotehost, [remote_host]}, timeout) do {:noproc, _} -> false host_verify_result -> host_verify_result end catch # timeout comes as :exit, reason. :exit, _reason -> false end end @doc """ Script message. This means the a scripting process wants a response to a request we just put the request in the plug queue and answer no_reply here. """ def script_command(device_id, command) do # put it into the script_element Logger.debug("API script_command called...#{inspect(device_id)}, #{inspect(command)}") try do timeout = case Application.fetch_env(:acs_ex, :script_timeout) do {:ok, to} -> to :error -> 2000 end Logger.debug("API script_command got timeout value: #{timeout}") GenServer.call(via_tuple(device_id), {:script_command, [command]}, timeout) catch :exit, reason -> case reason do # reply with timeout {:timeout, _} -> Logger.debug("API script_command timeout occured!") {:error, "timeout"} {what, ever} -> Logger.debug("Whatever occured #{inspect(what)}, #{inspect(ever)}") {what, ever} end end end defp takeover_session(session_id, tries \\ 5) defp takeover_session(_session_id, 0), do: {:error, "Could not take over session"} defp takeover_session(session_id, tries) do case :gproc.reg_or_locate({:n, :l, {:session_id, session_id}}) do {other, _} when other == self() -> :ok {other, _} -> ref = Process.monitor(other) # TODO: Maybe send a poison pill? Process.exit(other, :kill) receive do {:DOWN, _ref, :process, _other, _} -> takeover_session(session_id, tries - 1) after 1000 -> Process.demonitor(ref, [:flush]) takeover_session(session_id, tries - 1) end end end # SERVER def init([script_module, session_id, device_id, message, fun]) do # This should only be called when the Plug gets an Inform, it this up # to me to check, or the caller? I will assume caller. # This conn.body_params must be a parsed Inform, if not - ignore # Queue the response in the plug_element, so that it can be popped with next response # InformResponse into the plug queue Logger.metadata(serial: device_id.serial_number, sessionid: session_id) gspid = self() # TODO: Should be "first inform encountered", not just hd sspid = spawn_link(__MODULE__, :session_prestart, [ gspid, script_module, device_id, hd(message.entries), session_id, fun ]) # Start session script process, save pid to state case takeover_session(session_id) do :ok -> Process.flag(:trap_exit, true) {:ok, %{ device_id: device_id, session_id: session_id, script_element: nil, plug_element: nil, unmatched_incomming_list: [], sspid: sspid, cwmp_version: message.cwmp_version }} _ -> {:stop, "Could not take over session"} end end @doc """ Used for :trap_exit 1. signal with reply/2 that this is over 2. kill me? """ def handle_info({:EXIT, _pid, _reason}, state) do ## Session Script is done. Logger.debug("Script system exited.") case state.plug_element do nil -> Logger.debug( "Session script exited, and we have no waiting plug, leave the plug some time to end session" ) pe -> # Waiting plug, we have to tell it to stop by sending {204,""} Logger.debug( "Waiting plug when SS ends, just tell it to stop, which in turn will kill me (the session)" ) GenServer.reply(pe.from, {204, ""}) end {:noreply, %{state | plug_element: nil, script_element: nil, sspid: nil}, 5000} end def handle_info(:timeout, state) do # Kill self... Logger.warn("Session died due to timeout") # Update the Prometheus metrics Counter.inc(name: :acs_ex_dead_sessions, labels: [state.device_id.product_class]) {:stop, :timeout, state} end def handle_info(message, state) do Logger.error("Unhandled handle_info(#{inspect(message)}, #{inspect(state)})") end def terminate(reason, state) do Logger.debug("Session terminate called: #{inspect(reason)}, #{inspect(state)}") Gauge.dec(name: :acs_ex_nof_sessions, labels: [state.device_id.product_class]) :normal end def handle_call({:script_command, [command]}, from, state) do Logger.debug("handle_call(:script_command, [#{inspect(command)}])") case state.plug_element do %{message: _msg, from: plug_from, state: :waiting} -> # A plug is waiting when a scripting function has not ended, and the plug is ready for more requests # meaning it received "" from a CPE indicating that the CPE has nothing more. We keep waiting # because the scripting system is supposed to introduce new reqeusts, that is its purpose, and # as long as it is not dead, we must expect more. Logger.debug( "Session Script discovered a waiting plug. Sending scripted command at once!" ) case gen_request(command.method, command.args, "script", state.cwmp_version) do {:ok, {id, req}} -> GenServer.reply(plug_from, {200, req}) {:noreply, %{ state | plug_element: nil, script_element: %{command: command, from: from, state: :sent, id: id} }} {:error, msg} -> # some error should be returned to "from" who is the SS {:reply, {:error, msg}, %{state | script_element: nil}} end _ -> Logger.debug( "No known plug_element, meaning no plug is waiting, so store script command in state" ) # Just put the command in the script element, we wont affect the plug queue until the # session reaches the "what now?" stage (empty request from device) case state.script_element do nil -> {:noreply, %{state | script_element: %{command: command, from: from, state: :unhandled}}} _ -> Logger.error("Unable to handle multiple scripting commands at the time.") {:reply, :error, state} end end end @doc """ :verify_remotehost - verifies the remote_host by comparing it to the one in the state.device_id :process_message - Processes a message from the plug. message is the CWMP.Protocol version of the parsed request sent into the plug. """ def handle_call({:verify_remotehost, [remote_host]}, _from, state) do {:reply, state.device_id.ip == remote_host, state} end def handle_call({:process_message, [session_id, message]}, from, state) do Logger.debug("handle_call(:process_message, #{session_id}, #{inspect(message)})") # If this message is an Inform, it can be ignored, because the response to that # has already been queue in the plug_element by init. # If this message is empty, it means that the session is about to end, unless # we have something more to send. # Anything else means that the front element in the script_element must be responsible for # this thing arriving, and the script is currently awaiting this reply, so we must :reply # now. {plug, script, unmatched, sspid} = case length(Map.keys(message)) do 0 -> # Empty message here. This means examine the script_element to see if there are # any new scripted message to push into the plug queue. If nothing can be found, # push "" into the plug_element - ending the session. The Plug should kill it... Logger.debug( "Empty message discovered: sspid: #{inspect(state.sspid)}, script_element: #{inspect(state.script_element)}" ) case state.script_element do nil -> # nothing in the script thing, we can end the session... if the session script process has exited. Logger.debug("No script_element") case state.sspid do nil -> # no session script Logger.debug("No script pid") # we can stop... {{204, ""}, nil, [], state.sspid} sspid -> # Session script is going, but no element?? Maybee this is before it could queue # or maybee its in some long operation if Process.alive?(sspid) do Logger.debug("Script system IS alive ....") # If we have an :unscripted waiting, reply now {:noreply, nil, state.unmatched_incomming_list, sspid} else Logger.debug( "Script system is not actually alive, it only seems so. Missed an :exit?" ) {{204, ""}, nil, [], nil} end end %{command: :unscripted, from: script_from, state: :unhandled} -> Logger.debug("Replying to :unscripted command") GenServer.reply(script_from, state.unmatched_incomming_list) {:noreply, nil, [], state.sspid} %{command: command, from: script_from, state: :unhandled} -> # And unhandled message from script? Logger.debug("There is a script element") # Transform the command to a plug_element thing, and mark it :sent case gen_request(command.method, command.args, "script", state.cwmp_version) do {:ok, {id, req}} -> {{200, req}, %{command: command, from: script_from, state: :sent, id: id}, state.unmatched_incomming_list, state.sspid} {:error, msg} -> Logger.debug("gen_request error: #{msg}") # must send reply to SS with error, even though this should never happen, # then we must continue to wait in the plug GenServer.reply(script_from, {:error, msg}) {:noreply, nil, state.unmatched_incomming_list, state.sspid} end _ -> Logger.debug( "Cant identify script_element, clearing and discontinuing session: #{inspect(state.script_element)}" ) {{204, ""}, nil, [], nil} end 3 -> Logger.debug( "CWMP message discovered: script_element: #{inspect(state.script_element)}" ) # This could be a response that has to go to script land. In fact if the script_element # is empty this is weird and should be ignored and logged. # Could be that message is an Inform, in which case we just generate an InformResponse and dont stack anything in the plug element. case has_inform?(message.entries) do true -> Logger.debug("Session server saw inform, generating response") id = if !is_nil(message.header) && Map.has_key?(message.header, :id) do message.header.id else 0 end {{200, CWMP.Protocol.Generator.generate!( %CWMP.Protocol.Messages.Header{id: id}, %CWMP.Protocol.Messages.InformResponse{max_envelopes: 1}, message.cwmp_version )}, state.script_element, [], state.sspid} false -> case state.script_element do nil -> Logger.debug("Incomming non-inform message with no script element...") # what? - ignore that......or queue it somewhere in state if someone wants it") # Stuff the message into the junk list - the list of unsolicited messages. # We should still respond... {reply, msg} = construct_reply(message) if state.sspid != nil and Process.alive?(state.sspid) do Logger.debug("Script pid found to be alive") {reply, nil, msg, state.sspid} else Logger.debug("Script pid found to be dead") {reply, nil, msg, nil} end %{command: :unscripted, from: _from, state: :unhandled} -> Logger.debug( "We have a script wanting the unmatched list - we should still reply to this though" ) {reply, msg} = construct_reply(message) {reply, state.script_element, msg, state.sspid} # This only matches script elements that have acutally been sent to the CPE # If we have a script element waiting for the session to enter into a state # where we can send it, we will no come here on any autonomous # CPE message lige TransferComplete... %{command: _command, from: from, state: :sent, id: generated_header_id} -> # Check if the incomming message matches the one generated # by the script system - this can be done by ID comparison # Compare ID of incomming to ID of scripted message if message.header.id == generated_header_id do Logger.debug("Incomming message is meant for script") GenServer.reply(from, message) # We have nothing to reply with here, so we must stuff this in OutstandingPlug and # wait for someting from the Script, either next message or :EXIT # we have to answer :noreply here, and {:noreply, nil, [], state.sspid} else Logger.debug( "Incomming message is unmatched to script - we should reply somehow?" ) # If this is a Response to a CPE request, then we have to end the session at once with # a Fault. # If on the other hand this is an arbitrary request from a CPE, stack it in the unmatched # list and :reply with an appropriate response from here. # Generate a response for every message in the envelope. # TODO: Generate response for every message in entries and wrap # it in one envelope. This can be done by using CMWP.Protocol.Generate.generate(req) # directly, or expanding cwmp_ex to include the capacity to take a list of # entries. {reply, msg} = construct_reply(message) {reply, state.script_element, msg, state.sspid} end # In this case, then the incomming request can not be a response, and must # be stored into the junk list %{command: _command, from: _from, state: :unhandled} -> {reply, msg} = construct_reply(message) if state.sspid != nil and Process.alive?(state.sspid) do Logger.debug("Script pid found to be alive") {reply, state.script_element, msg, state.sspid} else Logger.debug("Script pid found to be dead") {reply, nil, msg, nil} end end end _ -> Logger.debug("Unknown message discovered - ignored") {state.plug_element, state.script_element, state.sspid} end Logger.debug( "process_message returning with #{inspect(plug)}, #{inspect(script)}, #{inspect(unmatched)} #{inspect(sspid)}" ) case plug do :noreply -> {:noreply, %{ state | plug_element: %{message: message, from: from, state: :waiting}, unmatched_incomming_list: state.unmatched_incomming_list ++ unmatched, sspid: sspid }} _ -> {:reply, plug, %{ state | script_element: script, unmatched_incomming_list: state.unmatched_incomming_list ++ unmatched, sspid: sspid }} end end # def handle_call(request,from,state) do # super(request,from,state) # end def has_inform?([]), do: false def has_inform?([%CWMP.Protocol.Messages.Inform{} | _]), do: true def has_inform?([_ | es]), do: has_inform?(es) @doc """ this is spawn_linked and should `apply` the call to the module """ def session_prestart(gspid, script_module, device_id, message, sessionid, fun) do # Set the metadata for the scripting process Logger.metadata(sessionid: sessionid, serial: device_id.serial_number) case fun do nil -> case script_module do nil -> Logger.error("Impossible to start a session with no script module or function") spec_mod -> apply(spec_mod, :session_start, [gspid, device_id, message]) end f when is_function(f) -> apply(fun, [gspid, device_id, message]) _ -> Logger.error("Can not figure out how to call the session_start function") end end # PRIVATE METHODS defp via_tuple(session_id) do {:via, :gproc, {:n, :l, {:session_id, session_id}}} end defp construct_reply(message) do entry = hd(message.entries) id = cond do Map.has_key?(message, :header) && message.header != nil -> message.header.id true -> 0 end case message_type(entry) do {:cpe, _messagetype} -> # ...Response and that type # just respond with {} since responses should # be always "handled" here.. {{204, ""}, [message]} {:acs, CWMP.Protocol.Messages.GetRPCMethods} -> {{200, CWMP.Protocol.Generator.generate!( %CWMP.Protocol.Messages.Header{id: id}, %CWMP.Protocol.Messages.GetRPCMethodsResponse{ methods: [ "GetRPCMethods", "Inform", "TransferComplete", "AutonomousTransferComplete", "Kicked", "RequestDownload", "DUStateChangeComplete", "AutonomousDUStateChangeComplete" ] }, message.cwmp_version )}, [message]} {:acs, CWMP.Protocol.Messages.TransferComplete} -> {{200, CWMP.Protocol.Generator.generate!( %CWMP.Protocol.Messages.Header{id: id}, %CWMP.Protocol.Messages.TransferCompleteResponse{}, message.cwmp_version )}, [message]} {:acs, CWMP.Protocol.Messages.AutonomousTransferComplete} -> {{200, CWMP.Protocol.Generator.generate!( %CWMP.Protocol.Messages.Header{id: id}, %CWMP.Protocol.Messages.AutonomousTransferCompleteResponse{}, message.cwmp_version )}, [message]} {:acs, CWMP.Protocol.Messages.Kicked} -> {{200, CWMP.Protocol.Generator.generate!( %CWMP.Protocol.Messages.Header{id: id}, %CWMP.Protocol.Messages.KickedResponse{next_url: entry.next}, message.cwmp_version )}, [message]} {:acs, CWMP.Protocol.Messages.RequestDownload} -> {{200, CWMP.Protocol.Generator.generate!( %CWMP.Protocol.Messages.Header{id: id}, %CWMP.Protocol.Messages.RequestDownloadResponse{}, message.cwmp_version )}, [message]} {:acs, CWMP.Protocol.Messages.DUStateChangeComplete} -> {{200, CWMP.Protocol.Generator.generate!( %CWMP.Protocol.Messages.Header{id: id}, %CWMP.Protocol.Messages.DUStateChangeCompleteResponse{}, message.cwmp_version )}, [message]} {:acs, CWMP.Protocol.Messages.AutonomousDUStateChangeComplete} -> {{200, CWMP.Protocol.Generator.generate!( %CWMP.Protocol.Messages.Header{id: id}, %CWMP.Protocol.Messages.AutonomousDUStateChangeCompleteResponse{}, message.cwmp_version )}, [message]} _ -> # unknown message type, what to do? - Fault back? {{204, ""}, [message]} end end defp message_type(entry) do case entry do %CWMP.Protocol.Messages.GetRPCMethods{} -> {:acs, entry.__struct__} %CWMP.Protocol.Messages.Inform{} -> {:acs, entry.__struct__} %CWMP.Protocol.Messages.TransferComplete{} -> {:acs, entry.__struct__} %CWMP.Protocol.Messages.AutonomousTransferComplete{} -> {:acs, entry.__struct__} %CWMP.Protocol.Messages.Kicked{} -> {:acs, entry.__struct__} %CWMP.Protocol.Messages.RequestDownload{} -> {:acs, entry.__struct__} %CWMP.Protocol.Messages.DUStateChangeComplete{} -> {:acs, entry.__struct__} %CWMP.Protocol.Messages.AutonomousDUStateChangeComplete{} -> {:acs, entry.__struct__} %CWMP.Protocol.Messages.SetParameterValuesResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.GetParameterValuesResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.GetParameterNamesResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.SetParameterAttributesResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.GetParameterAttributesResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.AddObjectResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.DeleteObjectResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.DownloadResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.RebootResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.GetQueuedTransfersResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.ScheduleInformResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.SetVouchersResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.GetOptionsResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.UploadResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.FactoryResetResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.GetAllQueuedTransfersResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.ScheduleDownloadResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.CancelTransferResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.ChangeDUStateResponse{} -> {:cpe, entry.__struct__} %CWMP.Protocol.Messages.Fault{} -> {:cpe, entry.__struct__} _ -> {:unknown, entry.__struct__} end end # interpret queue data, transform to appropriate CWMP.Protocol.Messages. struct and # ask CWMP.Protocol to generate defp gen_request(method, args, _source, cwmp_version) do Logger.debug("gen_request: #{method}") case validateArgs(method, args) do true -> id = generateID() header = %CWMP.Protocol.Messages.Header{id: id} message = case method do "GetRPCMethods" -> CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.GetRPCMethods{}, cwmp_version ) "SetParameterValues" -> params = for a <- args, do: %CWMP.Protocol.Messages.ParameterValueStruct{ name: a.name, type: a.type, value: a.value } CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.SetParameterValues{parameters: params}, cwmp_version ) "GetParameterValues" -> params = for a <- args, do: %CWMP.Protocol.Messages.GetParameterValuesStruct{name: a, type: "string"} CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.GetParameterValues{parameters: params}, cwmp_version ) "GetParameterNames" -> params = %CWMP.Protocol.Messages.GetParameterNames{ parameter_path: args.parameter_path, next_level: args.next_level } CWMP.Protocol.Generator.generate!(header, params, cwmp_version) "SetParameterAttributes" -> params = for a <- args, do: %CWMP.Protocol.Messages.SetParameterAttributesStruct{ name: a.name, notification_change: a.notification_change, notification: a.notification, accesslist_change: a.accesslist_change, accesslist: a.accesslist } CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.SetParameterAttributes{parameters: params}, cwmp_version ) "GetParameterAttributes" -> CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.GetParameterAttributes{parameters: args}, cwmp_version ) "AddObject" -> CWMP.Protocol.Generator.generate!( header, struct(CWMP.Protocol.Messages.AddObject, args), cwmp_version ) "DeleteObject" -> CWMP.Protocol.Generator.generate!( header, struct(CWMP.Protocol.Messages.DeleteObject, args), cwmp_version ) "Reboot" -> CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.Reboot{}, cwmp_version ) "Download" -> CWMP.Protocol.Generator.generate!( header, struct(CWMP.Protocol.Messages.Download, args), cwmp_version ) "GetQueuedTransfers" -> CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.GetQueuedTransfers{}, cwmp_version ) "ScheduleInform" -> CWMP.Protocol.Generator.generate!( header, struct(CWMP.Protocol.Messages.ScheduleInform, args), cwmp_version ) "SetVouchers" -> voucherlist = for xmlsig <- args, do: struct(CWMP.Protocol.Messages.XMLSignatureStruct, xmlsig) CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.SetVouchers{voucherlist: voucherlist}, cwmp_version ) "GetOptions" -> CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.GetOptions{option_name: args}, cwmp_version ) "Upload" -> CWMP.Protocol.Generator.generate!( header, struct(CWMP.Protocol.Messages.Upload, args), cwmp_version ) "FactoryReset" -> CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.FactoryReset{}, cwmp_version ) "GetAllQueuedTransfers" -> CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.GetAllQueuedTransfers{}, cwmp_version ) "ScheduleDownload" -> CWMP.Protocol.Generator.generate!( header, struct(CWMP.Protocol.Messages.ScheduleDownload, args), cwmp_version ) "CancelTransfer" -> CWMP.Protocol.Generator.generate!( header, %CWMP.Protocol.Messages.CancelTransfer{commandkey: args}, cwmp_version ) "ChangeDUState" -> CWMP.Protocol.Generator.generate!( header, struct(CWMP.Protocol.Messages.ChangeDUState, args), cwmp_version ) _ -> {:error, "Cant match request method: #{method}"} end {:ok, {id, message}} false -> {:error, "arguments for request #{method} do not validate"} end end defp validateArgs(method, args) do case method do "GetRPCMethods" -> # No args for this one true "SetParameterValues" -> # args must be list of maps with name,type and value keys case args do l when is_list(l) and length(l) > 0 -> Enum.all?(args, fn a -> Map.has_key?(a, :name) && Map.has_key?(a, :type) && Map.has_key?(a, :value) end) _ -> false end "GetParameterValues" -> # args must be map with name and type key in all elements case args do l when is_list(l) and length(l) > 0 -> Enum.all?(args, fn a -> String.valid?(a) end) _ -> false end "GetParameterNames" -> # args must be map with path and next_level keys is_map(args) and Map.has_key?(args, :parameter_path) and Map.has_key?(args, :next_level) "SetParameterAttributes" -> # args must be map with path and next_level keys case args do l when is_list(l) and length(l) > 0 -> Enum.all?(args, fn a -> Map.has_key?(a, :name) and Map.has_key?(a, :notification_change) and Map.has_key?(a, :notification) and Map.has_key?(a, :accesslist_change) and Map.has_key?(a, :accesslist) and is_list(a.accesslist) end) _ -> false end "GetParameterAttributes" -> # args must be list of string, at least 1 element in list is_list(args) and length(args) > 0 and String.valid?(hd(args)) "AddObject" -> # args must be map with at least key "object_name" and value must end in . if is_map(args) and Map.has_key?(args, :object_name) do String.last(args.object_name) == "." else false end "DeleteObject" -> # args must be map with at least key "object_name" and value must end in . if is_map(args) and Map.has_key?(args, :object_name) do String.last(args.object_name) == "." else false end "Reboot" -> # takes no params, always true true "Download" -> is_map(args) and Map.has_key?(args, :url) and Map.has_key?(args, :filesize) and Map.has_key?(args, :filetype) "GetQueuedTransfers" -> # takes no params, always true true "ScheduleInform" -> # args is a map with "commandkey" and "delay_seconds" if is_map(args) and Map.has_key?(args, :commandkey) and Map.has_key?(args, :delay_seconds) do if is_integer(args.delay_seconds) do true else Integer.parse(args.delay_seconds) != :error end else false end "SetVouchers" -> # args is a list of maps with keys # signature_value: # key_info # key_value # dsa_p # dsa_q # dsa_g # dsa_y # x509_data # issuer_serial # issuer_name # serial_number # subject_name # certificates [] # options, list of maps with # v_serial_num # deviceid # manufacturer # oui # product_class # serial_number # option_ident # option_desc # start_date (Timex.DateTime) # duration # duration_units # mode # sha1_digest if is_list(args) do Enum.all?(args, fn a -> if Map.has_key?(a, :signature_value) and Map.has_key?(a, :key_info) and Map.has_key?(a, :options) do Logger.debug("step1") if is_list(a.options) and length(a.options) > 0 and Map.has_key?(a.key_info, :key_value) and Map.has_key?(a.key_info, :x509_data) do Logger.debug("step2") if Map.has_key?(a.key_info.key_value, :dsa_p) and Map.has_key?(a.key_info.key_value, :dsa_q) and Map.has_key?(a.key_info.key_value, :dsa_g) and Map.has_key?(a.key_info.key_value, :dsa_y) do Logger.debug("step3") if Map.has_key?(a.key_info.x509_data, :issuer_serial) and Map.has_key?(a.key_info.x509_data, :subject_name) and Map.has_key?(a.key_info.x509_data, :certificates) and is_list(a.key_info.x509_data.certificates) and Map.has_key?(a.key_info.x509_data.issuer_serial, :issuer_name) and Map.has_key?(a.key_info.x509_data.issuer_serial, :serial_number) do Logger.debug("step4") # check all the options matching = Enum.all?(a.options, fn o -> Map.has_key?(o, :v_serial_num) and Map.has_key?(o, :deviceid) and Map.has_key?(o, :option_ident) and Map.has_key?(o, :option_desc) and Map.has_key?(o, :start_date) and Map.has_key?(o, :duration) and Map.has_key?(o, :duration_units) and Map.has_key?(o, :mode) and Map.has_key?(o, :sha1_digest) and Map.has_key?(o.deviceid, :manufacturer) and Map.has_key?(o.deviceid, :oui) and Map.has_key?(o.deviceid, :product_class) and Map.has_key?(o.deviceid, :serial_number) end) matching else false end else false end else false end else false end end) else false end "GetOptions" -> # args is just a string with the option name String.valid?(args) "Upload" -> # args must at least contain commandkey, url and filetype is_map(args) and Map.has_key?(args, :commandkey) and Map.has_key?(args, :url) and Map.has_key?(args, :filetype) "FactoryReset" -> # takes no params, always true true "GetAllQueuedTransfers" -> # takes no params, always true true "ScheduleDownload" -> if is_map(args) and Map.has_key?(args, :url) and Map.has_key?(args, :filesize) and Map.has_key?(args, :filetype) and Map.has_key?(args, :timewindowlist) and is_list(args.timewindowlist) and length(args.timewindowlist) > 0 do # Check that all elements of the timelist list conform Enum.all?(args.timewindowlist, fn tw -> is_map(tw) and Map.has_key?(tw, :window_start) and Map.has_key?(tw, :window_end) and Map.has_key?(tw, :window_mode) and Map.has_key?(tw, :max_retries) end) else false end "CancelTransfer" -> # args is just a string with the option name String.valid?(args) "ChangeDUState" -> if is_map(args) and Map.has_key?(args, :commandkey) and Map.has_key?(args, :operations) and is_list(args.operations) and length(args.operations) > 0 do # Check that all elements of the operations list conform Enum.all?(args.operations, fn o -> if is_map(o) do case o do %CWMP.Protocol.Messages.InstallOpStruct{} -> Map.has_key?(o, :url) and Map.has_key?(o, :uuid) and Map.has_key?(o, :username) and Map.has_key?(o, :password) and Map.has_key?(o, :execution_env_ref) %CWMP.Protocol.Messages.UpdateOpStruct{} -> Map.has_key?(o, :url) and Map.has_key?(o, :uuid) and Map.has_key?(o, :username) and Map.has_key?(o, :password) and Map.has_key?(o, :version) %CWMP.Protocol.Messages.UninstallOpStruct{} -> Map.has_key?(o, :url) and Map.has_key?(o, :uuid) and Map.has_key?(o, :execution_env_ref) end else false end end) else false end _ -> false end end defp generateID do Base.encode16(:erlang.md5(:crypto.strong_rand_bytes(32)), case: :lower) end end