farmbot_os/platform/target/bootstrap/configurator/captive_portal/hostapd.ex

219 lines
5.8 KiB
Elixir

defmodule Hostapd do
@moduledoc """
Manages an OS process of hostapd.
"""
defmodule State do
@moduledoc false
defstruct [:hostapd, :dnsmasq, :interface, :ip_addr]
end
use GenServer
use Farmbot.Logger
@hostapd_conf_file "hostapd.conf"
@hostapd_pid_file "hostapd.pid"
@dnsmasq_conf_file "dnsmasq.conf"
@dnsmasq_pid_file "dnsmasq.pid"
defp ensure_interface(interface) do
unless interface in Nerves.NetworkInterface.interfaces() do
Logger.debug 2, "Waiting for #{interface}: #{inspect Nerves.NetworkInterface.interfaces()}"
Process.sleep(100)
ensure_interface(interface)
end
end
@doc false
def start_link(opts, gen_server_opts \\ []) do
GenServer.start_link(__MODULE__, opts, gen_server_opts)
end
def init(opts) do
# We want to know if something does.
Process.flag(:trap_exit, true)
interface = Keyword.fetch!(opts, :interface)
address = Keyword.fetch!(opts, :address)
Logger.busy(3, "Starting hostapd on #{interface}")
ensure_interface(interface)
dnsmasq_path = System.find_executable("dnsmasq")
dnsmasq_settings = if dnsmasq_path do
setup_dnsmasq(address, interface)
else
nil
end
{hostapd_port, hostapd_os_pid} = setup_hostapd(interface, address)
state = %State{
hostapd: {hostapd_port, hostapd_os_pid},
dnsmasq: dnsmasq_settings,
interface: interface,
ip_addr: address
}
{:ok, state}
end
defp setup_dnsmasq(ip_addr, interface) do
dnsmasq_conf = build_dnsmasq_conf(ip_addr, interface)
File.mkdir!("/tmp/dnsmasq")
:ok = File.write("/tmp/dnsmasq/#{@dnsmasq_conf_file}", dnsmasq_conf)
dnsmasq_cmd = "dnsmasq -k --dhcp-lease " <>
"/tmp/dnsmasq/#{@dnsmasq_pid_file} " <>
"--conf-dir=/tmp/dnsmasq"
dnsmasq_port = Port.open({:spawn, dnsmasq_cmd}, [:binary])
dnsmasq_os_pid = dnsmasq_port|> Port.info() |> Keyword.get(:os_pid)
{dnsmasq_port, dnsmasq_os_pid}
end
defp build_dnsmasq_conf(ip_addr, interface) do
"""
interface=#{interface}
address=/#/#{ip_addr}
server=/farmbot/#{ip_addr}
local=/farmbot/
domain=farmbot
"""
end
defp setup_hostapd(interface, ip_addr) do
# Make sure the interface is in proper condition.
:ok = hostapd_ip_settings_up(interface, ip_addr)
# build the hostapd configuration
hostapd_conf = build_hostapd_conf(interface, build_ssid())
# build a config file
File.mkdir!("/tmp/hostapd")
File.write!("/tmp/hostapd/#{@hostapd_conf_file}", hostapd_conf)
hostapd_cmd =
"hostapd -P /tmp/hostapd/#{@hostapd_pid_file} "
<> "/tmp/hostapd/#{@hostapd_conf_file}"
hostapd_port = Port.open({:spawn, hostapd_cmd}, [:binary])
hostapd_os_pid = hostapd_port |> Port.info() |> Keyword.get(:os_pid)
{hostapd_port, hostapd_os_pid}
end
defp hostapd_ip_settings_up(interface, ip_addr) do
:ok = cmd("ip link set #{interface} up")
:ok = cmd("ip addr add #{ip_addr}/24 dev #{interface}")
:ok
end
defp hostapd_ip_settings_down(interface, ip_addr) do
:ok = cmd("ip link set #{interface} down")
:ok = cmd("ip addr del #{ip_addr}/24 dev #{interface}")
:ok = cmd("ip link set #{interface} up")
:ok
end
defp build_hostapd_conf(interface, ssid) do
"""
interface=#{interface}
ssid=#{ssid}
hw_mode=g
channel=6
auth_algs=1
wmm_enabled=0
"""
end
defp build_ssid do
node_str = node() |> Atom.to_string()
case node_str |> String.split("@") do
[name, "farmbot-" <> id] -> name <> "-" <> id
_ -> "Farmbot"
end
end
def handle_info({port, {:data, data}}, state) do
{hostapd_port, _} = state.hostapd
cond do
port == hostapd_port -> handle_hostapd(data, state)
match?({^port, _}, state.dnsmasq) -> handle_dnsmasq(data, state)
true -> {:noreply, state}
end
end
def handle_info(_, state), do: {:noreply, state}
defp handle_hostapd(data, state) when is_bitstring(data) do
Logger.debug(3, String.trim(data))
{:noreply, state}
end
defp handle_dnsmasq(data, state) when is_bitstring(data) do
Logger.debug(3, String.trim(data))
{:noreply, state}
end
defp stop_hostapd(state) do
case state.hostapd do
{hostapd_port, hostapd_pid} ->
Logger.busy 3, "Stopping hostapd"
Logger.busy 3, "Killing hostapd PID."
:ok = kill(hostapd_pid)
Port.close(hostapd_port)
Logger.busy 3, "Resetting ip settings."
hostapd_ip_settings_down(state.interface, state.ip_addr)
Logger.busy 3, "removing PID."
File.rm_rf!("/tmp/hostapd")
Logger.success 3, "Stopped hostapd."
:ok
_ ->
Logger.debug 3, "Hostapd not running."
:ok
end
rescue
e ->
Logger.error 3, "Error stopping hostapd: #{Exception.message(e)}"
:ok
end
defp stop_dnsmasq(state) do
case state.dnsmasq do
{dnsmasq_port, dnsmasq_os_pid} ->
Logger.busy 3, "Stopping dnsmasq"
Logger.busy 3, "Killing dnsmasq PID."
:ok = kill(dnsmasq_os_pid)
Port.close(dnsmasq_port)
Logger.success 3, "Stopped dnsmasq."
:ok
_ ->
Logger.debug 3, "Dnsmasq not running."
:ok
end
rescue
e ->
Logger.error 3, "Error stopping dnsmasq: #{Exception.message(e)}"
:ok
end
def terminate(_, state) do
stop_hostapd(state)
stop_dnsmasq(state)
Nerves.NetworkInterface.ifdown(state.interface)
Nerves.NetworkInterface.ifup(state.interface)
end
defp kill(os_pid), do: :ok = cmd("kill -9 #{os_pid}")
defp cmd(cmd_str) do
[command | args] = String.split(cmd_str, " ")
System.cmd(command, args, into: IO.stream(:stdio, :line))
|> print_cmd()
end
defp print_cmd({_, 0}), do: :ok
defp print_cmd({_, num}) do
Logger.error(2, "Encountered an error (#{num})")
:error
end
end