Packages

Elixir client for MikroTik RouterOS binary API with connection pooling, telemetry, and helper functions. Supports RouterOS 6.x and 7.x with both MD5 and plain text authentication over TCP and TLS.

Current section

Files

Jump to
routeros_api lib routeros_api protocol streaming.ex
Raw

lib/routeros_api/protocol/streaming.ex

defmodule RouterosApi.Protocol.Streaming do
@moduledoc """
Protocol extensions for streaming commands.
Provides utilities for:
- Tag generation for command identification
- Partial buffer parsing for async socket handling
- Cancel command generation
"""
import Bitwise
@doc """
Generates a unique tag for command identification.
Tags are used to identify which responses belong to which command,
enabling cancellation of specific streams.
## Examples
iex> tag = RouterosApi.Protocol.Streaming.generate_tag()
iex> String.starts_with?(tag, "s")
true
"""
@spec generate_tag() :: String.t()
def generate_tag do
"s#{:erlang.unique_integer([:positive])}"
end
@doc """
Adds a `.tag` attribute to a command word list.
## Examples
iex> RouterosApi.Protocol.Streaming.add_tag(["/interface/monitor-traffic"], "s123")
["/interface/monitor-traffic", ".tag=s123"]
"""
@spec add_tag([String.t()], String.t()) :: [String.t()]
def add_tag(words, tag) when is_list(words) and is_binary(tag) do
words ++ [".tag=#{tag}"]
end
@doc """
Generates a cancel command for a given tag.
## Examples
iex> RouterosApi.Protocol.Streaming.cancel_command("s123")
["/cancel", "=tag=s123"]
"""
@spec cancel_command(String.t()) :: [String.t()]
def cancel_command(tag) when is_binary(tag) do
["/cancel", "=tag=#{tag}"]
end
@doc """
Attempts to parse complete sentences from a binary buffer.
Returns `{sentences, remaining_buffer}` where:
- `sentences` is a list of parsed sentences (each sentence is a list of words)
- `remaining_buffer` is any incomplete data that needs more bytes
This function handles the RouterOS length-prefixed protocol and can parse
multiple sentences from a single buffer if they are complete.
## Examples
# Complete sentence in buffer
iex> buffer = <<6, "!done", 0>>
iex> {sentences, rest} = RouterosApi.Protocol.Streaming.parse_partial(buffer)
iex> sentences
[[{:done, "!done"}]]
iex> rest
<<>>
# Incomplete sentence - needs more data
iex> buffer = <<6, "!do">>
iex> {sentences, rest} = RouterosApi.Protocol.Streaming.parse_partial(buffer)
iex> sentences
[]
iex> rest
<<6, "!do">>
"""
@spec parse_partial(binary()) :: {[[{atom() | false, String.t()}]], binary()}
def parse_partial(buffer) when is_binary(buffer) do
parse_sentences(buffer, [])
end
# Parse as many complete sentences as possible
defp parse_sentences(buffer, acc) do
case parse_one_sentence(buffer, []) do
{:ok, sentence, rest} ->
parse_sentences(rest, [sentence | acc])
:incomplete ->
{Enum.reverse(acc), buffer}
end
end
# Parse a single sentence (list of words until empty word)
defp parse_one_sentence(buffer, word_acc) do
case decode_word_from_buffer(buffer) do
{:ok, "", rest} ->
# Empty word = end of sentence
{:ok, Enum.reverse(word_acc), rest}
{:ok, word, rest} ->
tagged_word = tag_word(word)
parse_one_sentence(rest, [tagged_word | word_acc])
:incomplete ->
:incomplete
end
end
# Tag status words for identification
defp tag_word("!done"), do: {:done, "!done"}
defp tag_word("!trap"), do: {:trap, "!trap"}
defp tag_word("!fatal"), do: {:fatal, "!fatal"}
defp tag_word("!re"), do: {:re, "!re"}
defp tag_word(word), do: {false, word}
@doc """
Attempts to decode a single word from a binary buffer.
Returns:
- `{:ok, word, remaining}` if a complete word was decoded
- `:incomplete` if more data is needed
## Examples
iex> RouterosApi.Protocol.Streaming.decode_word_from_buffer(<<5, "hello", "rest">>)
{:ok, "hello", "rest"}
iex> RouterosApi.Protocol.Streaming.decode_word_from_buffer(<<5, "hel">>)
:incomplete
"""
@spec decode_word_from_buffer(binary()) :: {:ok, String.t(), binary()} | :incomplete
def decode_word_from_buffer(<<>>) do
:incomplete
end
def decode_word_from_buffer(buffer) do
case decode_length_from_buffer(buffer) do
{:ok, 0, rest} ->
{:ok, "", rest}
{:ok, len, rest} ->
if byte_size(rest) >= len do
<<word::binary-size(len), remaining::binary>> = rest
{:ok, word, remaining}
else
:incomplete
end
:incomplete ->
:incomplete
end
end
@doc """
Attempts to decode a length value from a binary buffer.
Returns:
- `{:ok, length, remaining}` if the length was successfully decoded
- `:incomplete` if more bytes are needed
## Examples
iex> RouterosApi.Protocol.Streaming.decode_length_from_buffer(<<5, "rest">>)
{:ok, 5, "rest"}
iex> RouterosApi.Protocol.Streaming.decode_length_from_buffer(<<0x80, 200, "rest">>)
{:ok, 200, "rest"}
iex> RouterosApi.Protocol.Streaming.decode_length_from_buffer(<<0x80>>)
:incomplete
"""
@spec decode_length_from_buffer(binary()) :: {:ok, non_neg_integer(), binary()} | :incomplete
def decode_length_from_buffer(<<>>) do
:incomplete
end
# 1 byte length (0x00 - 0x7F)
def decode_length_from_buffer(<<byte, rest::binary>>) when byte < 0x80 do
{:ok, byte, rest}
end
# 2 byte length (0x80 - 0xBF)
def decode_length_from_buffer(<<byte, second, rest::binary>>)
when byte >= 0x80 and byte < 0xC0 do
len = (byte &&& 0x3F) <<< 8 ||| second
{:ok, len, rest}
end
def decode_length_from_buffer(<<byte>>) when byte >= 0x80 and byte < 0xC0 do
:incomplete
end
# 3 byte length (0xC0 - 0xDF)
def decode_length_from_buffer(<<byte, second, third, rest::binary>>)
when byte >= 0xC0 and byte < 0xE0 do
len = (byte &&& 0x1F) <<< 16 ||| second <<< 8 ||| third
{:ok, len, rest}
end
def decode_length_from_buffer(<<byte, _::binary>>) when byte >= 0xC0 and byte < 0xE0 do
:incomplete
end
# 4 byte length (0xE0 - 0xEF)
def decode_length_from_buffer(<<byte, second, third, fourth, rest::binary>>)
when byte >= 0xE0 and byte < 0xF0 do
len = (byte &&& 0x0F) <<< 24 ||| second <<< 16 ||| third <<< 8 ||| fourth
{:ok, len, rest}
end
def decode_length_from_buffer(<<byte, _::binary>>) when byte >= 0xE0 and byte < 0xF0 do
:incomplete
end
@doc """
Extracts the status from a parsed sentence.
Returns the status atom (`:done`, `:trap`, `:fatal`, `:re`) or `nil` if no status word found.
"""
@spec sentence_status([{atom() | false, String.t()}]) :: atom() | nil
def sentence_status(tagged_words) do
Enum.find_value(tagged_words, fn
{status, _word} when status in [:done, :trap, :fatal, :re] -> status
_ -> nil
end)
end
@doc """
Extracts the tag value from a parsed sentence if present.
## Examples
iex> RouterosApi.Protocol.Streaming.extract_tag([{false, "=.tag=s123"}, {:re, "!re"}])
"s123"
iex> RouterosApi.Protocol.Streaming.extract_tag([{:done, "!done"}])
nil
"""
@spec extract_tag([{atom() | false, String.t()}]) :: String.t() | nil
def extract_tag(tagged_words) do
Enum.find_value(tagged_words, fn
{false, "=.tag=" <> tag} -> tag
_ -> nil
end)
end
end