wrong dir again

pull/221/head
connor rigby 2016-12-28 11:23:20 -08:00
parent 63ded4001a
commit 695df41994
35 changed files with 1492 additions and 126 deletions

9
LICENSE 100644
View File

@ -0,0 +1,9 @@
The MIT License
Copyright (c) 2016 Farmbot Project
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,220 @@
defmodule Farmbot.Auth do
@moduledoc """
Gets a token and device information
"""
@modules Application.get_env(:farmbot_auth, :callbacks) ++ [Farmbot.Auth]
@path Application.get_env(:farmbot_filesystem, :path)
use GenServer
require Logger
alias Farmbot.FileSystem.ConfigStorage, as: CS
alias Farmbot.Token
@doc """
Gets the public key from the API
"""
def get_public_key(server) do
case HTTPotion.get("#{server}/api/public_key") do
%HTTPotion.ErrorResponse{message: message} ->
{:error, message}
%HTTPotion.Response{body: body, headers: _headers, status_code: 200} ->
{:ok, RSA.decode_key(body)}
end
end
@doc """
Returns the list of callback modules.
"""
def modules, do: @modules
@doc """
Encrypts the key with the email, pass, and server
"""
def encrypt(email, pass, pub_key) do
f = Poison.encode!(%{"email": email,
"password": pass,
"id": Nerves.Lib.UUID.generate,
"version": 1})
|> RSA.encrypt({:public, pub_key})
|> String.Chars.to_string
{:ok, f}
end
@doc """
Get a token from the server with given token
"""
@spec get_token_from_server(binary, String.t) :: {:ok, Token.t} | {:error, atom}
def get_token_from_server(secret, server) do
# I am not sure why this is done this way other than it works.
payload = Poison.encode!(%{user: %{credentials: :base64.encode_to_string(secret) |> String.Chars.to_string }} )
case HTTPotion.post "#{server}/api/tokens", [body: payload, headers: ["Content-Type": "application/json"]] do
# Any other http error.
%HTTPotion.ErrorResponse{message: reason} -> {:error, reason}
# bad Password
%HTTPotion.Response{body: _, headers: _, status_code: 422} -> {:error, :bad_password}
# Token invalid. Need to try to get a new token here.
%HTTPotion.Response{body: _, headers: _, status_code: 401} -> {:error, :expired_token}
# We won
%HTTPotion.Response{body: body, headers: _headers, status_code: 200} ->
# save the secret to disk.
Farmbot.FileSystem.transaction fn() ->
:ok = File.write(@path <> "/secret", :erlang.term_to_binary(secret))
end
Poison.decode!(body) |> Map.get("token") |> Token.create
end
end
@doc """
Gets the token.
Will return a token if one exists, nil if not.
Returns {:error, reason} otherwise
"""
def get_token do
GenServer.call(__MODULE__, :get_token)
end
@doc """
Gets teh server
will return either {:ok, server} or {:ok, nil}
"""
@spec get_server :: {:ok, nil} | {:ok, String.t}
def get_server, do: GenServer.call(CS, {:get, Authorization, :server})
@spec put_server(String.t | nil) :: no_return
defp put_server(server) when is_nil(server) or is_binary(server),
do: GenServer.cast(CS, {:put, Authorization, {:server, server}})
@doc """
Tries to log into web services with whatever auth method is stored in state.
"""
@spec try_log_in :: {:ok, Token.t} | {:error, atom}
def try_log_in do
case GenServer.call(__MODULE__, :try_log_in) do
{:ok, %Token{} = token} ->
do_callbacks(token)
error ->
Logger.error ">> Could not log in! #{inspect error}"
end
end
@doc """
Casts credentials to the Auth GenServer
"""
@spec interim(String.t, String.t, String.t) :: no_return
def interim(email, pass, server) do
GenServer.cast(__MODULE__, {:interim, {email,pass,server}})
end
@doc """
Reads the secret file from disk
"""
@spec get_secret :: {:ok, nil | binary}
def get_secret do
case File.read(@path <> "/secret") do
{:ok, sec} -> {:ok, :erlang.binary_to_term(sec)}
_ -> {:ok, nil}
end
end
@doc """
Application entry point
"""
def start(_type, args) do
Logger.debug(">> Starting Authorization services")
start_link(args)
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__ )
end
# Genserver stuff
def init(_args), do: get_secret
# casted creds, store them until something is ready to actually try a log in.
def handle_cast({:interim, {email, pass, server}},_) do
Logger.debug ">> Got some new credentials."
put_server(server)
{:noreply, {email,pass,server}}
end
def handle_call(:try_log_in, _, {email, pass, server}) do
Logger.debug ">> is trying to log in with credentials."
with {:ok, pub_key} <- get_public_key(server),
{:ok, secret } <- encrypt(email, pass, pub_key),
{:ok, %Token{} = token} <- get_token_from_server(secret, server)
do
{:reply, {:ok, token}, token}
else
e ->
Logger.error ">> error getting token #{inspect e}"
put_server(nil)
{:reply, e, nil}
end
end
def handle_call(:try_log_in, _, secret) when is_binary(secret) do
Logger.debug ">> is trying to log in with a secret."
with {:ok, server} <- get_server,
{:ok, %Token{} = token} <- get_token_from_server(secret, server)
do
{:reply, {:ok, token}, token}
else
e ->
Logger.error ">> error getting token #{inspect e}"
put_server(nil)
{:reply, e, nil}
end
end
def handle_call(:try_log_in, _, %Token{} = _token) do
Logger.warn ">> already has a token. Fetching another."
with {:ok, server} <- get_server,
{:ok, secret} <- get_secret,
{:ok, %Token{} = token} <- get_token_from_server(secret, server)
do
{:reply, {:ok, token}, token}
else
e ->
Logger.error ">> error getting token #{inspect e}"
put_server(nil)
{:reply, e, nil}
end
end
def handle_call(:try_log_in, _, nil) do
Logger.error ">> can't log in because i have no token or credentials!"
{:reply, {:error, :no_token}, nil}
end
# if we do have a token.
def handle_call(:get_token, _from, %Token{} = token) do
{:reply, {:ok, token}, token}
end
# if we dont.
def handle_call(:get_token, _, not_token) do
{:reply, nil, not_token}
end
# when we get a token.
def handle_info({:authorization, token}, _) do
{:noreply, token}
end
def terminate(:normal, state) do
Logger.debug("AUTH DIED: #{inspect state}")
end
def terminate(reason, state) do
Logger.error("AUTH DIED: #{inspect {reason, state}}")
end
defp do_callbacks(token) do
spawn(fn ->
Enum.all?(@modules, fn(module) ->
send(module, {:authorization, token})
end)
end)
end
end

View File

@ -1,116 +0,0 @@
defmodule Farmbot.Auth do
@moduledoc """
Gets a token and device information
"""
@modules Application.get_env(:farmbot_auth, :callbacks) ++ [Farmbot.Auth]
use Timex
use GenServer
require Logger
@doc """
Application entry point
"""
def start(_type, args) do
Logger.debug("Farmbot.Auth Starting.")
start_link(args)
end
@doc """
Gets the public key from the API
"""
def get_public_key(server) do
case HTTPotion.get("#{server}/api/public_key") do
%HTTPotion.ErrorResponse{message: message} -> {:error, message}
%HTTPotion.Response{body: body,
headers: _headers,
status_code: 200} -> {:ok, RSA.decode_key(body)}
end
end
@doc """
Returns the list of callback modules.
"""
def modules, do: @modules
@doc """
Encrypts the key with the email, pass, and server
"""
def encrypt(email, pass, pub_key) do
f = Poison.encode!(%{"email": email,
"password": pass,
"id": Nerves.Lib.UUID.generate,
"version": 1})
|> RSA.encrypt({:public, pub_key})
|> String.Chars.to_string
{:ok, f}
end
@doc """
Get a token from the server with given token
"""
def get_token_from_server(secret, server) do
# I am not sure why this is done this way other than it works.
payload = Poison.encode!(%{user: %{credentials: :base64.encode_to_string(secret) |> String.Chars.to_string }} )
case HTTPotion.post "#{server}/api/tokens", [body: payload, headers: ["Content-Type": "application/json"]] do
# Any other http error.
%HTTPotion.ErrorResponse{message: reason} -> {:error, reason}
# bad Password
%HTTPotion.Response{body: _, headers: _, status_code: 422} -> {:error, :bad_password}
# Token invalid. Need to try to get a new token here.
%HTTPotion.Response{body: _, headers: _, status_code: 401} -> {:error, :expired_token}
# We won
%HTTPotion.Response{body: body, headers: _headers, status_code: 200} ->
token = Poison.decode!(body) |> Map.get("token")
do_callbacks(token)
{:ok, token}
end
end
@doc """
Gets the token.
Will return a token if one exists, nil if not.
Returns {:error, reason} otherwise
"""
def get_token do
GenServer.call(__MODULE__, {:get_token})
end
# Genserver stuff
def init(_args) do
{:ok, nil}
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__ )
end
def handle_call({:get_token}, _from, nil) do
{:reply, nil, nil}
end
def handle_call({:get_token}, _from, token) do
{:reply, {:ok, token}, token}
end
def handle_info({:authorization, token}, _) do
{:noreply, token}
end
def terminate(:normal, state) do
Logger.debug("AUTH DIED: #{inspect {state}}")
end
def terminate(reason, state) do
Logger.error("AUTH DIED: #{inspect {reason, state}}")
end
defp do_callbacks(token) do
spawn(fn ->
Enum.all?(@modules, fn(module) ->
send(module, {:authorization, token})
end)
end)
end
end

View File

@ -0,0 +1,54 @@
defmodule Farmbot.HTTP do
@moduledoc """
Shortcuts to HTTPOtion because im Lazy.
"""
alias Farmbot.Auth
alias Farmbot.Token
@type http_resp :: HTTPotion.Response.t | HTTPotion.ErrorResponse.t
@doc """
POST request to the Farmbot Web Api
"""
@spec post(binary, binary) :: {:error, term} | http_resp
def post(path, body) do
with {:ok, server} <- fetch_server,
{:ok, auth_headers} <- build_auth,
do: HTTPotion.post("#{server}#{path}",
headers: auth_headers, body: body)
end
@doc """
GET request to the Farmbot Web Api
"""
@spec get(binary) :: {:error, term} | http_resp
def get(path) do
with {:ok, server} <- fetch_server,
{:ok, auth_headers} <- build_auth,
do: HTTPotion.get("#{server}#{path}", headers: auth_headers)
end
@doc """
Short cut for getting a path and piping it thro Poison.decode.
"""
@spec get_to_json(binary) :: map
def get_to_json(path), do: path |> get |> Map.get(:body) |> Poison.decode!
@type headers :: ["Content-Type": String.t, "Authorization": String.t]
@spec build_auth :: {:ok, headers} | {:error, term}
defp build_auth do
with {:ok, json_token} <- Auth.get_token,
{:ok, token} <- Token.create(json_token),
do:
{:ok,
["Content-Type": "application/json",
"Authorization": "Bearer " <> token.encoded]}
end
defp fetch_server do
case Auth.get_server do
nil -> {:error, :no_server}
server -> {:ok, server}
end
end
end

View File

@ -0,0 +1,94 @@
defmodule Farmbot.Token do
defmodule Unencoded do
@moduledoc """
The unencoded version of the token.
"""
@enforce_keys [:bot,
:exp,
:fw_update_server,
:os_update_server,
:iat,
:iss,
:jti,
:mqtt,
:sub]
@type t :: %__MODULE__{
# Github apis for bot and fw updates.
fw_update_server: String.t,
os_update_server: String.t,
# Dates for when the token expires and was issued at.
exp: number,
iat: number,
# Bot name for logging into MQTT.
bot: String.t,
# Issuer (api server)
iss: String.t,
# mqtt broker
mqtt: String.t,
# uuid
jti: String.t,
# Email
sub: String.t
}
defstruct [:bot,
:exp,
:fw_update_server,
:os_update_server,
:iat,
:iss,
:jti,
:mqtt,
:sub]
end
@moduledoc """
Token Object
"""
@enforce_keys [:encoded, :unencoded]
defstruct [:encoded, :unencoded]
@type t :: %__MODULE__{
encoded: binary,
unencoded: Unencoded.t
}
@doc """
Creates a valid token from json.
"""
@spec create(map | {:ok, map}) :: t | :not_valid
def create({:ok, token}), do: create(token)
def create(%{"encoded" => encoded,
"unencoded" =>
%{"bot" => bot,
"exp" => exp,
"fw_update_server" => fw_update_server,
"os_update_server" => os_update_server,
"iat" => iat,
"iss" => iss,
"jti" => jti,
"mqtt" => mqtt,
"sub" => sub}})
do
f =
%__MODULE__{encoded: encoded,
unencoded: %Unencoded{
bot: bot,
exp: exp,
iat: iat,
iss: iss,
jti: jti,
mqtt: mqtt,
sub: sub,
fw_update_server: fw_update_server,
os_update_server: os_update_server
}}
{:ok, f}
end
def create(_), do: :not_valid
def create!(thing) do
case create(thing) do
{:ok, win} -> win
fail -> raise "failed to create token: #{inspect fail}"
end
end
end

View File

@ -16,12 +16,11 @@ defmodule Farmbot.Auth.Mixfile do
def application do
[mod: {Farmbot.Auth, []},
applications: [:logger, :timex, :httpotion, :rsa, :nerves_lib, :poison]]
applications: [:logger, :httpotion, :rsa, :nerves_lib, :poison]]
end
defp deps do
[{:timex, "~> 3.0"},
{:httpotion, "~> 3.0.0"},
[{:httpotion, "~> 3.0.0"},
{:rsa, "~> 0.0.1"},
{:nerves_lib, github: "nerves-project/nerves_lib"},
{:poison, "~> 3.0"}]

View File

@ -1,5 +1 @@
use Mix.Config
# the path that will be used for file opperations.
# config :farmbot_filesystem,
# path: "/tmp",
# config_file_name: "default_config.json"

View File

@ -24,5 +24,5 @@ defmodule Farmbot.Filesystem.Mixfile do
defp deps, do: []
defp target(:prod), do: System.get_env("NERVES_TARGET") || "rpi3"
defp target(_), do: System.get_env("NERVES_TARGET") || "development"
defp target(_), do: "development"
end

View File

@ -11,5 +11,6 @@
},
"hardware":{
"params":{}
}
},
"ntp": false
}

View File

@ -13,5 +13,6 @@
},
"hardware":{
"params":{}
}
},
"ntp": true
}

17
apps/farmbot_network/.gitignore vendored 100644
View File

@ -0,0 +1,17 @@
# The directory Mix will write compiled artifacts to.
/_build
# If you run "mix test --cover", coverage assets end up here.
/cover
# The directory Mix downloads your dependencies sources to.
/deps
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez

View File

@ -0,0 +1,24 @@
# FarmbotNetwork
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
1. Add `farmbot_network` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:farmbot_network, "~> 0.1.0"}]
end
```
2. Ensure `farmbot_network` is started before your application:
```elixir
def application do
[applications: [:farmbot_network]]
end
```

View File

@ -0,0 +1 @@
use Mix.Config

View File

@ -0,0 +1,9 @@
defmodule Farmbot.Network.Handler do
@moduledoc """
A behaviour for managing networking.
"""
@type ret_val :: :ok | {:error, atom}
@callback init({pid, map}) :: {:ok, any}
@callback manager :: {:ok, pid}
end

View File

@ -0,0 +1,10 @@
defmodule Module.concat([Farmbot, Network, Handler, "development"]) do
@behaviour Farmbot.Network.Handler
require Logger
def manager, do: GenEvent.start_link
def init({parent, _config}) do
Logger.debug ">> development network handler init."
GenServer.cast parent, {:connected, "lo", "127.0.0.1"}
{:ok, parent}
end
end

View File

@ -0,0 +1,3 @@
defmodule Module.concat([Farmbot, Network, Handler, "qemu"]) do
end

View File

@ -0,0 +1,54 @@
defmodule Module.concat([Farmbot, Network, Handler, "rpi3"]) do
@moduledoc """
Event manager for network on Raspberry Pi 3
"""
defmodule State do
@moduledoc false
@type t :: %__MODULE__{parent: pid | nil}
defstruct [:parent]
end
@behaviour Farmbot.Network.Handler
require Logger
alias Nerves.NetworkInterface
@doc false
def manager, do: {:ok, NetworkInterface.event_manager}
@doc false
def init({parent, _config}) do
Process.flag :trap_exit, true
Logger.debug ">> rpi3 networking handler starting."
{:ok, %State{parent: parent}}
end
# {:udhcpc, pid, :bound,
# %{domain: "T-mobile.com",
# ifname: "eth0",
# ipv4_address: "192.168.29.186",
# ipv4_broadcast: "192.168.29.255",
# ipv4_gateway: "192.168.29.1",
# ipv4_subnet_mask: "255.255.255.0",
# nameservers: ["192.168.29.1"]}}
# event when we have an ip address.
def handle_event({:udhcpc, _, :bound,
%{ipv4_address: address, ifname: interface}}, state)
do
GenServer.cast(state.parent, {:connected, interface, address})
{:ok, state}
end
def handle_event({:hostapd, data}, state) do
Logger.debug ">> got some hostapd data: #{data}"
{:ok, state}
end
# def handle_event(event, state) do
# Logger.warn "got event: #{inspect event}"
# {:ok, state}
# end
def handle_event(_, state), do: {:ok, state}
def terminate(_, _state), do: :ok
end

View File

@ -0,0 +1,185 @@
defmodule Farmbot.Network.Hostapd do
@moduledoc """
Manages an OS process of hostapd and DNSMASQ.
"""
defmodule State do
@moduledoc false
defstruct [:hostapd, :dnsmasq, :interface, :ip_addr, :manager]
end
use GenServer
require Logger
@hostapd_conf_file "hostapd.conf"
@hostapd_pid_file "hostapd.pid"
@dnsmasq_conf_file "dnsmasq.conf"
@dnsmasq_pid_file "dnsmasq.pid"
@doc """
Example:
Iex> Hostapd.start_link ip_address: "192.168.24.1",
...> manager: Farmbot.Network.Manager, interface: "wlan0"
"""
def start_link(
[interface: interface, ip_address: ip_addr, manager: manager])
do
name = Module.concat([__MODULE__, interface])
GenServer.start_link(__MODULE__,
[interface: interface, ip_address: ip_addr, manager: manager],
name: name)
end
# Don't lint this. Its not too complex credo.
# No but really TODO: make this a little less complex.
@lint false
@doc false
def init([interface: interface, ip_address: ip_addr, manager: manager]) do
# We want to know if something does.
Process.flag :trap_exit, true
# ip_addr = @ip_addr
# HOSTAPD
# 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 = Port.info(hostapd_port) |> Keyword.get(:os_pid)
# DNSMASQ
dnsmasq_conf = build_dnsmasq_conf(ip_addr)
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 = Port.info(dnsmasq_port) |> Keyword.get(:os_pid)
state = %State{hostapd: {hostapd_port, hostapd_os_pid},
dnsmasq: {dnsmasq_port, dnsmasq_os_pid},
interface: interface,
ip_addr: ip_addr,
manager: manager}
{:ok,state}
end
@lint false # don't lint this because piping System.cmd looks weird to me.
defp hostapd_ip_settings_up(interface, ip_addr) do
:ok =
System.cmd("ip", ["link", "set", "#{interface}", "up"])
|> print_cmd
:ok =
System.cmd("ip", ["addr", "add", "#{ip_addr}/24", "dev", "#{interface}"])
|> print_cmd
:ok
end
@lint false # don't lint this because piping System.cmd looks weird to me.
defp hostapd_ip_settings_down(interface, ip_addr) do
:ok =
System.cmd("ip", ["link", "set", "#{interface}", "down"])
|> print_cmd
:ok =
System.cmd("ip", ["addr", "del", "#{ip_addr}/24", "dev", "#{interface}"])
|> print_cmd
:ok =
System.cmd("ip", ["link", "set", "#{interface}", "up"])
|> print_cmd
: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
[name, "nerves-" <> id] =
node_str |> String.split("@")
name <> "-" <> id
end
defp build_dnsmasq_conf(ip_addr) do
[a, b, c, _] = ip_addr |> String.split(".")
first_part = "#{a}.#{b}.#{c}."
"""
# bogus-priv
# server=/localnet/#{ip_addr}
# local=/localnet/
interface=wlan0
# domain=localnet
dhcp-range=#{first_part}50,#{first_part}250,2h
dhcp-option=3,#{ip_addr}
dhcp-option=6,#{ip_addr}
dhcp-authoritative
# address=/#/#{ip_addr}
"""
end
@lint false # don't lint this because piping System.cmd looks weird to me.
defp kill(os_pid),
do: :ok = System.cmd("kill", ["15", "#{os_pid}"]) |> print_cmd
defp print_cmd({_, 0}), do: :ok
defp print_cmd({res, num}) do
Logger.error ">> encountered an error (#{num}): #{res}"
:error
end
def handle_info({port, {:data, data}}, state) do
{hostapd_port,_} = state.hostapd
{dnsmasq_port,_} = state.dnsmasq
cond do
port == hostapd_port ->
handle_hostapd(data, state)
port == dnsmasq_port ->
handle_dnsmasq(data, state)
true -> {:noreply, state}
end
end
def handle_info(_thing, state), do: {:noreply, state}
defp handle_hostapd(data, state) when is_bitstring(data) do
GenEvent.notify(state.manager, {:hostapd, String.trim(data)})
{:noreply, state}
end
defp handle_hostapd(_,state), do: {:noreply, state}
defp handle_dnsmasq(data, state) when is_bitstring(data) do
GenEvent.notify(state.manager, {:dnsmasq, String.trim(data)})
{:noreply, state}
end
defp handle_dnsmasq(_,state), do: {:noreply, state}
def terminate(_,state) do
Logger.debug ">> is stopping hostapd"
{_hostapd_port, hostapd_pid} = state.hostapd
{_dnsmasq_port, dnsmasq_pid} = state.dnsmasq
# Port.close hostapd_port
# Port.close dnsmasq_port
:ok = kill(hostapd_pid)
:ok = kill(dnsmasq_pid)
hostapd_ip_settings_down(state.interface, state.ip_addr)
File.rm_rf! "/tmp/hostapd"
File.rm_rf! "/tmp/dnsmasq"
end
end

View File

@ -0,0 +1,271 @@
defmodule Farmbot.Network do
@moduledoc """
Manages messages from a network event manager.
"""
require Logger
alias Farmbot.FileSystem.ConfigStorage, as: FBConfigStorage
alias Farmbot.Configurator.EventManager, as: EM
alias Farmbot.Network.ConfigSocket, as: SocketHandler
alias Nerves.InterimWiFi
alias Farmbot.Network.Hostapd
alias Farmbot.Network.Ntp
defmodule Interface do
@moduledoc """
A network interface struct.
"""
defstruct [:ipv4_address, :pid]
end
defmodule State do
@moduledoc false
@enforce_keys [:manager, :hardware]
defstruct [connected?: false, interfaces: %{}, manager: nil, hardware: nil]
@type t :: %__MODULE__{connected?: boolean, interfaces: %{}, manager: pid}
end
def start(_, [args]), do: start_link(args.hardware)
@doc """
Starts Network Manager on hardware ("rpi3", "qemu_arm", "development", etc)
"""
def start_link(hardware) do
GenServer.start_link(__MODULE__, hardware, name: __MODULE__)
end
def init(hardware) do
Logger.debug ">> is initializing networking on: #{inspect hardware}"
Process.flag :trap_exit, true
# i guess this can be here.
GenEvent.add_handler(EM, SocketHandler, [])
# Logger.debug ">> is starting epmd."
# System.cmd("epmd", ["-daemon"])
{:ok, config} = get_config
# The module of the handler.
handler = Module.concat([Farmbot,Network,Handler,hardware])
# start (or forward) an event manager
{:ok, emanager} = handler.manager
# add the handler. (probably change this to a mon handler)
GenEvent.add_handler(emanager, handler, {self(), config})
{:ok, %State{connected?: false,
manager: emanager,
hardware: hardware,
interfaces: parse_config(config, hardware)}}
end
def handle_cast({:connected, interface, ip}, state) do
Logger.debug ">>'s #{interface} is connected: #{ip}"
# I don't want either of these here.
if get_config("ntp") == true do
# Only set time if required to do so.
Ntp.set_time
end
Farmbot.Auth.try_log_in
case Map.get(state.interfaces, interface) do
%Interface{} = thing ->
new_interface = %Interface{thing | ipv4_address: ip}
new_state =
%State{state | connected?: true,
interfaces: Map.put(state.interfaces, interface, new_interface)}
{:noreply, new_state}
t ->
Logger.warn(
">> encountered something weird updating #{interface} "
<> "state: #{inspect t}")
{:noreply, %State{state | connected?: true}}
end
end
def handle_info({:EXIT, pid, reason}, state) do
Logger.debug "something in network died: #{inspect pid}, #{inspect reason}"
{:noreply, state}
end
def handle_info(_, state), do: {:noreply, state}
def handle_call(:all_down, _, state) do
Enum.each((state |> Map.from_struct)[:interfaces], fn({interface, config}) ->
Logger.debug ">> is stoping #{interface}"
if is_pid(config.pid) do
Process.exit(config.pid, :down)
end
end)
Logger.debug ">> has stopped all network interfaces."
{:reply, :ok, %State{state | connected?: false, interfaces: %{}}}
end
def handle_call({:up, _iface, [host: true]}, _, state) do
{:reply, :todo, state}
end
def handle_call({:up, iface, settings}, _, state) do
{:ok, pid} = InterimWiFi.setup(iface, settings)
new_interface = %Interface{pid: pid}
{:reply, pid, %State{state |
interfaces: Map.put(state.interfaces, iface, new_interface)}}
end
def handle_call({:down, iface}, _, state) do
iface = state.interfaces[iface]
if iface do
Process.exit(iface.pid, :down)
{:reply, :ok, %State{state |
interfaces: Map.delete(state.interfaces, iface)}}
else
Logger.debug ">> could not bring down #{iface}."
{:reply, :no_iface, state}
end
end
def handle_call(:restart, _, state) do
if Enum.empty?(state.interfaces) do
{:ok, new_state} = init(state.hardware)
{:reply, :ok, new_state}
else
Logger.debug ">> detected there are still some network interfaces up."
{:reply, {:error, :not_down}, state}
end
end
def handle_call(:manager, _, state), do: {:reply, state.manager, state}
def handle_call(:state, _, state), do: {:reply, state, state}
@doc """
Gets the entire state. Will probably go away.
"""
def get_state, do: GenServer.call(__MODULE__, :state)
@doc """
Brings down every interface that has been brought up.
"""
def all_down, do: GenServer.call(__MODULE__, :all_down)
@doc """
The pid of the manager that Network was started with.
"""
def manager, do: GenServer.call(__MODULE__, :manager)
@doc """
Bring an interface up with Nerves.InterimWiFi.
"""
def up(iface, settings),do: GenServer.call(__MODULE__, {:up, iface, settings})
@doc """
Bring an interface down.
"""
def down(iface), do: GenServer.call(__MODULE__, {:down, iface})
@doc """
Restarts networking, reloading the config file.
"""
def restart do
all_down
# just to make sure everything is ready
Logger.debug ">> is waiting for The web socket handler to die."
GenEvent.remove_handler(EM, SocketHandler, [])
Logger.debug ">> is waiting for interfaces to come down."
Process.sleep 5000
GenServer.call(__MODULE__, :restart, :infinity)
end
def terminate(_reason,_state) do
GenEvent.remove_handler(EM, SocketHandler, [])
end
defp get_config, do: GenServer.call(FBConfigStorage, {:get, __MODULE__, :all})
@spec get_config(atom) :: any
defp get_config(key),
do: GenServer.call(FBConfigStorage, {:get, __MODULE__, key})
# Be very careful down here
defp parse_config(false, _),
do: %{}
defp parse_config(config, hardware) do
# {"wlan0", %{"type" => "hostapd"}}
# {"eth0", %{"type" => "ethernet", "ip" => %{"mode" => "dhcp"}}}
# {"wlan0",
# %{"type" => "wifi",
# "ip" => %{"mode" => "dhcp"},
# "wifi" => %{"ssid" =>"example",
# "psk" => "example_pass",
# "key_mgmt" => "WPA-PSK"}}}
something = Map.new(config, fn({interface, settings}) ->
case Map.get(settings, "type") do
"hostapd" ->
# start hostapd on this interface
ip = "192.168.24.1"
Logger.debug ">> is starting hostapd client"
handler = Module.concat([Farmbot,Network,Handler,hardware])
{:ok, emanager} = handler.manager
{:ok, pid} =
Hostapd.start_link(
[interface: interface, ip_address: ip, manager: emanager])
{interface, %Interface{ipv4_address: ip, pid: pid}}
"ethernet" ->
Logger.debug ">> is starting ethernet client"
interface_settings = parse_ethernet_settings(settings)
{:ok, pid} = InterimWiFi.setup(interface, interface_settings)
{interface, %Interface{pid: pid}}
"wifi" ->
Logger.debug ">> is starting wpa_supplicant client"
interface_settings = parse_wifi_settings(settings)
{:ok, pid} = InterimWiFi.setup(interface, interface_settings)
{interface, %Interface{pid: pid}}
end
end)
something
end
defp parse_ip_settings(ip_settings) do
case Map.get(ip_settings, "mode") do
"dhcp" -> [ipv4_address_method: "dhcp"]
"static" ->
s = Map.get(ip_settings, "settings")
parse_static_ip_settings(s)
end
end
defp parse_static_ip_settings(s) do
Enum.reduce(s, [ipv4_address_method: "static"], fn({k,v}, acc) ->
case k do
"ipv4_address" -> acc ++ [ipv4_address: v]
"ipv4_subnet_mask" -> acc ++ [ipv4_subnet_mask: v]
"name_servers" -> acc ++ [name_servers: v]
_ -> acc ++ []
end
end)
end
defp parse_ethernet_settings(settings) do
# %{"type" => "ethernet", "settings" => %{"ip" => %{"mode" => "dhcp"}} }
ip_settings =
settings
|> Map.get("settings")
|> Map.get("ip")
|> parse_ip_settings
ip_settings
end
defp parse_wifi_settings(settings) do
ip_settings =
settings
|> Map.get("settings")
|> Map.get("ip")
|> parse_ip_settings
wifi_settings =
settings
|> Map.get("settings")
|> Map.get("wifi")
|> parse_more_wifi_settings
ip_settings ++ wifi_settings
end
defp parse_more_wifi_settings(s) do
case Map.get(s, "key_mgmt") do
"NONE" -> [key_mgmt: :NONE]
"WPA-PSK" ->
[key_mgmt: :"WPA-PSK", psk: Map.get(s, "psk"), ssid: Map.get(s, "ssid")]
_ -> raise "unsupported key management"
end
end
end

View File

@ -0,0 +1,63 @@
defmodule Farmbot.Network.Ntp do
@moduledoc """
Sets time.
"""
require Logger
@doc """
Tries to set the time from ntp.
This will try 3 times to set the time. if it fails the thrid time,
it will return an error
"""
@spec set_time :: :ok | {:error, term}
def set_time do
Logger.debug ">> is getting time from NTP."
f = do_try_set_time
Logger.debug ">> ntp: #{inspect f}"
end
defp do_try_set_time(), do: do_try_set_time(0)
defp do_try_set_time(count) when count < 4 do
# we try to set ntp time 3 times before giving up.
Logger.debug ">> trying to set time (try #{count})"
cmd = "ntpd -q -n -p 0.pool.ntp.org -p 1.pool.ntp.org"
port = Port.open({:spawn, cmd},
[:stream,
:binary,
:exit_status,
:hide,
:use_stdio,
:stderr_to_stdout])
case handle_port(port) do
:ok -> :ok
{:error, _} ->
Logger.debug ">> failed to get time. trying again."
# kill old ntp if it exists
System.cmd("killall", ["ntpd"])
# sleep for a second
Process.sleep(1000)
do_try_set_time(count + 1)
end
end
defp do_try_set_time(_) do
{:error, :timeout}
end
defp handle_port(port) do
receive do
# This is so ugly lol
{^port, {:data, "ntpd: bad address" <> _}} -> {:error, :bad_address}
# print things that ntp says
{^port, {:data, data}} ->
IO.puts "ntp got stuff: #{data}"
handle_port(port)
# when ntp exits, check to make sure its REALLY set
{^port, {:exit_status, 0}} ->
if :os.system_time(:seconds) < 1_474_929 do
{:error, :not_set}
else
:ok
end
end
end
end

View File

@ -0,0 +1,132 @@
defmodule Farmbot.Network.ConfigSocket do
@moduledoc """
Handles the websocket connection from the frontend.
"""
alias RPC.Spec.Notification
alias RPC.Spec.Request
alias RPC.Spec.Response
import RPC.Parser
alias Farmbot.Configurator.EventManager, as: EM
alias Farmbot.FileSystem.ConfigStorage, as: CS
alias Farmbot.Network
alias Farmbot.Auth
use GenEvent
require Logger
def init([]), do: {:ok, []}
def handle_event({:from_socket, message}, state) do
message |> parse |> handle_socket
{:ok, state}
end
# hack to ignore messages from myself here.
def handle_event(_, state), do: {:ok, state}
def handle_socket(
%Request{id: id,
method: "get_current_config",
params: _})
do
Logger.debug ">> got request to get entire config file."
{:ok, read} = CS.read_config_file
thing = read |> Poison.decode!
%Response{id: id, result: Poison.encode!(thing), error: nil}
|> Poison.encode!
|> send_socket
end
def handle_socket(
%Request{id: id,
method: "get_network_interfaces",
params: _})
do
# {hc, 0} = System.cmd("iw", ["wlan0", "scan", "ap-force"])
# #FIXME!!
# interfaces = [
# %{name: "wlan0", type: "wireless", ssids: hc |> clean_ssid},
# %{name: "eth0", type: "ethernet"}
# ]
# %Response{id: id, result: Poison.encode!(interfaces), error: nil}
# |> Poison.encode!
# |> send_socket
interfaces = []
%Response{id: id, result: Poison.encode!(interfaces), error: nil}
|> Poison.encode!
|> send_socket
end
def handle_socket(
%Request{id: id,
method: "upload_config_file",
params: [%{"config" => config}]})
do
# replace the old config file with the new one.
_f = CS.replace_config_file(config)
%Response{id: id, result: "OK", error: nil}
|> Poison.encode!
|> send_socket
end
def handle_socket(
%Request{id: id,
method: "try_log_in",
params: _})
do
# Configurator is done configurating.
Logger.debug ">> has been configurated! going to try to log in."
%Response{id: id, result: "OK", error: nil}
|> Poison.encode!
|> send_socket
# this needs to be spawned because it is inside of an event which results in
# event handler waiting for networking to restart
# and networking waiting for this app to finish its event
# which is waiting for networking to wait for this event etc.
spawn fn() -> Network.restart end
end
def handle_socket(
%Request{id: id,
method: "web_app_creds",
params: [%{"email" => email,
"pass" => pass,
"server" => server}]})
do
Auth.interim(email, pass, server)
%Response{id: id, result: "OK", error: nil}
|> Poison.encode!
|> send_socket
end
def handle_socket(%Notification{} = notification) do
Logger.debug ">> got an incoming RPC Notification: #{inspect notification}"
end
def handle_socket(%Response{id: _, result: "pong", error: _}) do
nil
end
def handle_socket(%Response{} = response) do
Logger.debug ">> got an incoming RPC Response: #{inspect response}"
end
def handle_socket(m) do
Logger.debug ">> got an unhandled rpc message #{inspect m}"
end
defp send_socket(json), do: EM.send_socket({:from_bot, json})
defp clean_ssid(hc) do
hc
|> String.replace("\t", "")
|> String.replace("\\x00", "")
|> String.split("\n")
|> Enum.filter(fn(s) -> String.contains?(s, "SSID") end)
|> Enum.map(fn(z) -> String.replace(z, "SSID: ", "") end)
|> Enum.filter(fn(z) -> String.length(z) != 0 end)
end
def terminate(_,_) do
Logger.debug "websocket died."
end
end

View File

@ -0,0 +1,29 @@
defmodule FarmbotNetwork.Mixfile do
use Mix.Project
def project do
[app: :farmbot_network,
version: "0.1.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.3",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end
def application do
[applications: [:logger, :poison, :nerves_interim_wifi],
mod: {Farmbot.Network, [%{hardware: target(Mix.env)}]}]
end
defp deps, do: [
{:poison, "~> 3.0"},
{:json_rpc, in_umbrella: true},
{:nerves_interim_wifi, "~> 0.1.0"}
]
defp target(:prod), do: System.get_env("NERVES_TARGET") || "rpi3"
defp target(_), do: "development"
end

View File

@ -0,0 +1,8 @@
defmodule FarmbotNetworkTest do
use ExUnit.Case
doctest FarmbotNetwork
test "the truth" do
assert 1 + 1 == 2
end
end

View File

@ -0,0 +1 @@
ExUnit.start()

17
apps/json_rpc/.gitignore vendored 100644
View File

@ -0,0 +1,17 @@
# The directory Mix will write compiled artifacts to.
/_build
# If you run "mix test --cover", coverage assets end up here.
/cover
# The directory Mix downloads your dependencies sources to.
/deps
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez

View File

@ -0,0 +1,24 @@
# JsonRpc
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
1. Add `json_rpc` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:json_rpc, "~> 0.1.0"}]
end
```
2. Ensure `json_rpc` is started before your application:
```elixir
def application do
[applications: [:json_rpc]]
end
```

View File

@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.
# You can configure for your application as:
#
# config :json_rpc, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:json_rpc, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env}.exs"

View File

@ -0,0 +1,29 @@
alias Experimental.{GenStage}
defmodule RPC.MessageHandler do
@moduledoc """
Parses rpc messages and forwars them to the given handler.
"""
use GenStage
import RPC.Parser
@doc """
Requires configuration of a handler.
Handler requires a callback of handle_incoming/1 to be defined
which takes a parsed rpc message. Should probably
make @handler a behavior
and document it. l o l.
"""
def start_link(handler) do
GenStage.start_link(__MODULE__, handler)
end
def init(handler) do
{:consumer, handler, subscribe_to: [RPC.MessageManager]}
end
def handle_events(events, _from, handler) do
for event <- events, do: event |> parse |> handler.handle_incoming
{:noreply, [], handler}
end
end

View File

@ -0,0 +1,38 @@
alias Experimental.{GenStage}
defmodule RPC.MessageManager do
@moduledoc """
This is basically the GenStage Example from the docs.
"""
use GenStage
def start_link() do
GenStage.start_link(__MODULE__, :ok, name: __MODULE__)
end
@spec sync_notify(term, integer) :: any
def sync_notify(event, timeout \\ 5000) do
GenStage.call(__MODULE__, {:notify, event}, timeout)
end
def init(:ok) do
{:producer, {:queue.new, 0}, dispatcher: GenStage.BroadcastDispatcher}
end
def handle_call({:notify, event}, from, {queue, demand}) do
dispatch_events(:queue.in({from, event}, queue), demand, [])
end
def handle_demand(incoming_demand, {queue, demand}) do
dispatch_events(queue, incoming_demand + demand, [])
end
# This is some copy paste magic, not touching it.
defp dispatch_events(queue, demand, events) do
with d when d > 0 <- demand,
{{:value, {from, event}}, queue} <- :queue.out(queue) do
GenStage.reply(from, :ok)
dispatch_events(queue, demand - 1, [event | events])
else
_ -> {:noreply, Enum.reverse(events), {queue, demand}}
end
end
end

View File

@ -0,0 +1,27 @@
defmodule RPC.Parser do
@moduledoc """
Parses JSON RPC.
"""
alias RPC.Spec.Notification, as: Notification
alias RPC.Spec.Request, as: Request
alias RPC.Spec.Response, as: Response
@spec parse(map) :: Notification.t | Request.t | Response.t | :not_valid
# Notification
def parse(%{"id" => nil, "method" => method, "params" => params}) do
%Notification{id: nil, method: method, params: params}
end
# Request
def parse(%{"id" => id, "method" => method, "params" => params}) do
%Request{id: id, method: method, params: params}
end
# Response
def parse(%{"result" => result, "error" => error, "id" => id}) do
%Response{result: result, error: error, id: id}
end
def parse(_blah) do
:not_valid
end
end

View File

@ -0,0 +1,86 @@
defmodule RPC.Spec do
# TODO: maybe document rpc spec here or in the following modules?
@moduledoc false
defmodule Request do
@moduledoc false
@type t :: %__MODULE__{
method: String.t,
params: [map,...],
id: String.t
}
defstruct [
method: nil,
params: nil,
id: nil
]
def create(%{
"method" => method,
"params" => params,
"id" => id
})
do
%__MODULE__{
method: method,
params: params,
id: id
}
end
def create(_), do: {__MODULE__, :malformed}
end
defmodule Notification do
@moduledoc false
@type t :: %__MODULE__{
method: String.t,
params: [map,...],
id: nil
}
defstruct [
method: nil,
params: nil,
id: nil
]
def create(%{
"method" => method,
"params" => params,
"id" => nil
})
do
%__MODULE__{
method: method,
params: params,
id: nil
}
end
def create(_), do: {__MODULE__, :malformed}
end
defmodule Response do
@moduledoc false
@type t :: %__MODULE__{
result: any,
error: String.t | nil,
id: String.t
}
defstruct [
result: nil,
error: nil,
id: nil
]
def create(%{
"result" => result,
"error" => error,
"id" => id
})
do
%__MODULE__{
result: result,
error: error,
id: id
}
end
def create(_), do: {__MODULE__, :malformed}
end
end

View File

@ -0,0 +1,17 @@
defmodule RPC.Supervisor do
@moduledoc false
use Supervisor
@transport Application.get_env(:json_rpc, :transport)
@handler Application.get_env(:json_rpc, :handler)
def init(_args) do
children = [
worker(RPC.MessageManager, []),
worker(RPC.MessageHandler, [@handler], name: RPC.MessageHandler),
worker(@transport, [[]], restart: :permanent)
]
supervise(children, strategy: :one_for_one, name: __MODULE__)
end
def start_link(args), do: Supervisor.start_link(__MODULE__, args)
end

View File

@ -0,0 +1,24 @@
defmodule JsonRpc.Mixfile do
use Mix.Project
def project do
[app: :json_rpc,
version: "0.1.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.3",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps]
end
def application do
[applications: [:logger, :poison]]
end
defp deps do
[{:poison, "~> 3.0"}]
end
end

View File

@ -0,0 +1,8 @@
defmodule JsonRpcTest do
use ExUnit.Case
doctest JsonRpc
test "the truth" do
assert 1 + 1 == 2
end
end

View File

@ -0,0 +1 @@
ExUnit.start()