wrong dir again
parent
63ded4001a
commit
695df41994
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"}]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
},
|
||||
"hardware":{
|
||||
"params":{}
|
||||
}
|
||||
},
|
||||
"ntp": false
|
||||
}
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
},
|
||||
"hardware":{
|
||||
"params":{}
|
||||
}
|
||||
},
|
||||
"ntp": true
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
```
|
||||
|
|
@ -0,0 +1 @@
|
|||
use Mix.Config
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
defmodule Module.concat([Farmbot, Network, Handler, "qemu"]) do
|
||||
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
defmodule FarmbotNetworkTest do
|
||||
use ExUnit.Case
|
||||
doctest FarmbotNetwork
|
||||
|
||||
test "the truth" do
|
||||
assert 1 + 1 == 2
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
ExUnit.start()
|
|
@ -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
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
defmodule JsonRpcTest do
|
||||
use ExUnit.Case
|
||||
doctest JsonRpc
|
||||
|
||||
test "the truth" do
|
||||
assert 1 + 1 == 2
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
ExUnit.start()
|
Loading…
Reference in New Issue