215 lines
5.4 KiB
Elixir
215 lines
5.4 KiB
Elixir
defmodule Farmbot.Target.Bootstrap.Configurator.CaptivePortal do
|
|
use GenServer
|
|
require Farmbot.Logger
|
|
|
|
@interface Application.get_env(:farmbot, :captive_portal_interface, "wlan0")
|
|
@address Application.get_env(:farmbot, :captive_portal_address, "192.168.25.1")
|
|
@mdns_domain "farmbot-setup.local"
|
|
|
|
@dnsmasq_conf_file "dnsmasq.conf"
|
|
@dnsmasq_pid_file "dnsmasq.pid"
|
|
|
|
def start_link() do
|
|
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
|
end
|
|
|
|
def init([]) do
|
|
Farmbot.Logger.busy(3, "Starting captive portal.")
|
|
ensure_interface(@interface)
|
|
|
|
host_ap_opts = [
|
|
ssid: build_ssid(),
|
|
key_mgmt: :NONE,
|
|
mode: 2
|
|
]
|
|
|
|
ip_opts = [
|
|
ipv4_address_method: :static,
|
|
ipv4_address: @address,
|
|
ipv4_subnet_mask: "255.255.0.0"
|
|
]
|
|
settings = [networks: [host_ap_opts ++ ip_opts]]
|
|
Nerves.Network.setup(@interface, settings)
|
|
|
|
dhcp_opts = [
|
|
gateway: @address,
|
|
netmask: "255.255.255.0",
|
|
range: {dhcp_range_begin(@address), dhcp_range_end(@address)},
|
|
domain_servers: [@address]
|
|
]
|
|
|
|
{:ok, dhcp_server} = DHCPServer.start_link(@interface, dhcp_opts)
|
|
|
|
dnsmasq =
|
|
case setup_dnsmasq(@address, @interface) do
|
|
{:ok, dnsmasq} ->
|
|
dnsmasq
|
|
|
|
{:error, _} ->
|
|
Farmbot.Logger.error(1, "Failed to start DnsMasq")
|
|
nil
|
|
end
|
|
|
|
Farmbot.Leds.blue(:slow_blink)
|
|
init_mdns(@mdns_domain)
|
|
update_mdns(@address)
|
|
{:ok, %{dhcp_server: dhcp_server, dnsmasq: dnsmasq}}
|
|
end
|
|
|
|
def terminate(_, state) do
|
|
Farmbot.Logger.busy(3, "Stopping captive portal GenServer.")
|
|
|
|
Farmbot.Logger.busy(3, "Stopping mDNS.")
|
|
Mdns.Server.stop()
|
|
|
|
Farmbot.Logger.busy(3, "Stopping DHCP GenServer.")
|
|
GenServer.stop(state.dhcp_server, :normal)
|
|
|
|
stop_dnsmasq(state)
|
|
|
|
Nerves.Network.teardown(@interface)
|
|
end
|
|
|
|
def handle_info({_port, {:data, _data}}, state) do
|
|
{:noreply, state}
|
|
end
|
|
|
|
defp dhcp_range_begin(address) do
|
|
[a, b, c, _] = String.split(address, ".")
|
|
Enum.join([a, b, c, "2"], ".")
|
|
end
|
|
|
|
defp dhcp_range_end(address) do
|
|
[a, b, c, _] = String.split(address, ".")
|
|
Enum.join([a, b, c, "10"], ".")
|
|
end
|
|
|
|
defp ensure_interface(interface) do
|
|
unless interface in Nerves.NetworkInterface.interfaces() do
|
|
Farmbot.Logger.debug(
|
|
2,
|
|
"Waiting for #{interface}: #{inspect(Nerves.NetworkInterface.interfaces())}"
|
|
)
|
|
|
|
Process.sleep(100)
|
|
ensure_interface(interface)
|
|
end
|
|
end
|
|
|
|
defp build_ssid do
|
|
{:ok, hostname} = :inet.gethostname()
|
|
|
|
if String.starts_with?(to_string(hostname), "farmbot-") do
|
|
to_string('farmbot-' ++ Enum.take(hostname, -4))
|
|
else
|
|
to_string(hostname)
|
|
end
|
|
end
|
|
|
|
defp setup_dnsmasq(ip_addr, interface) do
|
|
dnsmasq_conf = build_dnsmasq_conf(ip_addr, interface)
|
|
File.mkdir_p!("/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])
|
|
get_dnsmasq_info(dnsmasq_port, ip_addr, interface)
|
|
end
|
|
|
|
defp get_dnsmasq_info(nil, ip_addr, interface) do
|
|
Farmbot.Logger.warn(1, "dnsmasq failed to start.")
|
|
Process.sleep(1000)
|
|
setup_dnsmasq(ip_addr, interface)
|
|
end
|
|
|
|
defp get_dnsmasq_info(dnsmasq_port, ip_addr, interface) when is_port(dnsmasq_port) do
|
|
case Port.info(dnsmasq_port, :os_pid) do
|
|
{:os_pid, dnsmasq_os_pid} ->
|
|
{dnsmasq_port, dnsmasq_os_pid}
|
|
|
|
nil ->
|
|
Farmbot.Logger.warn(1, "dnsmasq not ready yet.")
|
|
Process.sleep(1000)
|
|
setup_dnsmasq(ip_addr, interface)
|
|
end
|
|
end
|
|
|
|
defp build_dnsmasq_conf(ip_addr, interface) do
|
|
"""
|
|
interface=#{interface}
|
|
address=/#/#{ip_addr}
|
|
server=/farmbot/#{ip_addr}
|
|
local=/farmbot/
|
|
domain=farmbot
|
|
"""
|
|
end
|
|
|
|
defp stop_dnsmasq(state) do
|
|
case state.dnsmasq do
|
|
{dnsmasq_port, dnsmasq_os_pid} ->
|
|
Farmbot.Logger.busy(3, "Stopping dnsmasq")
|
|
Farmbot.Logger.busy(3, "Killing dnsmasq PID.")
|
|
:ok = kill(dnsmasq_os_pid)
|
|
Port.close(dnsmasq_port)
|
|
Farmbot.Logger.success(3, "Stopped dnsmasq.")
|
|
:ok
|
|
|
|
_ ->
|
|
Farmbot.Logger.debug(3, "Dnsmasq not running.")
|
|
:ok
|
|
end
|
|
rescue
|
|
e ->
|
|
Farmbot.Logger.error(3, "Error stopping dnsmasq: #{Exception.message(e)}")
|
|
:ok
|
|
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
|
|
Farmbot.Logger.error(2, "Encountered an error (#{num})")
|
|
:error
|
|
end
|
|
|
|
defp init_mdns(mdns_domain) do
|
|
Mdns.Server.add_service(%Mdns.Server.Service{
|
|
domain: mdns_domain,
|
|
data: :ip,
|
|
ttl: 120,
|
|
type: :a
|
|
})
|
|
end
|
|
|
|
defp update_mdns(ip) do
|
|
ip_tuple = to_ip_tuple(ip)
|
|
Mdns.Server.stop()
|
|
|
|
# Give the interface time to settle to fix an issue where mDNS's multicast
|
|
# membership is not registered. This occurs on wireless interfaces and
|
|
# needs to be revisited.
|
|
:timer.sleep(100)
|
|
|
|
Mdns.Server.start(interface: ip_tuple)
|
|
Mdns.Server.set_ip(ip_tuple)
|
|
end
|
|
|
|
defp to_ip_tuple(str) do
|
|
str
|
|
|> String.split(".")
|
|
|> Enum.map(&String.to_integer/1)
|
|
|> List.to_tuple()
|
|
end
|
|
end
|