First workspace commit

This commit is contained in:
mhfowler 2021-08-06 13:58:40 -04:00
parent 87435932dc
commit d8803e9974
251 changed files with 22022 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
target

3834
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[workspace]
members = [
"peach-lib",
"peach-config",
"peach-network",
"peach-web",
"peach-menu",
"peach-monitor",
"peach-stats",
"peach-probe",
"peach-dyndns-updater"
]

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

1
peach-config/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target

37
peach-config/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "peach-config"
version = "0.1.10"
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
edition = "2018"
description = "Command line tool for installing, updating and configuring PeachCloud"
homepage = "https://opencollective.com/peachcloud"
repository = "https://github.com/peachcloud/peach-config"
readme = "README.md"
license = "AGPL-3.0-only"
publish = false
[package.metadata.deb]
depends = "$auto"
extended-description = """\
peach-config is a command line tool for installing, updating and configuring PeachCloud"""
maintainer-scripts="debian"
assets = [
["target/release/peach-config", "usr/bin/", "755"],
["conf/**/*", "/var/lib/peachcloud/conf/", "644"],
["README.md", "usr/share/doc/peach-config/README", "644"],
]
[badges]
travis-ci = { repository = "peachcloud/peach-config", branch = "main" }
maintenance = { status = "actively-developed" }
[dependencies]
env_logger = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.64"
snafu = "0.6"
regex = "1"
structopt = "0.3.13"
clap = "2.33.3"
log = "0.4"
lazy_static = "1.4.0"

94
peach-config/README.md Normal file
View File

@ -0,0 +1,94 @@
# peach-config
[![Build Status](https://travis-ci.com/peachcloud/peach-config.svg?branch=main)](https://travis-ci.com/peachcloud/peach-config)
![Generic badge](https://img.shields.io/badge/version-0.1.10-<COLOR>.svg)
Rust crate which provides a CLI tool for installing and updating PeachCloud.
## Installation From PeachCloud Disc Image
The recommended way to install PeachCloud is to download the latest PeachCloud disc image from http://releases.peachcloud.org,
and flash it to an SD card. peach-config is included as part of this disc image, and can then
be used as a tool for updating PeachCloud as needed.
You can find detailed instructions on setting up PeachCloud from a PeachCloud disc image [here](docs/installation-from-peach-disc-image.md).
## Installation From Debian Disc Image
You can find a guide for installing plain Debian onto a Raspberry pi [here](docs/installation-from-debian-disc-image.md).
Once you have Debian running on your pi, you can install peach-config by adding the PeachCloud apt repository and using apt.
To add the PeachCloud Debian package archive as an apt source, run the following commands from your Pi:
``` bash
echo "deb http://apt.peachcloud.org/ buster main" > /etc/apt/sources.list.d/peach.list
wget -O - http://apt.peachcloud.org/pubkey.gpg | sudo apt-key add -
```
You can then install peach-config with apt:
``` bash
sudo apt-get update
sudo apt-get install python3-peach-config
```
Alternatively you can run the following one-liner, which does all of the above:
> curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/peachcloud/peach-config-rust/main/install.sh | sh
peach-config has only been tested on a Raspberry Pi 3 B+ running Debian 10.
## Usage
The peach-config debian module installs a command-line tool to `/usr/bin/peach-config`.
`peach-config` is a tool for installing PeachCloud and for updating it.
`peach-config -h` shows the help menu:
```bash
USAGE:
peach-config [FLAGS] [SUBCOMMAND]
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
-v, --verbose
SUBCOMMANDS:
help Prints this message or the help of the given subcommand(s)
manifest Prints json manifest of peach configurations
setup Idempotent setup of PeachCloud
update Updates all PeachCloud microservices
```
The setup command takes a few different parameters to customize configuration.
```bash
USAGE:
peach-config setup [FLAGS] [OPTIONS]
FLAGS:
-d, --default-locale Use the default en_US.UTF-8 locale for compatability
-h, --help Prints help information
-i, --i2c Setup i2c configurations
-n, --no-input Run peach-config in non-interactive mode
-V, --version Prints version information
OPTIONS:
-r, --rtc <rtc> Optionally select which model of real-time-clock is being used {ds1307, ds3231}
```
I2C configuration is necessary for the OLED display and physical interface to work correctly. RTC configuration is required for the real-time clock to work correctly. When passing the `-r` flag, the type of real-time clock module must be included (either ds1307 or ds3231). Selecting real-time clock configuration will not work if the I2C flag is not selected (in other words, the real-time clock requires I2C).
Run the script as follows for a full installation and configuration with I2C and the ds3231 RTC module:
`peach-config setup -i -r ds3231 -n -d`
## Licensing
AGPL-3.0

2
peach-config/build.sh Executable file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env bash
sudo RUST_LOG=info cargo build

View File

@ -0,0 +1 @@
SUBSYSTEM=="gpio", KERNEL=="gpiochip*", ACTION=="add", PROGRAM="/bin/sh -c 'chown -R root:gpio-user /dev/gpiochip* ; chmod -R 770 /dev/gpiochip*'"

View File

@ -0,0 +1,20 @@
# Configuration File Paths
```bash
/boot/firmware/bcm2710-rpi-3-b.dtb
/boot/firmware/config.txt
/boot/firmware/overlays/mygpio.dtbo
/etc/default/hostapd
/etc/dhcpd.conf
/etc/dnsmasq.conf
/etc/hostapd/hostapd.conf
/etc/hostname
/etc/hosts
/etc/modules
/etc/network/interfaces
/etc/nginx/sites-available/peach.conf
/etc/systemd/system/activate-rtc.service
/etc/udev/rules.d/00-accesspoint.rules
/etc/wpa_supplicant/wpa_supplicant.conf
```

View File

@ -0,0 +1,10 @@
[Unit]
Description=Add RTC as new I2C device
[Service]
Type=oneshot
RemainAfterExit=no
ExecStart=/usr/local/bin/activate_rtc
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,2 @@
#!/bin/bash
echo ds1307 0x68 > /sys/class/i2c-adapter/i2c-1/new_device

View File

@ -0,0 +1,14 @@
#!/bin/bash
# Start the ap0 service (access point) if wlan0 is active but not connected
# returns "active" or "inactive"
wlan_active=$(/usr/bin/systemctl is-active wpa_supplicant@wlan0.service)
# returns "up" or "down"
wlan_state=$(cat /sys/class/net/wlan0/operstate)
if [ $wlan_active = "active" ] && [ $wlan_state = "down" ]; then
echo "Starting ap0 service"
/usr/bin/systemctl start wpa_supplicant@ap0.service
fi

View File

@ -0,0 +1,2 @@
SUBSYSTEM=="net", ACTION=="add", RUN+="/usr/sbin/iw dev wlan0 interface add ap0 type __ap"
SUBSYSTEM=="net", ACTION=="add", RUN+="/usr/bin/ip address add 11.11.11.10/24 brd + dev ap0"

View File

@ -0,0 +1,3 @@
# Backup
This directory contains all the legacy configuration files for PeachCloud networking. These files have been deprecated by the transition to using systemd-networkd for networking. They are being kept here as a backup but will eventually be removed entirely.

View File

@ -0,0 +1,3 @@
interface ap0
static ip_address=11.11.11.10/24
nohook wpa_supplicant

View File

@ -0,0 +1,8 @@
interface=ap0
listen-address=11.11.11.10
bind-dynamic
server=208.67.222.222
server=208.67.220.220
domain-needed
bogus-priv
dhcp-range=11.11.11.11,11.11.11.30,255.255.255.0,24h

View File

@ -0,0 +1,22 @@
# Defaults for hostapd initscript
#
# WARNING: The DAEMON_CONF setting has been deprecated and will be removed
# in future package releases.
#
# See /usr/share/doc/hostapd/README.Debian for information about alternative
# methods of managing hostapd.
#
# Uncomment and set DAEMON_CONF to the absolute path of a hostapd configuration
# file and hostapd will be started during system boot. An example configuration
# file can be found at /usr/share/doc/hostapd/examples/hostapd.conf.gz
#
DAEMON_CONF="/etc/hostapd/hostapd.conf"
# Additional daemon options to be appended to hostapd command:-
# -d show more debug messages (-dd for even more)
# -K include key data in debug messages
# -t include timestamps in some debug messages
#
# Note that -B (daemon mode) and -P (pidfile) options are automatically
# configured by the init.d script and must not be added to DAEMON_OPTS.
#
#DAEMON_OPTS=""

View File

@ -0,0 +1,15 @@
interface=ap0
hw_mode=g
channel=8
wmm_enabled=0
macaddr_acl=0
beacon_int=100
auth_algs=3
wmm_enabled=1
ignore_broadcast_ssid=0
wpa=2
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP
ssid=peach
wpa_passphrase=cloudless

View File

@ -0,0 +1,22 @@
# interfaces(5) file used by ifup(8) and ifdown(8)
# Include files from /etc/network/interfaces.d:
# source-directory /etc/network/interfaces.d
# Loopback
auto lo
iface lo inet loopback
# Ethernet
iface eth0 inet dhcp
# Wireless
auto wlan0
iface wlan0 inet dhcp
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
# Access Point
iface ap0 inet static
address 11.11.11.10
netmask 255.255.255.0
network 11.11.11.0
broadcast 11.11.11.255

View File

@ -0,0 +1,17 @@
#
# Allow peach-network user to execute activate_ap and
# activate_client scripts without needing to enter
# a password for sudo'd command.
#
# User alias for PeachCloud microservices which control networking
User_Alias PEACH_NTWK = peach-network
# Command alias for activate_ap and activate_client scripts
Cmnd_Alias SCRIPTS = /usr/local/bin/activate_ap, /usr/local/bin/activate_client
# Command alias for network-related actions
Cmnd_Alias SERVICE = /usr/bin/systemctl unmask hostapd, /usr/bin/systemctl start hostapd, /usr/bin/systemctl stop hostapd, /usr/bin/systemctl stop dnsmasq, /usr/bin/systemctl start dnsmasq, /usr/bin/systemctl start wpa_supplicant, /usr/bin/systemctl stop wpa_supplicant, /usr/sbin/ifup wlan0, /usr/sbin/ifdown wlan0, /bin/ip link set wlan0 mode default
# Allow PEACH_NTWK users to execute SCRIPTS & SERVICE commands without password
PEACH_NTWK ALL=(ALL) NOPASSWD: SCRIPTS, SERVICE

View File

@ -0,0 +1,8 @@
ctrl_interface=/run/wpa_supplicant
update_config=1
network={
ssid="YOUR_SSID"
#psk="YOUR_PASS"
psk=7f4f1a9d128f9fc2741f4229d57e3e7c355b1760b27aa8c3816c461497f5cd2a
}

Binary file not shown.

9
peach-config/conf/config.txt Executable file
View File

@ -0,0 +1,9 @@
# Switch the CPU from ARMv7 into ARMv8 (aarch64) mode
arm_control=0x200
enable_uart=1
kernel=vmlinuz-4.19.0-10-arm64
# For details on the initramfs directive, see
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
initramfs initrd.img-4.19.0-10-arm64

View File

@ -0,0 +1,16 @@
# Switch the CPU from ARMv7 into ARMv8 (aarch64) mode
arm_control=0x200
enable_uart=1
upstream_kernel=1
# Activate I2C
dtparam=i2c_arm=on
# Activate DS1307 RTC module
dtoverlay=i2c-rtc,ds1307
# Apply device tree overlay to enable pull-up resistors for buttons
device_tree_overlay=overlays/mygpio.dtbo
kernel=vmlinuz-4.19.0-10-arm64
# For details on the initramfs directive, see
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
initramfs initrd.img-4.19.0-10-arm64

View File

@ -0,0 +1,16 @@
# Switch the CPU from ARMv7 into ARMv8 (aarch64) mode
arm_control=0x200
enable_uart=1
upstream_kernel=1
# Activate I2C
dtparam=i2c_arm=on
# Activate DS3231 RTC module
dtoverlay=i2c-rtc,ds3231
# Apply device tree overlay to enable pull-up resistors for buttons
device_tree_overlay=overlays/mygpio.dtbo
kernel=vmlinuz-4.19.0-10-arm64
# For details on the initramfs directive, see
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
initramfs initrd.img-4.19.0-10-arm64

View File

@ -0,0 +1,14 @@
# Switch the CPU from ARMv7 into ARMv8 (aarch64) mode
arm_control=0x200
enable_uart=1
upstream_kernel=1
# Activate I2C
dtparam=i2c_arm=on
# Apply device tree overlay to enable pull-up resistors for buttons
device_tree_overlay=overlays/mygpio.dtbo
kernel=vmlinuz-4.19.0-17-arm64
# For details on the initramfs directive, see
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
initramfs initrd.img-4.19.0-17-arm64

View File

@ -0,0 +1 @@
peach

5
peach-config/conf/hosts Normal file
View File

@ -0,0 +1,5 @@
127.0.0.1 localhost
127.0.1.1 peach
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

View File

@ -0,0 +1,6 @@
# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.
i2c-dev
i2c-bcm2835

View File

@ -0,0 +1,7 @@
# /etc/modules: kernel modules to load at boot time.
#
# This file contains the names of kernel modules that should be loaded
# at boot time, one per line. Lines beginning with "#" are ignored.
i2c-dev
i2c-bcm2835
rtc-ds1307

BIN
peach-config/conf/mygpio.dtbo Executable file

Binary file not shown.

View File

@ -0,0 +1,22 @@
[Match]
Name=e*
[Network]
## Uncomment only one option block
# Option: using a DHCP server and multicast DNS
LLMNR=no
LinkLocalAddressing=no
MulticastDNS=yes
DHCP=ipv4
# Option: using link-local ip addresses and multicast DNS
#LLMNR=no
#LinkLocalAddressing=yes
#MulticastDNS=yes
# Option: using static ip address and multicast DNS
# (example, use your settings)
#Address=192.168.50.60/24
#Gateway=192.168.50.1
#DNS=84.200.69.80 1.1.1.1
#MulticastDNS=yes

View File

@ -0,0 +1,4 @@
[Match]
Name=wlan0
[Network]
DHCP=yes

View File

@ -0,0 +1,11 @@
[Match]
Name=ap0
[Network]
Address=11.11.11.1/24
# IPMasquerade is doing NAT
# Uncomment the two lines below to share internet over ap0
#IPMasquerade=yes
#IPForward=yes
DHCPServer=yes
[DHCPServer]
DNS=84.200.69.80 1.1.1.1

View File

@ -0,0 +1,10 @@
[Unit]
Description=Start the ap0 service (access point) if the wlan0 service is active but not connected to any access point
[Service]
Type=oneshot
RemainAfterExit=no
ExecStart=/usr/local/bin/ap_auto_deploy
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,9 @@
[Unit]
Description=Determine when and how often the ap_auto_deploy script is run
[Timer]
OnBootSec=60s
OnUnitActiveSec=180s
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,8 @@
[Unit]
Before=network.target
[Service]
ExecStart=/usr/local/bin/copy-wlan.sh
[Install]
WantedBy=default.target

View File

@ -0,0 +1,8 @@
#!/bin/bash
FILE=/boot/firmware/wpa_supplicant.conf
if test -f "$FILE"; then
cp $FILE /etc/wpa_supplicant/wpa_supplicant-wlan0.conf
chown root:netdev /etc/wpa_supplicant/wpa_supplicant-wlan0.conf
rm $FILE
fi

View File

@ -0,0 +1,11 @@
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="peach"
mode=2
key_mgmt=WPA-PSK
proto=RSN WPA
psk="cloudless"
frequency=2412
}

View File

@ -0,0 +1,6 @@
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="YourRouterSsid"
psk="password_goes_here"
}

View File

@ -0,0 +1,18 @@
[Unit]
Description=WPA supplicant daemon (interface-specific version)
Requires=sys-subsystem-net-devices-wlan0.device
After=sys-subsystem-net-devices-wlan0.device
Conflicts=wpa_supplicant@wlan0.service
Before=network.target
Wants=network.target
# NetworkManager users will probably want the dbus version instead.
[Service]
Type=simple
ExecStartPre=/sbin/iw dev wlan0 interface add ap0 type __ap
ExecStart=/sbin/wpa_supplicant -c/etc/wpa_supplicant/wpa_supplicant-%I.conf -Dnl80211,wext -i%I
ExecStopPost=/sbin/iw dev ap0 del
[Install]
Alias=multi-user.target.wants/wpa_supplicant@%i.service

View File

@ -0,0 +1,7 @@
server {
listen 80;
server_name peach.local www.peach.local;
location / {
proxy_pass http://127.0.0.1:3000;
}
}

View File

@ -0,0 +1 @@
deb http://apt.peachcloud.org/ buster main

View File

@ -0,0 +1,2 @@
[Service]
TimeoutStartSec=5

View File

@ -0,0 +1,13 @@
#
# Allow peach microservices to initiate reboot / shutdown
# without needing to enter a password for sudo'd command.
#
# User alias for PeachCloud microservices which initiate shutdown
User_Alias PEACH_CTRL = peach-menu, peach-web
# Command alias for reboot and shutdown
Cmnd_Alias SHUTDOWN = /sbin/reboot, /sbin/shutdown
# Allow PEACH_CTRL users to execute SHUTDOWN commands without password
PEACH_CTRL ALL=(ALL) NOPASSWD: SHUTDOWN

View File

@ -0,0 +1,13 @@
[Unit]
Description=Query and configure network interfaces using JSON-RPC over HTTP.
[Service]
Type=simple
User=root
Group=netdev
Environment="RUST_LOG=error"
ExecStart=/usr/bin/peach-network
Restart=always
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,64 @@
# Direct Ethernet Setup
This file contains documentation for two different ways of working with PeachCloud using a direct ethernet connection (useful for development purposes).
## Method 1
If you are close to a router, you can plug your Pi into the router via ethernet and you should be able to SSH into the Pi from any laptop connected to the same router via WiFi or ethernet.
## Method 2
Using a DHCP server on your laptop (instructions for a laptop running Debian).
**Install the DHCP server:**
`sudo apt-get install isc-dhcp-server`
**Configure the DHCP server:**
In `/etc/dhcp/dhcpd.conf`, add the following section:
```plaintext
subnet 10.0.2.0 netmask 255.255.255.240 {
range 10.0.2.2 10.0.2.14;
option routers 10.0.2.1;
host peach {
hardware ethernet b8:27:eb:b1:b1:4e;
fixed-address 10.0.2.4;
}
}
```
Note that `b8:27:eb:b1:b1:4e` may need to be replaced with the address of your Pi's ethernet interface, which you can look up by running `ip a` on the Pi. This address should be static. The `fixed-address` section of the config tells the DHCP server to always give the specified client (`peach`) the `10.0.2.4` IP address.
In `/etc/default/isc-dhcp-server`, add the following section with the name of your ethernet interface (in this case `ens9`):
```plaintext
INTERFACESv4="ens9"
INTERFACESv6=""
```
In `/etc/network/interfaces`, set a static IP for your ethernet interface by adding this section:
```plaintext
auto ens9
iface ens9 inet static
address 10.0.2.1
```
**Start the DHCP server:**
`sudo systemctl start isc-dhcp-server`
**Connect the ethernet cable:**
Connect your Pi to the laptop via an ethernet cable. You should then be able to SSH into the Pi using the following command:
`ssh peach@10.0.2.4`
_Note:_ On the Pi, internet traffic will still need to go through `wlan0` interface.
On Mac OS you don't need to change the network config on your laptop. Simply enable internet sharing over ethernet and you should be able to connect to the Pi.

View File

@ -0,0 +1,147 @@
## Installation From Debian Disc Image
You can also setup PeachCloud by installing Debian onto an sd card, and then installing and running peach-config.
This is essentially how the PeachCloud disc image is created (see [peach-img-builder](https://github.com/peachcloud/peach-img-builder)).
Here are the steps for installing peach-config on Debian.
#### Step 1: Flash The SD Card
Download the latest Debian Buster preview image for RPi3 and flash it to an SD card.
_Note:_ Be sure to use the correct device location in the `dd` command, otherwise you risk wiping another connected USB device. `sudo dmesg | tail` can be run after plugging in the SD card to determine the correct device location:
```bash
wget https://raspi.debian.net/verified/20200831_raspi_3.img.xz
xzcat 20200831_raspi_3.img.xz | sudo dd of=/dev/sdb bs=64k oflag=dsync status=progress
```
On Mac OS, use the following command to flash the SD card:
`xzcat 20200831_raspi_3.img.xz | sudo dd of=/dev/sdcarddisc`
Alternatively, use [Etcher](https://www.balena.io/etcher/).
_Note:_ If the above image link stops working, you can find the complete list of Raspberry Pi Debian images [here](https://raspi.debian.net/tested-images/).
#### Step 2: Connect To The Internet
Use the following commands to connect to a local WiFi network over the `wlan0` interface (assuming `eth0` connection is not possible):
```bash
# username
root
# password (by default raspberry debian requires no password, so we set the password for root here)
passwd
# set interface up (run command twice if you receive 'link is not ready' error on first try)
ip link set wlan0 up
# append ssid and password for wifi access point
wpa_passphrase <SSID> <PASS> > /etc/wpa_supplicant/wpa_supplicant.conf
# open wpa_supplicant.conf
nano /etc/wpa_supplicant/wpa_supplicant.conf
```
[ Add the following two lines to top of file ]
```plaintext
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
```
[ Save and exit ]
```bash
# open network interfaces config
nano /etc/network/interfaces
```
[ Add the following lines to the file ]
```plaintext
auto lo
iface lo inet loopback
auto eth0
allow-hotplug eth0
iface eth0 inet dhcp
auto wlan0
allow-hotplug wlan0
iface wlan0 inet dhcp
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
```
[ Save and exit ]
`reboot now`
[ Pi should now be connected to the WiFi network ]
#### Step 3: Install PeachCloud
You can run the following one-liner to install peach-cloud (TODO: make this install peachcloud instead of rust):
```
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
Alternatively, you can run the commands in the install script manually.
## Connecting
Once the setup script has been run, connect to the system remotely over the local network using ssh or mosh:
`ssh user@peach.local` or `mosh user@peach.local`
There is a file with detailed instructions on how to connect via a direct ethernet cable located in `docs/direct-ethernet-setup.md`
## Network
Networking is handled by `wpa_supplicant` and `systemd-networkd`.
The RPi connects to other networks with the `wlan0` interface and deploys an access point on the `ap0` interface. Only one of these modes is active at a time (client or access point). The RPi boots in client mode by default.
To switch to access point mode:
`sudo systemctl start wpa_supplicant@ap0.service`
To switch to client mode:
`sudo systemctl start wpa_supplicant@wlan0.service`
_Note:_ No stopping of services or rebooting is required.
To enable access point mode on boot:
```bash
sudo systemctl disable wpa_supplicant@wlan0.service
sudo systemctl enable wpa_supplicant@ap0.service
```
A standalone networking configuration script is included in this repository (`scripts/setup_networking.py`). Network-related documentation can also be found in this repository (`docs`).
This repository also contains a script for automatically starting an access point on `ap0` if the `wlan0` service is active but not connected (`scripts/ap_auto_deploy.sh`). The executable script is installed at `/usr/local/bin/ap_auto_deploy` and can either be run once-off or scheduled for repeated execution using a `systemd` service file and timer file (`conf/network/ap-auto-deploy.service` and `conf/network/ap-auto-deploy.timer`). When the timer is enabled for repeated execution, the script is automatically run 60 seconds after boot and every 180 seconds after that.
To stop and disable the access point auto deploy service:
```bash
sudo systemctl stop ap-auto-deploy.timer
sudo systemctl disable ap-auto-deploy.timer
```
## Troubleshooting
You may encounter DNS issues if your system time is inaccurate. Please refer to this [StackExchange answer](https://unix.stackexchange.com/a/570382/450882) for details. The steps to remedy the situation are offered here in brief:
```bash
sudo -Es
timedatectl set-ntp 0
# edit this line according to your current date & time
timedatectl set-time "2021-01-13 11:37:10"
timedatectl set-ntp 1
exit

View File

@ -0,0 +1,38 @@
## Installation From PeachCloud Disc Image
#### Step 1: Flash The SD Card
Download the latest PeachCloud image from http://releases.peachcloud.org and flash it to an SD card.
_Note:_ Be sure to use the correct device location in the `dd` command, otherwise you risk wiping another connected USB device. `sudo dmesg | tail` can be run after plugging in the SD card to determine the correct device location:
```bash
wget http://releases.peachcloud.org/peach-imgs/20210225/20210225_peach_raspi3.img
sudo dd 20210225_peach_raspi3.img of=/dev/sdb bs=64k oflag=dsync status=progress
```
On Mac OS, use the following command to flash the SD card:
`xzcat 20200831_raspi_3.img.xz | sudo dd of=/dev/sdcarddisc`
Alternatively, use [Etcher](https://www.balena.io/etcher/).
_Note:_ If the above image link stops working, you can find the latest image [here](http://releases.peachcloud.org).
Your SD card now has a complete PeachCloud installation on it and is ready to use.
#### Step 2: Connecting To The Internet
## Via peach.local
TODO: write this documentation
## Via a Screen
TODO: write this documentation
#### Step 3: Getting Started
TODO: write this documentation

View File

@ -0,0 +1,116 @@
install drivers for usb wifi adapter (RT5370 chipset):
apt install firmware-ralink
-----
quick-setup (https://raspberrypi.stackexchange.com/a/108593):
# create interface file for a wired connection
sudo -Es
cat > /etc/systemd/network/04-wired.network <<EOF
[Match]
Name=e*
[Network]
## Uncomment only one option block
# Option: using a DHCP server and multicast DNS
LLMNR=no
LinkLocalAddressing=no
MulticastDNS=yes
DHCP=ipv4
# Option: using link-local ip addresses and multicast DNS
#LLMNR=no
#LinkLocalAddressing=yes
#MulticastDNS=yes
# Option: using static ip address and multicast DNS
# (example, use your settings)
#Address=192.168.50.60/24
#Gateway=192.168.50.1
#DNS=84.200.69.80 1.1.1.1
#MulticastDNS=yes
EOF
# deinstall classic networking
sudo -Es
apt --autoremove purge ifupdown dhcpcd5 isc-dhcp-client isc-dhcp-common rsyslog
apt-mark hold ifupdown dhcpcd5 isc-dhcp-client isc-dhcp-common rsyslog raspberrypi-net-mods openresolv
rm -r /etc/network /etc/dhcp
# setup/enable systemd-resolved and systemd-networkd
apt --autoremove purge avahi-daemon
apt-mark hold avahi-daemon libnss-mdns
apt install libnss-resolve
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
systemctl enable systemd-networkd.service systemd-resolved.service
exit
# configure wpa_supplicant for wlan1 as access point
sudo -Es
apt install rfkill
cat > /etc/wpa_supplicant/wpa_supplicant-wlan0.conf <<EOF
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="peach"
mode=2
key_mgmt=WPA-PSK
psk="cloudless"
frequency=2412
}
EOF
chmod 600 /etc/wpa_supplicant/wpa_supplicant-wlan1.conf
systemctl disable wpa_supplicant.service
systemctl enable wpa_supplicant@wlan1.service
rfkill unblock 1
# configure wpa_supplicant for wlan0 as client
cat > /etc/wpa_supplicant/wpa_supplicant-wlan1.conf <<EOF
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="TestNet"
psk="anotherSecretPassword"
}
EOF
chmod 600 /etc/wpa_supplicant/wpa_supplicant-wlan0.conf
systemctl disable wpa_supplicant.service
systemctl enable wpa_supplicant@wlan0.service
rfkill unblock 0
# configure interfaces
cat > /etc/systemd/network/08-wlan1.network <<EOF
[Match]
Name=wlan1
[Network]
Address=11.11.11.1/24
# IPMasquerade is doing NAT
#IPMasquerade=yes
#IPForward=yes
DHCPServer=yes
[DHCPServer]
DNS=84.200.69.80 1.1.1.1
EOF
cat > /etc/systemd/network/12-wlan0.network <<EOF
[Match]
Name=wlan0
[Network]
DHCP=yes
EOF

View File

@ -0,0 +1,150 @@
Infinite thanks to [ingo](https://raspberrypi.stackexchange.com/users/79866/ingo) for sharing these setup instructions online!
-----
[quick-setup](https://raspberrypi.stackexchange.com/a/108593)
# deinstall classic networking
sudo -Es # if not already done
apt install libnss-resolve
apt --autoremove purge ifupdown dhcpcd5 isc-dhcp-client isc-dhcp-common rsyslog
apt-mark hold ifupdown dhcpcd5 isc-dhcp-client isc-dhcp-common rsyslog raspberrypi-net-mods openresolv
rm -r /etc/network /etc/dhcp
# setup/enable systemd-resolved and systemd-networkd
apt --autoremove purge avahi-daemon
apt-mark hold avahi-daemon libnss-mdns
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
systemctl enable systemd-networkd.service systemd-resolved.service
# create interface file for a wired connection
sudo -Es
cat > /etc/systemd/network/04-wired.network <<EOF
[Match]
Name=e*
[Network]
## Uncomment only one option block
# Option: using a DHCP server and multicast DNS
LLMNR=no
LinkLocalAddressing=no
MulticastDNS=yes
DHCP=ipv4
# Option: using link-local ip addresses and multicast DNS
#LLMNR=no
#LinkLocalAddressing=yes
#MulticastDNS=yes
# Option: using static ip address and multicast DNS
# (example, use your settings)
#Address=192.168.50.60/24
#Gateway=192.168.50.1
#DNS=84.200.69.80 1.1.1.1
#MulticastDNS=yes
EOF
reboot
[switch between wifi client and access point without reboot](https://raspberrypi.stackexchange.com/questions/93311/switch-between-wifi-client-and-access-point-without-reboot)
# setup wpa_supplicant as wifi client with wlan0
cat > /etc/wpa_supplicant/wpa_supplicant-wlan0.conf <<EOF
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="TestNet"
psk="verySecretPwassword"
}
EOF
chmod 600 /etc/wpa_supplicant/wpa_supplicant-wlan0.conf
systemctl disable wpa_supplicant.service
systemctl enable wpa_supplicant@wlan0.service
# setup wpa_supplicant as access point with ap0
cat > /etc/wpa_supplicant/wpa_supplicant-ap0.conf <<EOF
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="RPiNet"
mode=2
key_mgmt=WPA-PSK
proto=RSN WPA
psk="anotherPassword"
frequency=2412
}
EOF
chmod 600 /etc/wpa_supplicant/wpa_supplicant-ap0.conf
# configure interfaces
cat > /etc/systemd/network/08-wlan0.network <<EOF
[Match]
Name=wlan0
[Network]
DHCP=yes
EOF
cat > /etc/systemd/network/12-ap0.network <<EOF
[Match]
Name=ap0
[Network]
Address=192.168.4.1/24
DHCPServer=yes
[DHCPServer]
DNS=84.200.69.80 1.1.1.1
EOF
# modify service for access point to use ap0
systemctl disable wpa_supplicant@ap0.service
systemctl edit --full wpa_supplicant@ap0.service
# modify/insert only these lines: Requires=, After=, Conflicts=, ExecStartPre= and ExecStopPost= as shown:
[Unit]
Description=WPA supplicant daemon (interface-specific version)
Requires=sys-subsystem-net-devices-wlan0.device
After=sys-subsystem-net-devices-wlan0.device
Conflicts=wpa_supplicant@wlan0.service
Before=network.target
Wants=network.target
# NetworkManager users will probably want the dbus version instead.
[Service]
Type=simple
ExecStartPre=/sbin/iw dev wlan0 interface add ap0 type __ap
ExecStart=/sbin/wpa_supplicant -c/etc/wpa_supplicant/wpa_supplicant-%I.conf -Dnl80211,wext -i%I
ExecStopPost=/sbin/iw dev ap0 del
[Install]
Alias=multi-user.target.wants/wpa_supplicant@%i.service
# set wlan0 to run as client on startup
sudo systemctl enable wpa_supplicant@wlan0.service
sudo systemctl disable wpa_supplicant@ap0.service
reboot
# switch the service when desired (no stopping of services is required)
sudo systemctl start wpa_supplicant@ap0.service
sudo systemctl start wpa_supplicant@wlan0.service

6
peach-config/install.sh Normal file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
echo "deb http://apt.peachcloud.org/ buster main" > /etc/apt/sources.list.d/peach.list
wget -O - http://apt.peachcloud.org/pubkey.gpg | sudo apt-key add -
apt-get update
apt-get install -y peach-config
RUST_LOG=info peach-config setup -i -n -d

View File

@ -0,0 +1,25 @@
// Directory on peachcloud device where CONF files are store
// before they are copied to their eventual locations
pub const CONF: &str = "/var/lib/peachcloud/conf";
// List of package names which are installed via apt-get
pub const SERVICES: [&str; 11] = [
"peach-oled",
"peach-network",
"peach-stats",
"peach-web",
"peach-menu",
"peach-buttons",
"peach-monitor",
"peach-probe",
"peach-dyndns-updater",
"peach-go-sbot",
"peach-config",
];
// File path to where current hardware configurations are stored
// note: this is stored separately from /var/lib/peachcloud/config.yml
// because it is not a configuration which should be manually edited
// the values in the hardware_config.json are a log of what peach-config configured
// whereas the values in config.yml can be manually modified if needed
pub const HARDWARE_CONFIG_FILE: &str = "/var/lib/peachcloud/hardware_config.json";

48
peach-config/src/error.rs Normal file
View File

@ -0,0 +1,48 @@
#![allow(clippy::nonstandard_macro_braces)]
pub use snafu::ResultExt;
use snafu::Snafu;
#[derive(Debug, Snafu)]
#[allow(clippy::enum_variant_names)]
#[snafu(visibility(pub(crate)))]
pub enum PeachConfigError {
#[snafu(display("Command not found: \"{}\"", command))]
CmdIoError {
source: std::io::Error,
command: String,
},
#[snafu(display("\"{}\" returned an error. {}", command, msg))]
CmdError { msg: String, command: String },
#[snafu(display("Command could not parse stdout: \"{}\"", command))]
CmdParseOutputError {
source: std::str::Utf8Error,
command: String,
},
#[snafu(display("Failed to write file: {}", file))]
FileWriteError {
file: String,
source: std::io::Error,
},
#[snafu(display("Failed to read file: {}", file))]
FileReadError {
file: String,
source: std::io::Error,
},
#[snafu(display("Error serializing json: {}", source))]
SerdeError { source: serde_json::Error },
}
impl From<std::io::Error> for PeachConfigError {
fn from(err: std::io::Error) -> PeachConfigError {
PeachConfigError::CmdIoError {
source: err,
command: "unknown".to_string(),
}
}
}
impl From<serde_json::Error> for PeachConfigError {
fn from(err: serde_json::Error) -> PeachConfigError {
PeachConfigError::SerdeError { source: err }
}
}

View File

@ -0,0 +1,119 @@
use regex::Regex;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use std::collections::HashMap;
use std::fs;
use crate::constants::HARDWARE_CONFIG_FILE;
use crate::error::{FileReadError, FileWriteError, PeachConfigError};
use crate::utils::get_output;
use crate::RtcOption;
/// Returns a HashMap<String, String> of all the peach-packages which are currently installed
/// mapped to their version number e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
pub fn get_currently_installed_microservices() -> Result<HashMap<String, String>, PeachConfigError>
{
// gets a list of all packages currently installed with dpkg
let packages = get_output(&["dpkg", "-l"])?;
// this regex matches packages which contain the word peach in them
// and has two match groups
// 1. the first match group gets the package name
// 2. the second match group gets the version number of the package
let re: Regex = Regex::new(r"\S+\s+(\S*peach\S+)\s+(\S+).*\n").unwrap();
// the following iterator, iterates through the captures matched via the regex
// and for each capture, creates a value in the hash map,
// which maps the name of the package, to its version number
// e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
let peach_packages: HashMap<String, String> = re
.captures_iter(&packages)
.filter_map(|cap| {
let groups = (cap.get(1), cap.get(2));
match groups {
(Some(package), Some(version)) => {
Some((package.as_str().to_string(), version.as_str().to_string()))
}
_ => None,
}
})
.collect();
// finally the hashmap of packages and version numbers is returned
Ok(peach_packages)
}
/// Output form of manifest
#[derive(Debug, Serialize, Deserialize)]
pub struct Manifest {
// packages is a map of {package_name: version}
packages: HashMap<String, String>,
hardware: Option<HardwareConfig>,
}
/// The form that hardware configs are saved in when peach-config setup runs successfully
#[derive(Debug, Serialize, Deserialize)]
pub struct HardwareConfig {
// packages is a map of {package_name: version}
i2c: bool,
rtc: Option<RtcOption>,
}
/// Log which hardware settings were configured to a .json file
/// # Arguments
///
/// * `i2c` - a boolean flag, if true i2c will be configured
/// * `rtc` - an optional enum, if supplied indicates which real-time-clock model
/// is being used
///
/// Any error results in a PeachConfigError, otherwise the saved HardwareConfig object
/// is returned.
pub fn save_hardware_config(
i2c: bool,
rtc: Option<RtcOption>,
) -> Result<HardwareConfig, PeachConfigError> {
let hardware_config = HardwareConfig { i2c, rtc };
let json_str = serde_json::to_string(&hardware_config)?;
fs::write(HARDWARE_CONFIG_FILE, json_str).context(FileWriteError {
file: HARDWARE_CONFIG_FILE.to_string(),
})?;
Ok(hardware_config)
}
/// Load the hardware configs that were saved from the last successful run of peach-config setup
///
/// Returns an Ok(Some<HardwareConfg>) containing the configuration if one is found,
/// and returns Ok(None) if no hardware configuration was found.
fn load_hardware_config() -> Result<Option<HardwareConfig>, PeachConfigError> {
// if there is no hardware_config, return None
let hardware_config_exists = std::path::Path::new(HARDWARE_CONFIG_FILE).exists();
if !hardware_config_exists {
Ok(None)
}
// otherwise we load hardware_config from json
else {
let contents = fs::read_to_string(HARDWARE_CONFIG_FILE).context(FileReadError {
file: HARDWARE_CONFIG_FILE.to_string(),
})?;
let hardware_config: HardwareConfig = serde_json::from_str(&contents)?;
Ok(Some(hardware_config))
}
}
/// Outputs a Manifest in json form to stdout
/// which contains the currently installed peach packages
/// as well as the hardware configuration of the last run of peach-config setup.
pub fn generate_manifest() -> Result<(), PeachConfigError> {
let packages = get_currently_installed_microservices()?;
let hardware_config_option = load_hardware_config()?;
let manifest = Manifest {
packages,
hardware: hardware_config_option,
};
let output = serde_json::to_string(&manifest)?;
println!("{}", output);
Ok(())
}

126
peach-config/src/main.rs Normal file
View File

@ -0,0 +1,126 @@
mod constants;
mod error;
mod generate_manifest;
mod setup_networking;
mod setup_peach;
mod setup_peach_deb;
mod update;
mod utils;
use clap::arg_enum;
use log::error;
use serde::{Deserialize, Serialize};
use structopt::StructOpt;
use crate::generate_manifest::generate_manifest;
use crate::setup_peach::setup_peach;
use crate::update::update;
#[derive(StructOpt, Debug)]
#[structopt(
name = "peach-config",
about = "a CLI tool for updating, installing and configuring PeachCloud"
)]
struct Opt {
#[structopt(short, long)]
verbose: bool,
// SUBCOMMANDS
#[structopt(subcommand)]
commands: Option<PeachConfig>,
}
#[derive(StructOpt, Debug)]
#[structopt(name = "peach-config", about = "about")]
enum PeachConfig {
/// Prints json manifest of peach configurations
#[structopt(name = "manifest")]
Manifest,
/// Idempotent setup of PeachCloud
#[structopt(name = "setup")]
Setup(SetupOpts),
/// Updates all PeachCloud microservices
#[structopt(name = "update")]
Update(UpdateOpts),
}
#[derive(StructOpt, Debug)]
struct SetupOpts {
/// Setup i2c configurations
#[structopt(short, long)]
i2c: bool,
/// Optionally select which model of real-time-clock is being used,
/// {ds1307, ds3231}
#[structopt(short, long)]
rtc: Option<RtcOption>,
/// Run peach-config in non-interactive mode
#[structopt(short, long)]
no_input: bool,
/// Use the default en_US.UTF-8 locale for compatability
#[structopt(short, long)]
default_locale: bool,
}
#[derive(StructOpt, Debug)]
pub struct UpdateOpts {
/// Only update other microservices and not peach-config
#[structopt(short, long)]
microservices: bool,
/// Only update peach-config and not other microservices
#[structopt(short, long = "--self")]
self_only: bool,
/// List microservices which are available for updating
#[structopt(short, long)]
list: bool,
}
arg_enum! {
/// enum options for real-time clock choices
#[derive(Debug)]
#[allow(non_camel_case_types)]
#[allow(clippy::enum_variant_names)]
#[derive(Serialize, Deserialize)]
pub enum RtcOption {
DS1307,
DS3231
}
}
fn main() {
// initialize the logger
env_logger::init();
// parse cli arguments
let opt = Opt::from_args();
// switch based on subcommand
if let Some(subcommand) = opt.commands {
match subcommand {
PeachConfig::Setup(cfg) => {
match setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
Ok(_) => {}
Err(err) => {
error!("peach-config encountered an error: {}", err)
}
}
}
PeachConfig::Manifest => match generate_manifest() {
Ok(_) => {}
Err(err) => {
error!(
"peach-config countered an error generating manifest: {}",
err
)
}
},
PeachConfig::Update(opts) => match update(opts) {
Ok(_) => {}
Err(err) => {
error!("peach-config encountered an error during update: {}", err)
}
},
}
}
}

View File

@ -0,0 +1,147 @@
use log::info;
use std::path::Path;
use crate::error::PeachConfigError;
use crate::utils::{cmd, conf};
/// Idempotent script to configure a Debian installation to use
/// systemd-networkd for general networking. The script configures the eth0,
/// wlan0 and ap0 interfaces. This configuration allows switching between
/// wireless client mode (wlan0) and wireless access point mode (ap0)
pub fn configure_networking() -> Result<(), PeachConfigError> {
info!("[ INSTALLING SYSTEM REQUIREMENTS ]");
cmd(&["apt", "install", "-y", "libnss-resolve"])?;
info!("[ SETTING HOST ]");
cmd(&["cp", &conf("hostname"), "/etc/hostname"])?;
cmd(&["cp", &conf("hosts"), "/etc/hosts"])?;
info!("[ DEINSTALLING CLASSIC NETWORKING ]");
cmd(&[
"apt-get",
"autoremove",
"-y",
"ifupdown",
"dhcpcd5",
"isc-dhcp-client",
"isc-dhcp-common",
"rsyslog",
])?;
cmd(&[
"apt-mark",
"hold",
"ifupdown",
"dhcpcd5",
"isc-dhcp-client",
"isc-dhcp-common",
"rsyslog",
"openresolv",
])?;
cmd(&["rm", "-rf", "/etc/network", "/etc/dhcp"])?;
info!("[ SETTING UP SYSTEMD-RESOLVED & SYSTEMD-NETWORKD ]");
cmd(&["apt-get", "autoremove", "-y", "avahi-daemon"])?;
cmd(&["apt-mark", "hold", "avahi-daemon", "libnss-mdns"])?;
cmd(&[
"ln",
"-sf",
"/run/systemd/resolve/stub-resolv.conf",
"/etc/resolv.conf",
])?;
cmd(&[
"systemctl",
"enable",
"systemd-networkd.service",
"systemd-resolved.service",
])?;
info!("[ CREATING INTERFACE FILE FOR WIRED CONNECTION ]");
cmd(&[
"cp",
&conf("network/04-wired.network"),
"/etc/systemd/network/04-wired.network",
])?;
info!("[ SETTING UP WPA_SUPPLICANT AS WIFI CLIENT WITH WLAN0 ]");
// to avoid overwriting previous credentials, only copy file if it doesn't already exist
let wlan0 = "/etc/wpa_supplicant/wpa_supplicant-wlan0.conf";
if !Path::new(wlan0).exists() {
cmd(&["cp", &conf("network/wpa_supplicant-wlan0.conf"), wlan0])?;
cmd(&["chmod", "660", wlan0])?;
cmd(&["chown", "root:netdev", wlan0])?;
}
cmd(&["systemctl", "disable", "wpa_supplicant.service"])?;
cmd(&["systemctl", "enable", "wpa_supplicant@wlan0.service"])?;
info!("[ CREATING BOOT SCRIPT TO COPY NETWORK CONFIGS ]");
cmd(&[
"cp",
&conf("network/copy-wlan.sh"),
"/usr/local/bin/copy-wlan.sh",
])?;
cmd(&["chmod", "770", "/usr/local/bin/copy-wlan.sh"])?;
cmd(&[
"cp",
&conf("network/copy-wlan.service"),
"/etc/systemd/system/copy-wlan.service",
])?;
cmd(&["systemctl", "enable", "copy-wlan.service"])?;
info!("[ SETTING UP WPA_SUPPLICANT AS ACCESS POINT WITH AP0 ]");
cmd(&[
"cp",
&conf("network/wpa_supplicant-ap0.conf"),
"/etc/wpa_supplicant/wpa_supplicant-ap0.conf",
])?;
cmd(&[
"chmod",
"600",
"/etc/wpa_supplicant/wpa_supplicant-ap0.conf",
])?;
info!("[ CONFIGURING INTERFACES ]");
cmd(&[
"cp",
&conf("network/08-wlan0.network"),
"/etc/systemd/network/08-wlan0.network",
])?;
cmd(&[
"cp",
&conf("network/12-ap0.network"),
"/etc/systemd/network/12-ap0.network",
])?;
info!("[ MODIFYING SERVICE FOR ACCESS POINT TO USE AP0 ]");
cmd(&["systemctl", "disable", "wpa_supplicant@ap0.service"])?;
cmd(&[
"cp",
&conf("network/wpa_supplicant@ap0.service"),
"/etc/systemd/system/wpa_supplicant@ap0.service",
])?;
info!("[ SETTING WLAN0 TO RUN AS CLIENT ON STARTUP ]");
cmd(&["systemctl", "enable", "wpa_supplicant@wlan0.service"])?;
cmd(&["systemctl", "disable", "wpa_supplicant@ap0.service"])?;
info!("[ CREATING ACCESS POINT AUTO-DEPLOY SCRIPT ]");
cmd(&[
"cp",
&conf("ap_auto_deploy.sh"),
"/usr/local/bin/ap_auto_deploy",
])?;
info!("[ CONFIGURING ACCESS POINT AUTO-DEPLOY SERVICE ]");
cmd(&[
"cp",
&conf("network/ap-auto-deploy.service"),
"/etc/systemd/system/ap-auto-deploy.service",
])?;
cmd(&[
"cp",
&conf("network/ap-auto-deploy.timer"),
"/etc/systemd/system/ap-auto-deploy.timer",
])?;
info!("[ NETWORKING HAS BEEN CONFIGURED ]");
Ok(())
}

View File

@ -0,0 +1,245 @@
use log::info;
use snafu::ResultExt;
use std::fs;
use crate::error::{FileWriteError, PeachConfigError};
use crate::generate_manifest::save_hardware_config;
use crate::setup_networking::configure_networking;
use crate::setup_peach_deb::setup_peach_deb;
use crate::update::update_microservices;
use crate::utils::{cmd, conf, create_group_if_doesnt_exist, does_user_exist, get_output};
use crate::RtcOption;
/// Idempotent setup of PeachCloud device which sets up networking configuration,
/// configures the peachcloud apt repository, installs system dependencies,
/// installs microservices, and creates necessary system groups and users.
///
/// # Arguments
///
/// * `no_input` - a bool, if true, runs the script without requiring user interaction
/// * `default_locale` - a bool, if true, sets the default locale of the device to en_US.UTF-8
/// * `i2c` - a bool, if true, setup i2c configurations for peach-menu
/// * `rtc` - an optional enum, which if provided indicates the model number of the real-time
/// clock being used
///
/// If any command in the script returns an error (non-zero exit status) a PeachConfigError
/// is returned, otherwise an Ok is returned.
pub fn setup_peach(
no_input: bool,
default_locale: bool,
i2c: bool,
rtc: Option<RtcOption>,
) -> Result<(), PeachConfigError> {
info!("[ RUNNING SETUP PEACH ]");
// list of system users for (micro)services
let users = [
"peach-buttons",
"peach-menu",
"peach-monitor",
"peach-network",
"peach-oled",
"peach-stats",
"peach-web",
];
// Update Pi and install requirements
info!("[ UPDATING OPERATING SYSTEM ]");
// cmd(&["apt-get", "update", "-y"])?;
// cmd(&["apt-get", "upgrade", "-y"])?;
info!("[ INSTALLING SYSTEM REQUIREMENTS ]");
cmd(&[
"apt-get",
"install",
"vim",
"man-db",
"locales",
"iw",
"git",
"python-smbus",
"i2c-tools",
"build-essential",
"curl",
"libnss-resolve",
"mosh",
"sudo",
"pkg-config",
"libssl-dev",
"nginx",
"wget",
"-y",
])?;
// Create system groups first
info!("[ CREATING SYSTEM GROUPS ]");
create_group_if_doesnt_exist("peach")?;
create_group_if_doesnt_exist("gpio-user")?;
// Add the system users
info!("[ ADDING SYSTEM USER ]");
if no_input {
// if no input, then peach user starts with password peachcloud
let default_password = "peachcloud";
let enc_password = get_output(&["openssl", "passwd", "-crypt", default_password])?;
info!("[ CREATING SYSTEM USER WITH DEFAULT PASSWORD ]");
if !(does_user_exist("peach")?) {
cmd(&[
"/usr/sbin/useradd",
"-m",
"-p",
&enc_password,
"-g",
"peach",
"-s",
"/bin/bash",
"peach",
])?;
}
} else {
cmd(&["/usr/sbin/adduser", "peach"])?;
}
cmd(&["usermod", "-aG", "sudo", "peach"])?;
cmd(&["usermod", "-aG", "peach", "peach"])?;
info!("[ CREATING SYSTEM USERS ]");
// Peachcloud microservice users
for user in users {
// Create new system user without home directory and add to `peach` group
cmd(&[
"/usr/sbin/adduser",
"--system",
"--no-create-home",
"--ingroup",
"peach",
user,
])?;
}
info!("[ ASSIGNING GROUP MEMBERSHIP ]");
cmd(&[
"/usr/sbin/usermod",
"-a",
"-G",
"gpio-user",
"peach-buttons",
])?;
cmd(&["/usr/sbin/usermod", "-a", "-G", "netdev", "peach-network"])?;
cmd(&["/usr/sbin/usermod", "-a", "-G", "i2c", "peach-oled"])?;
// Overwrite configuration files
info!("[ CONFIGURING OPERATING SYSTEM ]");
info!("[ CONFIGURING GPIO ]");
cmd(&[
"cp",
&conf("50-gpio.rules"),
"/etc/udev/rules.d/50-gpio.rules",
])?;
if i2c {
info!("[ CONFIGURING I2C ]");
cmd(&["mkdir", "-p", "/boot/firmware/overlays"])?;
cmd(&[
"cp",
&conf("mygpio.dtbo"),
"/boot/firmware/overlays/mygpio.dtbo",
])?;
cmd(&["cp", &conf("config.txt_i2c"), "/boot/firmware/config.txt"])?;
cmd(&["cp", &conf("modules"), "/etc/modules"])?;
}
if let Some(rtc_model) = &rtc {
if i2c {
match rtc_model {
RtcOption::DS1307 => {
info!("[ CONFIGURING DS1307 RTC MODULE ]");
cmd(&[
"cp",
&conf("config.txt_ds1307"),
"/boot/firmware/config.txt",
])?;
}
RtcOption::DS3231 => {
info!("[ CONFIGURING DS3231 RTC MODULE ]");
cmd(&[
"cp",
&conf("config.txt_ds3231"),
"/boot/firmware/config.txt",
])?;
}
}
cmd(&["cp", &conf("modules_rtc"), "/etc/modules"])?;
cmd(&[
"cp",
&conf("activate_rtc.sh"),
"/usr/local/bin/activate_rtc",
])?;
cmd(&[
"cp",
&conf("activate-rtc.service"),
"/etc/systemd/system/activate-rtc.service",
])?;
cmd(&["systemctl", "daemon-reload"])?;
cmd(&["systemctl", "enable", "activate-rtc"])?;
}
}
info!("[ CONFIGURING NGINX ]");
cmd(&[
"cp",
&conf("peach.conf"),
"/etc/nginx/sites-available/peach.conf",
])?;
cmd(&[
"ln",
"-sf",
"/etc/nginx/sites-available/peach.conf",
"/etc/nginx/sites-enabled/",
])?;
if !no_input {
info!("[ CONFIGURING LOCALE ]");
cmd(&["dpkg-reconfigure", "locales"])?;
// this is specified as an argument, so a user can run this script in no-input mode without updating their locale
// if they have already set it
if default_locale {
info!("[ SETTING DEFAULT LOCALE TO en_US.UTF-8 FOR COMPATIBILITY ]");
cmd(&[
"sed",
"-i",
"-e",
"s/// en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/",
"/etc/locale.gen",
])?;
fs::write("/etc/default/locale", "LANG=\"en_US.UTF-8\"").context(FileWriteError {
file: "/etc/default/locale".to_string(),
})?;
cmd(&["dpkg-reconfigure", "--frontend=noninteractive", "locales"])?;
}
}
info!("[ CONFIGURING CONSOLE LOG-LEVEL PRINTING ]");
// TODO: for now commenting this out, because its throwing an error
// cmd(&["sysctl", "-w", "kernel.printk=4 4 1 7"])?;
info!("[ CONFIGURING SUDOERS ]");
cmd(&["mkdir", "-p", "/etc/sudoers.d"])?;
cmd(&["cp", &conf("shutdown"), "/etc/sudoers.d/shutdown"])?;
info!("[ CONFIGURING PEACH APT REPO ]");
setup_peach_deb()?;
info!("[ INSTALLING PEACH MICROSERVICES ]");
update_microservices()?;
info!("[ CONFIGURING NETWORKING ]");
configure_networking()?;
info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]");
save_hardware_config(i2c, rtc)?;
info!("[ PEACHCLOUD SETUP COMPLETE ]");
info!("[ ------------------------- ]");
info!("[ please reboot your device ]");
Ok(())
}

View File

@ -0,0 +1,20 @@
use crate::error::PeachConfigError;
use crate::utils::{cmd, conf};
/// Adds apt.peachcloud.org to the list of debian apt sources and sets the public key appropriately
pub fn setup_peach_deb() -> Result<(), PeachConfigError> {
cmd(&[
"cp",
&conf("peach.list"),
"/etc/apt/sources.list.d/peach.list",
])?;
cmd(&[
"wget",
"-O",
"/tmp/pubkey.gpg",
"http://apt.peachcloud.org/pubkey.gpg",
])?;
cmd(&["apt-key", "add", "/tmp/pubkey.gpg"])?;
cmd(&["rm", "/tmp/pubkey.gpg"])?;
Ok(())
}

View File

@ -0,0 +1,84 @@
use crate::constants::SERVICES;
use crate::error::PeachConfigError;
use crate::utils::{cmd, get_output};
use crate::UpdateOpts;
use serde::{Deserialize, Serialize};
/// Parses update subcommand CLI arguments and calls correct methods.
///
/// If no options are passed, it runs a full update
/// - first updating peach-config
/// - and then re-running peach-config to update all the other microservices
///
/// # Arguments
///
/// * `opts` - an UpdateOpts object containing parsed CLI args
///
/// Any error results in a PeachConfigError, otherwise an Ok is returned.
pub fn update(opts: UpdateOpts) -> Result<(), PeachConfigError> {
if opts.self_only {
run_update_self()
} else if opts.microservices {
update_microservices()
} else if opts.list {
list_available_updates()
}
// otherwise no options were passed, and we do a full update:
// - first updating peach-config
// - and then re-running peach-config to update all the other microservices
else {
run_update_self()?;
cmd(&["/usr/bin/peach-config", "update", "--microservices"])?;
Ok(())
}
}
/// Updates peach-config using apt-get
pub fn run_update_self() -> Result<(), PeachConfigError> {
cmd(&["apt-get", "update"])?;
cmd(&["apt-get", "install", "-y", "peach-config"])?;
Ok(())
}
/// Installs all peach microservices or updates them to the latest version
/// except for peach-config
pub fn update_microservices() -> Result<(), PeachConfigError> {
// update apt
cmd(&["apt-get", "update"])?;
// filter out peach-config from list of services
let services_to_update: Vec<&str> = SERVICES
.to_vec()
.into_iter()
.filter(|&x| x != "peach-config")
.collect();
// apt-get install all services
let mut update_cmd = ["apt-get", "install", "-y"].to_vec();
update_cmd.extend(services_to_update);
cmd(&update_cmd)?;
Ok(())
}
/// Output form of list_available_updates
#[derive(Debug, Serialize, Deserialize)]
pub struct ListAvailableUpdatesOutput {
// packages is a list of package names
upgradeable: Vec<String>,
}
/// Checks if there are any PeachCloud updates available and displays them
pub fn list_available_updates() -> Result<(), PeachConfigError> {
cmd(&["apt-get", "update"])?;
let output = get_output(&["apt", "list", "--upgradable"])?;
let lines = output.split('\n');
// filter down to just lines which are one of the services
let upgradeable: Vec<String> = lines
.into_iter()
.filter(|x| SERVICES.iter().any(|s| x.contains(s)))
.map(|x| x.to_string())
.collect();
let list_available_updates_output = ListAvailableUpdatesOutput { upgradeable };
let output = serde_json::to_string(&list_available_updates_output)?;
println!("{}", output);
Ok(())
}

91
peach-config/src/utils.rs Normal file
View File

@ -0,0 +1,91 @@
use log::{debug, info};
use snafu::ResultExt;
use std::process::{Command, Output};
use crate::constants::CONF;
use crate::error::PeachConfigError;
use crate::error::{CmdIoError, CmdParseOutputError};
/// Utility function which takes in a vector of &str and executes them as a bash command.
/// This function is intended to make scripted bash via rust more ergonomic.
///
/// The first item in the vector is used as the command,
/// and the following items, if supplied, are used as arguments for the command.
///
/// Returns a std::process::Output if successful and a PeachConfigError otherwise.
pub fn cmd(args: &[&str]) -> Result<Output, PeachConfigError> {
info!("command: {:?}", args);
let output = Command::new(args[0])
.args(&args[1..args.len()])
.output()
.context(CmdIoError {
command: format!("{:?}", args),
})?;
debug!("output: {:?}", output);
if output.status.success() {
Ok(output)
} else {
let err_msg = String::from_utf8(output.stderr).expect("failed to read stderr");
Err(PeachConfigError::CmdError {
msg: err_msg,
command: format!("{:?}", args),
})
}
}
/// Utility function which calls calls cmd (above) but converts the Output to a String
/// before returning.
pub fn get_output(args: &[&str]) -> Result<String, PeachConfigError> {
let output = cmd(args)?;
let std_out = std::str::from_utf8(&output.stdout).context(CmdParseOutputError {
command: format!("{:?}", args),
})?;
let mut std_out = std_out.to_string();
if std_out.ends_with('\n') {
std_out.pop();
}
Ok(std_out)
}
/// Takes in a relative path from the conf dir and returns the absolute path to the file
pub fn conf(path: &str) -> String {
let full_path = format!("{}/{}", CONF, path);
full_path
}
/// Creates a linux group with the given name if it doesn't already exist
pub fn create_group_if_doesnt_exist(group: &str) -> Result<(), PeachConfigError> {
let output = Command::new("getent")
.arg("group")
.arg(group)
.output()
.context(CmdIoError {
command: format!("getent group {}", group),
})?;
if output.status.success() {
// then group already exists
Ok(())
} else {
// otherwise create group
cmd(&["/usr/sbin/groupadd", group])?;
Ok(())
}
}
/// Creates a linux user with the given username if it doesn't already exist
pub fn does_user_exist(user: &str) -> Result<bool, PeachConfigError> {
let output = Command::new("getent")
.arg("passwd")
.arg(user)
.output()
.context(CmdIoError {
command: format!("getent passwd {}", user),
})?;
if output.status.success() {
// then user already exists
Ok(true)
} else {
// otherwise user does not exist
Ok(false)
}
}

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

1
peach-dyndns-updater/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

View File

@ -0,0 +1,33 @@
[package]
name = "peach-dyndns-updater"
version = "0.1.6"
authors = ["Max Fowler <mfowler@commoninternet.net>"]
edition = "2018"
description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate."
homepage = "https://opencollective.com/peachcloud"
repository = "https://github.com/peachcloud/peach-dyndns-updater"
readme = "README.md"
license = "AGPL-3.0-only"
publish = false
[package.metadata.deb]
depends = "$auto"
extended-description = """\
Cron job which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate.
"""
maintainer-scripts="debian"
systemd-units = { unit-name = "peach-dyndns-updater" }
assets = [
["target/release/peach-dyndns-updater", "usr/bin/", "755"],
["README.md", "usr/share/doc/peach-dyndns-updater/README", "644"],
]
[badges]
travis-ci = { repository = "peachcloud/peach-dyndns-updater", branch = "main" }
maintenance = { status = "actively-developed" }
[dependencies]
peach-lib = { path = "../peach-lib" }
env_logger = "0.6"
log = "0.4"

View File

@ -0,0 +1,25 @@
# peach-dyndns-updater
This is a debian service which uses a systemd timer and nsudpate to keep the IP address of a dynamic dns record up to date.
It is a simple wrapper for the function peach_lib::dyndns_client::dyndns_update_ip(),
which reads the PeachCloud configurations from disc, and then if it finds
that dyndns is enabled, it uses nsupdate to update the IP address of the configured domain records.
The nsupdate requests use the subdomain, dyndns_server_address and a path to a TSIG key (for authentication),
as provided by the PeachCloud configurations.
## setup
peach-dyndns-udpater is packaged as a debian service, so it can be installed and automatically enabled via:
``` bash
apt-get install peach-dyndns-updater
```
After being installed, it uses a system timer to run the script every five minutes.
You can see that it is running properly by running:
``` bash
journalctl -u peach-dyndns-udpater
```

View File

@ -0,0 +1,12 @@
[Unit]
Description=Systemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate.
[Service]
Type=oneshot
User=peach-dyndns-updater
Group=peach
Environment="RUST_LOG=info"
ExecStart=/usr/bin/peach-dyndns-updater
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,11 @@
[Unit]
Description=Systemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate.
[Timer]
Unit=peach-dyndns-updater.service
OnCalendar=*:0/5
Persistent=true
AccuracySec=30s
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,12 @@
#!/bin/sh
set -e
# create user which peach-dyndns-updater runs as
adduser --quiet --system peach-dyndns-updater
usermod -g peach peach-dyndns-updater
# set permissions
chown peach-dyndns-updater /usr/bin/peach-dyndns-updater
# cargo deb automatically replaces this token below, see https://github.com/mmstick/cargo-deb/blob/master/systemd.md
#DEBHELPER#

View File

@ -0,0 +1,12 @@
use peach_lib::dyndns_client::dyndns_update_ip;
use log::{info};
fn main() {
// initalize the logger
env_logger::init();
info!("Running peach-dyndns-updater");
let result = dyndns_update_ip();
info!("result: {:?}", result);
}

2
peach-lib/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

23
peach-lib/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "peach-lib"
version = "1.2.15"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
jsonrpc-client-core = "0.5"
jsonrpc-client-http = "0.5"
jsonrpc-core = "8.0.1"
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0"
serde_yaml = "0.8"
env_logger = "0.6"
snafu = "0.6"
regex = "1"
chrono = "0.4.19"
rand="0.8.4"
fslock="0.1.6"

37
peach-lib/README.md Normal file
View File

@ -0,0 +1,37 @@
# peach-lib
![Generic badge](https://img.shields.io/badge/version-1.2.9-<COLOR>.svg)
JSON-RPC client library for the PeachCloud ecosystem.
`peach-lib` offers the ability to programmatically interact with the `peach-network`, `peach-oled` and `peach-stats` microservices.
## Overview
The `peach-lib` crate bundles JSON-RPC client code for making requests to the three PeachCloud microservices which expose JSON-RPC servers (`peach-network`, `peach-oled` and `peach-menu`). The full list of available RPC APIs can be found in the READMEs of the respective microservices ([peach-network](https://github.com/peachcloud/peach-network), [peach-oled](https://github.com/peachcloud/peach-oled), [peach-menu](https://github.com/peachcloud/peach-menu)), or in the [developer documentation for PeachCloud](http://docs.peachcloud.org/software/microservices/index.html).
The library also includes a custom error type, `PeachError`, which bundles the underlying error types into three variants: `JsonRpcHttp`, `JsonRpcCore` and `Serde`. When used as the returned error type in a `Result` function response, this allows convenient use of the `?` operator (as illustrated in the example usage code below).
## Usage
Define the dependency in your `Cargo.toml` file:
`peach-lib = { git = "https://github.com/peachcloud/peach-lib", branch = "main" }`
Import the required client from the library:
```rust
use peach_lib::network_client;
```
Call one of the exposed methods:
```rust
network_client::ip("wlan0")?;
```
Further example usage can be found in the [`peach-menu`](https://github.com/peachcloud/peach-menu) code (see `src/states.rs`).
## Licensing
AGPL-3.0

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -0,0 +1,12 @@
[package]
name = "debug"
version = "0.1.0"
authors = ["notplants <mfowler.email@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
peach-lib = { path = "../" }
env_logger = "0.6"
chrono = "0.4.19"

View File

@ -0,0 +1,65 @@
use peach_lib::dyndns_client::{dyndns_update_ip, register_domain, is_dns_updater_online, log_successful_nsupdate, get_num_seconds_since_successful_dns_update };
use peach_lib::password_utils::{verify_password, set_new_password, verify_temporary_password, set_new_temporary_password, send_password_reset};
use peach_lib::config_manager::{add_ssb_admin_id, delete_ssb_admin_id};
use peach_lib::sbot_client;
use std::process;
use chrono::prelude::*;
fn main() {
// initalize the logger
env_logger::init();
//
// println!("Hello, world its debug!");
// let result = set_new_password("password3");
// println!("result: {:?}", result);
//
// let result = verify_password("password1");
// println!("result should be error: {:?}", result);
//
// let result = verify_password("password3");
// println!("result should be ok: {:?}", result);
//
//
// println!("Testing temporary passwords");
// let result = set_new_temporary_password("abcd");
// println!("result: {:?}", result);
//
// let result = verify_temporary_password("password1");
// println!("result should be error: {:?}", result);
//
// let result = verify_temporary_password("abcd");
// println!("result should be ok: {:?}", result);
//
let result = send_password_reset();
println!("send password reset result should be ok: {:?}", result);
// sbot_client::post("hi cat");
// let result = sbot_client::whoami();
// let result = sbot_client::create_invite(50);
// let result = sbot_client::post("is this working");
// println!("result: {:?}", result);
// let result = sbot_client::post("nice we have contact");
// let result = sbot_client::update_pub_name("vermont-pub");
// let result = sbot_client::private_message("this is a private message", "@LZx+HP6/fcjUm7vef2eaBKAQ9gAKfzmrMVGzzdJiQtA=.ed25519");
// println!("result: {:?}", result);
// let result = send_password_reset();
// let result = add_ssb_admin_id("xyzdab");
// println!("result: {:?}", result);
// let result = delete_ssb_admin_id("xyzdab");
// println!("result: {:?}", result);
// let result = delete_ssb_admin_id("ab");
// println!("result: {:?}", result);
//// let result = log_successful_nsupdate();
//// let result = get_num_seconds_since_successful_dns_update();
// let is_online = is_dns_updater_online();
// println!("is online: {:?}", is_online);
//
//// let result = get_last_successful_dns_update();
//// println!("result: {:?}", result);
//// register_domain("newquarter299.dyn.peachcloud.org");
// let result = dyndns_update_ip();
// println!("result: {:?}", result);
}

View File

@ -0,0 +1,143 @@
//! Interfaces for writing and reading PeachCloud configurations, stored in yaml.
//!
//! Different PeachCloud microservices import peach-lib, so that they can share this interface.
//!
//! The configuration file is located at: "/var/lib/peachcloud/config.yml"
use fslock::LockFile;
use serde::{Deserialize, Serialize};
use std::fs;
use crate::error::PeachError;
use crate::error::*;
// main configuration file
pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
// lock file (used to avoid race conditions during config reading & writing)
pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
// we make use of Serde default values in order to make PeachCloud
// robust and keep running even with a not fully complete config.yml
// main type which represents all peachcloud configurations
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct PeachConfig {
#[serde(default)]
pub external_domain: String,
#[serde(default)]
pub dyn_domain: String,
#[serde(default)]
pub dyn_dns_server_address: String,
#[serde(default)]
pub dyn_tsig_key_path: String,
#[serde(default)] // default is false
pub dyn_enabled: bool,
#[serde(default)] // default is empty vector
pub ssb_admin_ids: Vec<String>,
}
// helper functions for serializing and deserializing PeachConfig from disc
fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
// use a file lock to avoid race conditions while saving config
let mut lock = LockFile::open(LOCK_FILE_PATH)?;
lock.lock()?;
let yaml_str = serde_yaml::to_string(&peach_config)?;
fs::write(YAML_PATH, yaml_str).context(WriteConfigError {
file: YAML_PATH.to_string(),
})?;
// unlock file lock
lock.unlock()?;
// return peach_config
Ok(peach_config)
}
pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
let peach_config: PeachConfig;
// if this is the first time loading peach_config, we can create a default here
if !peach_config_exists {
peach_config = PeachConfig {
external_domain: "".to_string(),
dyn_domain: "".to_string(),
dyn_dns_server_address: "".to_string(),
dyn_tsig_key_path: "".to_string(),
dyn_enabled: false,
ssb_admin_ids: Vec::new(),
};
}
// otherwise we load peach config from disk
else {
let contents = fs::read_to_string(YAML_PATH).context(ReadConfigError {
file: YAML_PATH.to_string(),
})?;
peach_config = serde_yaml::from_str(&contents)?;
}
Ok(peach_config)
}
// interfaces for setting specific config values
pub fn set_peach_dyndns_config(
dyn_domain: &str,
dyn_dns_server_address: &str,
dyn_tsig_key_path: &str,
dyn_enabled: bool,
) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.dyn_domain = dyn_domain.to_string();
peach_config.dyn_dns_server_address = dyn_dns_server_address.to_string();
peach_config.dyn_tsig_key_path = dyn_tsig_key_path.to_string();
peach_config.dyn_enabled = dyn_enabled;
save_peach_config(peach_config)
}
pub fn set_external_domain(new_external_domain: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.external_domain = new_external_domain.to_string();
save_peach_config(peach_config)
}
pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
let peach_config = load_peach_config()?;
if !peach_config.external_domain.is_empty() {
Ok(Some(peach_config.external_domain))
} else if !peach_config.dyn_domain.is_empty() {
Ok(Some(peach_config.dyn_domain))
} else {
Ok(None)
}
}
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.dyn_enabled = enabled_value;
save_peach_config(peach_config)
}
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.ssb_admin_ids.push(ssb_id.to_string());
save_peach_config(peach_config)
}
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
let mut ssb_admin_ids = peach_config.ssb_admin_ids;
let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id);
match index_result {
Some(index) => {
ssb_admin_ids.remove(index);
peach_config.ssb_admin_ids = ssb_admin_ids;
save_peach_config(peach_config)
}
None => Err(PeachError::SsbAdminIdNotFound {
id: ssb_id.to_string(),
}),
}
}

View File

@ -0,0 +1,272 @@
//! Client which makes jsonrpc requests via HTTP to the `peach-dyndns-server` API which runs on the peach-vps.
//! Note this is the one service in peach-lib which makes requests to an external server off of the local device.
//!
//! If the requests are successful, dyndns configurations are saved locally on the PeachCloud device,
//! which are then used by the peach-dyndns-cronjob to update the dynamic IP using nsupdate.
//!
//! There is also one function in this file, dyndns_update_ip, which doesn't interact with the jsonrpc server.
//! This function uses nsupdate to actually update dns records directly.
//!
//! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml
//! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key
use crate::config_manager::{load_peach_config, set_peach_dyndns_config};
use crate::error::PeachError;
use crate::error::{
ChronoParseError, DecodeNsUpdateOutputError, DecodePublicIpError, GetPublicIpError,
NsCommandError, SaveDynDnsResultError, SaveTsigKeyError,
};
use chrono::prelude::*;
use jsonrpc_client_core::{expand_params, jsonrpc_client};
use jsonrpc_client_http::HttpTransport;
use log::{debug, info};
use regex::Regex;
use snafu::ResultExt;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::str::ParseBoolError;
/// constants for dyndns configuration
pub const PEACH_DYNDNS_URL: &str = "http://dynserver.dyn.peachcloud.org";
pub const TSIG_KEY_PATH: &str = "/var/lib/peachcloud/peach-dyndns/tsig.key";
pub const PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns";
pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log";
/// helper function which saves dyndns TSIG key returned by peach-dyndns-server to /var/lib/peachcloud/peach-dyndns/tsig.key
pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
// create directory if it doesn't exist
fs::create_dir_all(PEACH_DYNDNS_CONFIG_PATH).context(SaveTsigKeyError {
path: PEACH_DYNDNS_CONFIG_PATH.to_string(),
})?;
// write key text
let mut file = OpenOptions::new()
.write(true)
.create(true)
.open(TSIG_KEY_PATH)
.context(SaveTsigKeyError {
path: TSIG_KEY_PATH.to_string(),
})?;
writeln!(file, "{}", key).context(SaveTsigKeyError {
path: TSIG_KEY_PATH.to_string(),
})?;
Ok(())
}
/// Makes a post request to register a new domain with peach-dyns-server
/// if the post is successful, the domain is registered with peach-dyndns-server
/// a unique TSIG key is returned and saved to disk,
/// and peachcloud is configured to start updating the IP of this domain using nsupdate
pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?;
let http_server = PEACH_DYNDNS_URL;
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(http_server)?;
info!("Creating client for peach-dyndns service.");
let mut client = PeachDynDnsClient::new(transport_handle);
info!("Performing register_domain call to peach-dyndns-server");
let res = client.register_domain(domain).call();
match res {
Ok(key) => {
// save new TSIG key
save_dyndns_key(&key)?;
// save new configuration values
let set_config_result =
set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true);
match set_config_result {
Ok(_) => {
let response = "success".to_string();
Ok(response)
}
Err(err) => Err(err),
}
}
Err(err) => Err(PeachError::JsonRpcClientCore { source: err }),
}
}
/// Makes a post request to check if a domain is available
pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> {
debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?;
let http_server = PEACH_DYNDNS_URL;
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachDynDnsClient::new(transport_handle);
info!("Performing register_domain call to peach-dyndns-server");
let res = client.is_domain_available(domain).call();
info!("res: {:?}", res);
match res {
Ok(result_str) => {
let result: Result<bool, ParseBoolError> = FromStr::from_str(&result_str);
match result {
Ok(result_bool) => Ok(result_bool),
Err(err) => Err(PeachError::PeachParseBoolError { source: err }),
}
}
Err(err) => Err(PeachError::JsonRpcClientCore { source: err }),
}
}
/// Helper function to get public ip address of PeachCloud device.
fn get_public_ip_address() -> Result<String, PeachError> {
// TODO: consider other ways to get public IP address
let output = Command::new("/usr/bin/curl")
.arg("ifconfig.me")
.output()
.context(GetPublicIpError)?;
let command_output = std::str::from_utf8(&output.stdout).context(DecodePublicIpError)?;
Ok(command_output.to_string())
}
/// Reads dyndns configurations from config.yml
/// and then uses nsupdate to update the IP address for the configured domain
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
info!("Running dyndns_update_ip");
let peach_config = load_peach_config()?;
info!(
"Using config:
dyn_tsig_key_path: {:?}
dyn_domain: {:?}
dyn_dns_server_address: {:?}
dyn_enabled: {:?}
",
peach_config.dyn_tsig_key_path,
peach_config.dyn_domain,
peach_config.dyn_dns_server_address,
peach_config.dyn_enabled,
);
if !peach_config.dyn_enabled {
info!("dyndns is not enabled, not updating");
Ok(false)
} else {
// call nsupdate passing appropriate configs
let nsupdate_command = Command::new("/usr/bin/nsupdate")
.arg("-k")
.arg(peach_config.dyn_tsig_key_path)
.arg("-v")
.stdin(Stdio::piped())
.spawn()
.context(NsCommandError)?;
// pass nsupdate commands via stdin
let public_ip_address = get_public_ip_address()?;
info!("found public ip address: {}", public_ip_address);
let ns_commands = format!(
"
server {NAMESERVER}
zone {ZONE}
update delete {DOMAIN} A
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
send",
NAMESERVER = "ns.peachcloud.org",
ZONE = peach_config.dyn_domain,
DOMAIN = peach_config.dyn_domain,
PUBLIC_IP_ADDRESS = public_ip_address,
);
write!(nsupdate_command.stdin.as_ref().unwrap(), "{}", ns_commands).unwrap();
let nsupdate_output = nsupdate_command
.wait_with_output()
.context(NsCommandError)?;
info!("output: {:?}", nsupdate_output);
// We only return a successful result if nsupdate was successful
if nsupdate_output.status.success() {
info!("nsupdate succeeded, returning ok");
// log a timestamp that the update was successful
log_successful_nsupdate()?;
// return true
Ok(true)
} else {
info!("nsupdate failed, returning error");
let err_msg =
String::from_utf8(nsupdate_output.stdout).context(DecodeNsUpdateOutputError)?;
Err(PeachError::NsUpdateError { msg: err_msg })
}
}
}
// Helper function to log a timestamp of the latest successful nsupdate
pub fn log_successful_nsupdate() -> Result<bool, PeachError> {
let now_timestamp = chrono::offset::Utc::now().to_rfc3339();
let mut file = OpenOptions::new()
.write(true)
.create(true)
.open(DYNDNS_LOG_PATH)
.context(SaveDynDnsResultError)?;
write!(file, "{}", now_timestamp).context(SaveDynDnsResultError)?;
Ok(true)
}
/// Helper function to return how many seconds since peach-dyndns-updater successfully ran
pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, PeachError> {
let log_exists = std::path::Path::new(DYNDNS_LOG_PATH).exists();
if !log_exists {
Ok(None)
} else {
let contents =
fs::read_to_string(DYNDNS_LOG_PATH).expect("Something went wrong reading the file");
// replace newline if found
let contents = contents.replace("\n", "");
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).context(ChronoParseError {
msg: "Error parsing dyndns time from latest_result.log".to_string(),
})?;
let current_time: DateTime<Utc> = Utc::now();
let duration = current_time.signed_duration_since(time_ran_dt);
let duration_in_seconds = duration.num_seconds();
Ok(Some(duration_in_seconds))
}
}
/// helper function which returns a true result if peach-dyndns-updater is enabled
/// and has successfully run recently (in the last six minutes)
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
// first check if it is enabled in peach-config
let peach_config = load_peach_config()?;
let is_enabled = peach_config.dyn_enabled;
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
let ran_recently: bool;
match num_seconds_since_successful_update {
Some(seconds) => {
ran_recently = seconds < (60 * 6);
}
// if the value is None, then the last time it ran successfully is unknown
None => {
ran_recently = false;
}
}
// debug log
info!("is_dyndns_enabled: {:?}", is_enabled);
info!("dyndns_ran_recently: {:?}", ran_recently);
// if both are true, then return true
Ok(is_enabled && ran_recently)
}
/// helper function which builds a full dynamic dns domain from a subdomain
pub fn get_full_dynamic_domain(subdomain: &str) -> String {
format!("{}.dyn.peachcloud.org", subdomain)
}
/// helper function to get a dyndns subdomain from a dyndns full domain
pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
let re = Regex::new(r"(.*)\.dyn\.peachcloud\.org").ok()?;
let caps = re.captures(dyndns_full_domain)?;
let subdomain = caps.get(1).map_or("", |m| m.as_str());
Some(subdomain.to_string())
}
// helper function which checks if a dyndns domain is new
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool {
let peach_config = load_peach_config().unwrap();
let previous_dyndns_domain = peach_config.dyn_domain;
dyndns_full_domain != previous_dyndns_domain
}
jsonrpc_client!(pub struct PeachDynDnsClient {
pub fn register_domain(&mut self, domain: &str) -> RpcRequest<String>;
pub fn is_domain_available(&mut self, domain: &str) -> RpcRequest<String>;
});

134
peach-lib/src/error.rs Normal file
View File

@ -0,0 +1,134 @@
//! Basic error handling for the network, OLED, stats and dyndns JSON-RPC clients.
pub use snafu::ResultExt;
use snafu::Snafu;
use std::error;
pub type BoxError = Box<dyn error::Error>;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum PeachError {
#[snafu(display("{}", source))]
JsonRpcHttp { source: jsonrpc_client_http::Error },
#[snafu(display("{}", source))]
JsonRpcClientCore { source: jsonrpc_client_core::Error },
#[snafu(display("{}", source))]
Serde { source: serde_json::error::Error },
#[snafu(display("{}", source))]
PeachParseBoolError { source: std::str::ParseBoolError },
#[snafu(display("{}", source))]
SetConfigError { source: serde_yaml::Error },
#[snafu(display("Failed to read: {}", file))]
ReadConfigError {
source: std::io::Error,
file: String,
},
#[snafu(display("Failed to save: {}", file))]
WriteConfigError {
source: std::io::Error,
file: String,
},
#[snafu(display("Failed to save tsig key: {} {}", path, source))]
SaveTsigKeyError {
source: std::io::Error,
path: String,
},
#[snafu(display("{}", msg))]
NsUpdateError { msg: String },
#[snafu(display("Failed to run nsupdate: {}", source))]
NsCommandError { source: std::io::Error },
#[snafu(display("Failed to get public IP address: {}", source))]
GetPublicIpError { source: std::io::Error },
#[snafu(display("Failed to decode public ip: {}", source))]
DecodePublicIpError { source: std::str::Utf8Error },
#[snafu(display("Failed to decode nsupdate output: {}", source))]
DecodeNsUpdateOutputError { source: std::string::FromUtf8Error },
#[snafu(display("{}", source))]
YamlError { source: serde_yaml::Error },
#[snafu(display("{:?}", err))]
JsonRpcCore { err: jsonrpc_core::Error },
#[snafu(display("Error creating regex: {}", source))]
RegexError { source: regex::Error },
#[snafu(display("Failed to decode utf8: {}", source))]
FromUtf8Error { source: std::string::FromUtf8Error },
#[snafu(display("Encountered Utf8Error: {}", source))]
Utf8Error { source: std::str::Utf8Error },
#[snafu(display("Stdio error: {}: {}", msg, source))]
StdIoError { source: std::io::Error, msg: String },
#[snafu(display("Failed to parse time from {} {}", source, msg))]
ChronoParseError {
source: chrono::ParseError,
msg: String,
},
#[snafu(display("Failed to save dynamic dns success log: {}", source))]
SaveDynDnsResultError { source: std::io::Error },
#[snafu(display("New passwords do not match"))]
PasswordsDoNotMatch,
#[snafu(display("The supplied password was not correct"))]
InvalidPassword,
#[snafu(display("Error saving new password: {}", msg))]
FailedToSetNewPassword { msg: String },
#[snafu(display("Error calling sbotcli: {}", msg))]
SbotCliError { msg: String },
#[snafu(display("Error deleting ssb admin id, id not found"))]
SsbAdminIdNotFound { id: String },
}
impl From<jsonrpc_client_http::Error> for PeachError {
fn from(err: jsonrpc_client_http::Error) -> PeachError {
PeachError::JsonRpcHttp { source: err }
}
}
impl From<jsonrpc_client_core::Error> for PeachError {
fn from(err: jsonrpc_client_core::Error) -> PeachError {
PeachError::JsonRpcClientCore { source: err }
}
}
impl From<serde_json::error::Error> for PeachError {
fn from(err: serde_json::error::Error) -> PeachError {
PeachError::Serde { source: err }
}
}
impl From<serde_yaml::Error> for PeachError {
fn from(err: serde_yaml::Error) -> PeachError {
PeachError::YamlError { source: err }
}
}
impl From<std::io::Error> for PeachError {
fn from(err: std::io::Error) -> PeachError {
PeachError::StdIoError {
source: err,
msg: "".to_string(),
}
}
}
impl From<regex::Error> for PeachError {
fn from(err: regex::Error) -> PeachError {
PeachError::RegexError { source: err }
}
}
impl From<std::string::FromUtf8Error> for PeachError {
fn from(err: std::string::FromUtf8Error) -> PeachError {
PeachError::FromUtf8Error { source: err }
}
}
impl From<std::str::Utf8Error> for PeachError {
fn from(err: std::str::Utf8Error) -> PeachError {
PeachError::Utf8Error { source: err }
}
}
impl From<chrono::ParseError> for PeachError {
fn from(err: chrono::ParseError) -> PeachError {
PeachError::ChronoParseError {
source: err,
msg: "".to_string(),
}
}
}

18
peach-lib/src/lib.rs Normal file
View File

@ -0,0 +1,18 @@
// this is to ignore a clippy warning that suggests
// to replace code with the same code that is already there (possibly a bug)
#![allow(clippy::nonstandard_macro_braces)]
pub mod config_manager;
pub mod dyndns_client;
pub mod error;
pub mod network_client;
pub mod oled_client;
pub mod password_utils;
pub mod sbot_client;
pub mod stats_client;
// re-export error types
pub use jsonrpc_client_core;
pub use jsonrpc_core;
pub use serde_json;
pub use serde_yaml;

View File

@ -0,0 +1,597 @@
//! Perform JSON-RPC calls to the `peach-network` microservice.
//!
//! This module contains a JSON-RPC client and associated data structures for
//! making calls to the `peach-network` microservice. Each RPC has a
//! corresponding method which creates an HTTP transport, makes the call to the
//! RPC microservice and returns the response to the caller. These convenience
//! methods simplify the process of performing RPC calls from other modules.
//!
//! Several helper methods are also included here which bundle multiple client
//! calls to achieve the desired functionality.
// TODO: fix these clippy errors so this allow can be removed
#![allow(clippy::needless_borrow)]
use std::env;
use jsonrpc_client_core::{expand_params, jsonrpc_client};
use jsonrpc_client_http::HttpTransport;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use crate::error::PeachError;
use crate::stats_client::Traffic;
#[derive(Debug, Deserialize, Serialize)]
pub struct AccessPoint {
pub detail: Option<Scan>,
pub signal: Option<i32>,
pub state: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Networks {
pub ssid: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Scan {
pub protocol: String,
pub frequency: String,
pub signal_level: String,
pub ssid: String,
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `activate_ap` method.
pub fn activate_ap() -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.activate_ap().call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `activate_client` method.
pub fn activate_client() -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.activate_client().call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `add_wifi` method.
///
/// # Arguments
///
/// * `ssid` - A string slice containing the SSID of an access point.
/// * `pass` - A string slice containing the password for an access point.
pub fn add(ssid: &str, pass: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.add(ssid, pass).call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `available_networks` method, which returns a list of in-range access points.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
pub fn available_networks(iface: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.available_networks(iface).call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `connect` method, which disables other network connections and enables the
/// connection for the chosen network, identified by ID and interface.
///
/// # Arguments
///
/// * `id` - A string slice containing a network identifier.
/// * `iface` - A string slice containing the network interface identifier.
pub fn connect(id: &str, iface: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.connect(id, iface).call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `id` and `disable` methods.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
/// * `ssid` - A string slice containing the SSID of a network.
pub fn disable(iface: &str, ssid: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
info!("Performing id call to peach-network microservice.");
let id = client.id(&iface, &ssid).call()?;
info!("Performing disable call to peach-network microservice.");
client.disable(&id, &iface).call()?;
let response = "success".to_string();
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `id`, `delete` and `save` methods.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
/// * `ssid` - A string slice containing the SSID of a network.
pub fn forget(iface: &str, ssid: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
info!("Performing id call to peach-network microservice.");
let id = client.id(&iface, &ssid).call()?;
info!("Performing delete call to peach-network microservice.");
// WEIRD BUG: the parameters below are technically in the wrong order:
// it should be id first and then iface, but somehow they get twisted.
// i don't understand computers.
client.delete(&iface, &id).call()?;
info!("Performing save call to peach-network microservice.");
client.save().call()?;
let response = "success".to_string();
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `id` method.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
/// * `ssid` - A string slice containing the SSID of a network.
pub fn id(iface: &str, ssid: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.id(iface, ssid).call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `ip` method.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
pub fn ip(iface: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.ip(iface).call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `ping` method, which serves as a means of determining availability of the
/// microservice (ie. there will be no response if `peach-network` is not
/// running).
pub fn ping() -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.ping().call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `reconfigure` method.
pub fn reconfigure() -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.reconfigure().call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `rssi` method.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
pub fn rssi(iface: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.rssi(iface).call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `rssi_percent` method.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
pub fn rssi_percent(iface: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.rssi_percent(iface).call()?;
Ok(response)
}
/// Helper function to determine if a given SSID already exists in the
/// `wpa_supplicant.conf` file, indicating that network credentials have already
/// been added for that access point. Creates a JSON-RPC client with http
/// transport and calls the `peach-network` `saved_networks` method. Returns a
/// boolean expression inside a Result type.
///
/// # Arguments
///
/// * `ssid` - A string slice containing the SSID of a network.
pub fn saved_ap(ssid: &str) -> std::result::Result<bool, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
// retrieve a list of access points with saved credentials
let saved_aps = match client.saved_networks().call() {
Ok(ssids) => {
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())
.expect("Failed to deserialize saved_networks response");
networks
}
// return an empty vector if there are no saved access point credentials
Err(_) => Vec::new(),
};
// loop through the access points in the list
for network in saved_aps {
// return true if the access point ssid matches the given ssid
if network.ssid == ssid {
return Ok(true);
}
}
// return false if no matches are found
Ok(false)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `saved_networks` method, which returns a list of networks saved in
/// `wpa_supplicant.conf`.
pub fn saved_networks() -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.saved_networks().call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `ssid` method.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
pub fn ssid(iface: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.ssid(iface).call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `state` method.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
pub fn state(iface: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.state(iface).call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `status` method.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
pub fn status(iface: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.status(iface).call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `traffic` method.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
pub fn traffic(iface: &str) -> std::result::Result<Traffic, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
let response = client.traffic(iface).call()?;
let t: Traffic = serde_json::from_str(&response).unwrap();
Ok(t)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-network`
/// `id`, `delete`, `save` and `add` methods. These combined calls allow the
/// saved password for an access point to be updated.
///
/// # Arguments
///
/// * `iface` - A string slice containing the network interface identifier.
/// * `ssid` - A string slice containing the SSID of a network.
/// * `pass` - A string slice containing the password for a network.
pub fn update(iface: &str, ssid: &str, pass: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for network client.");
let transport = HttpTransport::new().standalone()?;
let http_addr =
env::var("PEACH_NETWORK_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachNetworkClient::new(transport_handle);
// get the id of the network
info!("Performing id call to peach-network microservice.");
let id = client.id(&iface, &ssid).call()?;
// delete the old credentials
// WEIRD BUG: the parameters below are technically in the wrong order:
// it should be id first and then iface, but somehow they get twisted.
// i don't understand computers.
info!("Performing delete call to peach-network microservice.");
client.delete(&iface, &id).call()?;
// save the updates to wpa_supplicant.conf
info!("Performing save call to peach-network microservice.");
client.save().call()?;
// add the new credentials
info!("Performing add call to peach-network microservice.");
client.add(ssid, pass).call()?;
// reconfigure wpa_supplicant with latest addition to config
info!("Performing reconfigure call to peach-network microservice.");
client.reconfigure().call()?;
let response = "success".to_string();
Ok(response)
}
jsonrpc_client!(pub struct PeachNetworkClient {
/// JSON-RPC request to activate the access point.
pub fn activate_ap(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to activate the wireless client (wlan0).
pub fn activate_client(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to add credentials for an access point.
pub fn add(&mut self, ssid: &str, pass: &str) -> RpcRequest<String>;
/// JSON-RPC request to list all networks in range of the given interface.
pub fn available_networks(&mut self, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to connect the network for the given interface and ID.
pub fn connect(&mut self, id: &str, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to delete the credentials for the given network from the wpa_supplicant config.
pub fn delete(&mut self, id: &str, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to disable the network for the given interface and ID.
pub fn disable(&mut self, id: &str, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to disconnect the network for the given interface.
//pub fn disconnect(&mut self, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to get the ID for the given interface and SSID.
pub fn id(&mut self, iface: &str, ssid: &str) -> RpcRequest<String>;
/// JSON-RPC request to get the IP address for the given interface.
pub fn ip(&mut self, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to set a new network password for the given interface and ID.
//pub fn modify(&mut self, id: &str, iface: &str, pass: &str) -> RpcRequest<String>;
/// JSON-RPC request to check peach-network availability.
pub fn ping(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to reread the wpa_supplicant config for the given interface.
pub fn reconfigure(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to reconnect WiFi for the given interface.
//pub fn reconnect(&mut self, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to get the average signal strength (dBm) for the given interface.
pub fn rssi(&mut self, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to get the average signal quality (%) for the given interface.
pub fn rssi_percent(&mut self, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to save network configuration updates to file.
pub fn save(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to list all networks saved in `wpa_supplicant.conf`.
pub fn saved_networks(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to get the SSID of the currently-connected network for the given interface.
pub fn ssid(&mut self, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to get the state for the given interface.
pub fn state(&mut self, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to get the status of the given interface.
pub fn status(&mut self, iface: &str) -> RpcRequest<String>;
/// JSON-RPC request to get the network traffic for the given interface.
pub fn traffic(&mut self, iface: &str) -> RpcRequest<String>;
});

View File

@ -0,0 +1,165 @@
use std::env;
use jsonrpc_client_core::{expand_params, jsonrpc_client};
use jsonrpc_client_http::HttpTransport;
use log::{debug, info};
use crate::error::PeachError;
/// Creates a JSON-RPC client with http transport and calls the `peach-oled`
/// `clear` method.
pub fn clear() -> std::result::Result<(), PeachError> {
debug!("Creating HTTP transport for OLED client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_oled service.");
let mut client = PeachOledClient::new(transport_handle);
client.clear().call()?;
debug!("Cleared the OLED display.");
Ok(())
}
/// Creates a JSON-RPC client with http transport and calls the `peach-oled`
/// `draw` method.
///
/// # Arguments
///
/// * `bytes` - A Vec of 8 byte unsigned int.
/// * `width` - A 32 byte unsigned int.
/// * `height` - A 32 byte unsigned int.
/// * `x_coord` - A 32 byte signed int.
/// * `y_coord` - A 32 byte signed int.
pub fn draw(
bytes: Vec<u8>,
width: u32,
height: u32,
x_coord: i32,
y_coord: i32,
) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for OLED client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_oled service.");
let mut client = PeachOledClient::new(transport_handle);
client.draw(bytes, width, height, x_coord, y_coord).call()?;
debug!("Drew to the OLED display.");
Ok("success".to_string())
}
/// Creates a JSON-RPC client with http transport and calls the `peach-oled`
/// `flush` method.
pub fn flush() -> std::result::Result<(), PeachError> {
debug!("Creating HTTP transport for OLED client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_oled service.");
let mut client = PeachOledClient::new(transport_handle);
client.flush().call()?;
debug!("Flushed the OLED display.");
Ok(())
}
/// Creates a JSON-RPC client with http transport and calls the `peach-oled`
/// `ping` method.
pub fn ping() -> std::result::Result<(), PeachError> {
debug!("Creating HTTP transport for OLED client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_oled service.");
let mut client = PeachOledClient::new(transport_handle);
client.ping().call()?;
debug!("Pinged the OLED microservice.");
Ok(())
}
/// Creates a JSON-RPC client with http transport and calls the `peach-oled`
/// `power` method.
///
/// # Arguments
///
/// * `power` - A boolean expression
pub fn power(on: bool) -> std::result::Result<(), PeachError> {
debug!("Creating HTTP transport for OLED client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_oled service.");
let mut client = PeachOledClient::new(transport_handle);
client.power(on).call()?;
debug!("Toggled the OLED display power.");
Ok(())
}
/// Creates a JSON-RPC client with http transport and calls the `peach-oled`
/// `draw` method.
///
/// # Arguments
///
/// * `x_coord` - A 32 byte signed int.
/// * `y_coord` - A 32 byte signed int.
/// * `string` - A reference to a string slice
/// * `font_size` - A reference to a string slice
pub fn write(
x_coord: i32,
y_coord: i32,
string: &str,
font_size: &str,
) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for OLED client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_oled service.");
let mut client = PeachOledClient::new(transport_handle);
client.write(x_coord, y_coord, string, font_size).call()?;
debug!("Wrote to the OLED display.");
Ok("success".to_string())
}
jsonrpc_client!(pub struct PeachOledClient {
/// Creates a JSON-RPC request to clear the OLED display.
pub fn clear(&mut self) -> RpcRequest<String>;
/// Creates a JSON-RPC request to draw to the OLED display.
pub fn draw(&mut self, bytes: Vec<u8>, width: u32, height: u32, x_coord: i32, y_coord: i32) -> RpcRequest<String>;
/// Creates a JSON-RPC request to flush the OLED display.
pub fn flush(&mut self) -> RpcRequest<String>;
/// Creates a JSON-RPC request to ping the OLED microservice.
pub fn ping(&mut self) -> RpcRequest<String>;
/// Creates a JSON-RPC request to toggle the power of the OLED display.
pub fn power(&mut self, on: bool) -> RpcRequest<String>;
/// Creates a JSON-RPC request to write to the OLED display.
pub fn write(&mut self, x_coord: i32, y_coord: i32, string: &str, font_size: &str) -> RpcRequest<String>;
});

View File

@ -0,0 +1,150 @@
use crate::config_manager::{get_peachcloud_domain, load_peach_config};
use crate::error::PeachError;
use crate::error::StdIoError;
use crate::sbot_client;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use snafu::ResultExt;
use std::iter;
use std::process::Command;
/// filepath where nginx basic auth passwords are stored
pub const HTPASSWD_FILE: &str = "/var/lib/peachcloud/passwords/htpasswd";
/// filepath where random temporary password is stored for password resets
pub const HTPASSWD_TEMPORARY_PASSWORD_FILE: &str =
"/var/lib/peachcloud/passwords/temporary_password";
/// the username of the user for nginx basic auth
pub const PEACHCLOUD_AUTH_USER: &str = "admin";
/// Returns Ok(()) if the supplied password is correct,
/// and returns Err if the supplied password is incorrect.
pub fn verify_password(password: &str) -> Result<(), PeachError> {
let output = Command::new("/usr/bin/htpasswd")
.arg("-vb")
.arg(HTPASSWD_FILE)
.arg(PEACHCLOUD_AUTH_USER)
.arg(password)
.output()
.context(StdIoError {
msg: "htpasswd is not installed",
})?;
if output.status.success() {
Ok(())
} else {
Err(PeachError::InvalidPassword)
}
}
/// Checks if the given passwords are valid, and returns Ok() if they are and
/// a PeachError otherwise.
/// Currently this just checks that the passwords are the same,
/// but could be extended to test if they are strong enough.
pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Result<(), PeachError> {
if new_password1 == new_password2 {
Ok(())
} else {
Err(PeachError::PasswordsDoNotMatch)
}
}
/// Uses htpasswd to set a new password for the admin user
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
let output = Command::new("/usr/bin/htpasswd")
.arg("-cb")
.arg(HTPASSWD_FILE)
.arg(PEACHCLOUD_AUTH_USER)
.arg(new_password)
.output()
.context(StdIoError {
msg: "htpasswd is not installed",
})?;
if output.status.success() {
Ok(())
} else {
let err_output = String::from_utf8(output.stderr)?;
Err(PeachError::FailedToSetNewPassword { msg: err_output })
}
}
/// Uses htpasswd to set a new temporary password for the admin user
/// which can be used to reset the permanent password
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
let output = Command::new("/usr/bin/htpasswd")
.arg("-cb")
.arg(HTPASSWD_TEMPORARY_PASSWORD_FILE)
.arg(PEACHCLOUD_AUTH_USER)
.arg(new_password)
.output()
.context(StdIoError {
msg: "htpasswd is not installed",
})?;
if output.status.success() {
Ok(())
} else {
let err_output = String::from_utf8(output.stderr)?;
Err(PeachError::FailedToSetNewPassword { msg: err_output })
}
}
/// Returns Ok(()) if the supplied temp_password is correct,
/// and returns Err if the supplied temp_password is incorrect
pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
// TODO: confirm temporary password has not expired
let output = Command::new("/usr/bin/htpasswd")
.arg("-vb")
.arg(HTPASSWD_TEMPORARY_PASSWORD_FILE)
.arg(PEACHCLOUD_AUTH_USER)
.arg(password)
.output()
.context(StdIoError {
msg: "htpasswd is not installed",
})?;
if output.status.success() {
Ok(())
} else {
Err(PeachError::InvalidPassword)
}
}
/// generates a temporary password and sends it via ssb dm
/// to the ssb id configured to be the admin of the peachcloud device
pub fn send_password_reset() -> Result<(), PeachError> {
// first generate a new random password of ascii characters
let mut rng = thread_rng();
let temporary_password: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(10)
.collect();
// save this string as a new temporary password
set_new_temporary_password(&temporary_password)?;
let domain = get_peachcloud_domain()?;
// then send temporary password as a private ssb message to admin
let mut msg = format!(
"Your new temporary password is: {}
If you are on the same WiFi network as your PeachCloud device you can reset your password \
using this link: http://peach.local/reset_password",
temporary_password
);
// if there is an external domain, then include remote link in message
// otherwise dont include it
let remote_link = match domain {
Some(domain) => {
format!(
"\n\nOr if you are on a different WiFi network, you can reset your password \
using the the following link: {}/reset_password",
domain
)
}
None => "".to_string(),
};
msg += &remote_link;
// finally send the message to the admins
let peach_config = load_peach_config()?;
for ssb_admin_id in peach_config.ssb_admin_ids {
sbot_client::private_message(&msg, &ssb_admin_id)?;
}
Ok(())
}

View File

@ -0,0 +1,109 @@
//! Interfaces for monitoring and configuring go-sbot using sbotcli.
//!
use crate::error::PeachError;
use serde::{Deserialize, Serialize};
use std::process::Command;
pub fn is_sbot_online() -> Result<bool, PeachError> {
let output = Command::new("/usr/bin/systemctl")
.arg("status")
.arg("peach-go-sbot")
.output()?;
let status = output.status;
// returns true if the service had an exist status of 0 (is running)
let is_running = status.success();
Ok(is_running)
}
/// currently go-sbotcli determines where the working directory is
/// using the home directory of th user that invokes it
/// this could be changed to be supplied as CLI arg
/// but for now all sbotcli commands must first become peach-go-sbot before running
/// the sudoers file is configured to allow this to happen without a password
pub fn sbotcli_command() -> Command {
let mut command = Command::new("sudo");
command
.arg("-u")
.arg("peach-go-sbot")
.arg("/usr/bin/sbotcli");
command
}
pub fn post(msg: &str) -> Result<(), PeachError> {
let mut command = sbotcli_command();
let output = command.arg("publish").arg("post").arg(msg).output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCliError {
msg: format!("Error making ssb post: {}", stderr),
})
}
}
#[derive(Serialize, Deserialize)]
struct WhoAmIValue {
id: String,
}
pub fn whoami() -> Result<String, PeachError> {
let mut command = sbotcli_command();
let output = command.arg("call").arg("whoami").output()?;
let text_output = std::str::from_utf8(&output.stdout)?;
let value: WhoAmIValue = serde_json::from_str(text_output)?;
let id = value.id;
Ok(id)
}
pub fn create_invite(uses: i32) -> Result<String, PeachError> {
let mut command = sbotcli_command();
let output = command
.arg("invite")
.arg("create")
.arg("--uses")
.arg(uses.to_string())
.output()?;
let text_output = std::str::from_utf8(&output.stdout)?;
let output = text_output.replace("\n", "");
Ok(output)
}
pub fn update_pub_name(new_name: &str) -> Result<(), PeachError> {
let pub_ssb_id = whoami()?;
let mut command = sbotcli_command();
let output = command
.arg("publish")
.arg("about")
.arg("--name")
.arg(new_name)
.arg(pub_ssb_id)
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCliError {
msg: format!("Error updating pub name: {}", stderr),
})
}
}
pub fn private_message(msg: &str, recipient: &str) -> Result<(), PeachError> {
let mut command = sbotcli_command();
let output = command
.arg("publish")
.arg("post")
.arg("--recps")
.arg(recipient)
.arg(msg)
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCliError {
msg: format!("Error sending ssb private message: {}", stderr),
})
}
}

View File

@ -0,0 +1,198 @@
//! Perform JSON-RPC calls to the `peach-stats` microservice.
//!
//! This module contains a JSON-RPC client and associated data structures for
//! making calls to the `peach-stats` microservice. Each RPC has a corresponding
//! method which creates an HTTP transport, makes the call to the RPC
//! microservice and returns the response to the caller. These convenience
//! methods simplify the process of performing RPC calls from other modules.
use std::env;
use jsonrpc_client_core::{expand_params, jsonrpc_client};
use jsonrpc_client_http::HttpTransport;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use crate::error::PeachError;
#[derive(Debug, Deserialize, Serialize)]
pub struct CpuStat {
pub user: u64,
pub system: u64,
pub idle: u64,
pub nice: u64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct CpuStatPercentages {
pub user: f32,
pub system: f32,
pub idle: f32,
pub nice: f32,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct DiskUsage {
pub filesystem: Option<String>,
pub one_k_blocks: u64,
pub one_k_blocks_used: u64,
pub one_k_blocks_free: u64,
pub used_percentage: u32,
pub mountpoint: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LoadAverage {
pub one: f32,
pub five: f32,
pub fifteen: f32,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct MemStat {
pub total: u64,
pub free: u64,
pub used: u64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Traffic {
pub received: u64,
pub transmitted: u64,
pub rx_unit: Option<String>,
pub tx_unit: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Uptime {
pub secs: u64,
pub nanos: u32,
}
/// Creates a JSON-RPC client with http transport and calls the `peach-stats`
/// `cpu_stats_percent` method.
pub fn cpu_stats_percent() -> std::result::Result<CpuStatPercentages, PeachError> {
debug!("Creating HTTP transport for stats client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_stats service.");
let mut client = PeachStatsClient::new(transport_handle);
let response = client.cpu_stats_percent().call()?;
let c: CpuStatPercentages = serde_json::from_str(&response)?;
Ok(c)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-stats`
/// `disk_usage` method.
pub fn disk_usage() -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for stats client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_stats service.");
let mut client = PeachStatsClient::new(transport_handle);
let response = client.disk_usage().call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-stats`
/// `cpu_stats_percent` method.
pub fn load_average() -> std::result::Result<LoadAverage, PeachError> {
debug!("Creating HTTP transport for stats client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_stats service.");
let mut client = PeachStatsClient::new(transport_handle);
let response = client.load_average().call()?;
let l: LoadAverage = serde_json::from_str(&response)?;
Ok(l)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-stats`
/// `cpu_stats_percent` method.
pub fn mem_stats() -> std::result::Result<MemStat, PeachError> {
debug!("Creating HTTP transport for stats client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_stats service.");
let mut client = PeachStatsClient::new(transport_handle);
let response = client.mem_stats().call()?;
let m: MemStat = serde_json::from_str(&response)?;
Ok(m)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-stats`
/// `ping` method.
pub fn ping() -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for stats client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_stats service.");
let mut client = PeachStatsClient::new(transport_handle);
let response = client.ping().call()?;
Ok(response)
}
/// Creates a JSON-RPC client with http transport and calls the `peach-stats`
/// `uptime` method. If a successful response is returned, the uptime value (in
/// seconds) is converted to minutes before being returned to the caller.
pub fn uptime() -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for stats client.");
let transport = HttpTransport::new().standalone()?;
let http_addr = env::var("PEACH_STATS_SERVER").unwrap_or_else(|_| "127.0.0.1:5113".to_string());
let http_server = format!("http://{}", http_addr);
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach_stats service.");
let mut client = PeachStatsClient::new(transport_handle);
let response = client.uptime().call()?;
let u: Uptime = serde_json::from_str(&response)?;
let minutes = (u.secs / 60).to_string();
Ok(minutes)
}
jsonrpc_client!(pub struct PeachStatsClient {
/// JSON-RPC request to get measurement of current CPU statistics.
pub fn cpu_stats_percent(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to get measurement of current disk usage statistics.
pub fn disk_usage(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to get measurement of current load average statistics.
pub fn load_average(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to get measurement of current memory statistics.
pub fn mem_stats(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to check availability of the `peach-stats` microservice.
pub fn ping(&mut self) -> RpcRequest<String>;
/// JSON-RPC request to get system uptime.
pub fn uptime(&mut self) -> RpcRequest<String>;
});

4
peach-menu/.cargo/config Normal file
View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

2
peach-menu/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
**/*.rs.bk

7
peach-menu/.travis.yml Normal file
View File

@ -0,0 +1,7 @@
language: rust
rust:
- nightly
before_script:
- rustup component add clippy
script:
- cargo clippy -- -D warnings

43
peach-menu/Cargo.toml Normal file
View File

@ -0,0 +1,43 @@
[package]
name = "peach-menu"
version = "0.2.7"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018"
description = "Menu for monitoring and interacting with the PeachCloud device. A state machine which listens for GPIO events (button presses) by subscribing to peach-buttons over websockets and makes JSON-RPC calls to relevant PeachCloud microservices."
homepage = "https://opencollective.com/peachcloud"
repository = "https://github.com/peachcloud/peach-menu"
readme = "README.md"
license = "AGPL-3.0-only"
publish = false
[package.metadata.deb]
depends = "$auto"
extended-description = """\
Menu for monitoring and interacting with the PeachCloud device. \
A state machine which listens for GPIO events (button presses) by \
subscribing to peach-buttons over websockets and makes JSON-RPC calls \
to relevant PeachCloud microservices."""
maintainer-scripts="debian"
systemd-units = { unit-name = "peach-menu" }
assets = [
["target/release/peach-menu", "usr/bin/", "755"],
["README.md", "usr/share/doc/peach-menu/README", "644"],
]
[badges]
travis-ci = { repository = "peachcloud/peach-menu", branch = "master" }
maintenance = { status = "actively-developed" }
[dependencies]
chrono = "0.4"
crossbeam-channel = "0.3"
env_logger = "0.6"
jsonrpc-client-core = "0.5.0"
jsonrpc-client-http = "0.5.0"
jsonrpc-http-server = "11"
jsonrpc-test = "11"
log = "0.4"
peach-lib = { git = "https://github.com/peachcloud/peach-lib", branch = "main" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ws = "0.8"

108
peach-menu/README.md Normal file
View File

@ -0,0 +1,108 @@
# peach-menu
[![Build Status](https://travis-ci.com/peachcloud/peach-menu.svg?branch=master)](https://travis-ci.com/peachcloud/peach-menu) ![Generic badge](https://img.shields.io/badge/version-0.2.7-<COLOR>.svg)
OLED menu microservice module for PeachCloud. A state machine which listens for GPIO events (button presses) by subscribing to `peach-buttons` over websockets and makes [JSON-RPC](https://www.jsonrpc.org/specification) calls to relevant PeachCloud microservices (`peach-network`, `peach-oled`, `peach-stats`).
_Note: This module is a work-in-progress._
### Button Code Mappings
```
0 => Center,
1 => Left,
2 => Right,
3 => Up,
4 => Down,
5 => A,
6 => B
```
### States
```
Home(u8),
Logo,
Network,
NetworkConf(u8),
NetworkMode(u8),
OledPower(u8),
Reboot,
Shutdown,
Stats,
```
### Environment
The JSON-RPC HTTP server address and port for the OLED microservice can be configured with the `PEACH_OLED_SERVER` environment variable:
`export PEACH_OLED_SERVER=127.0.0.1:5000`
When not set, the value defaults to `127.0.0.1:5112`.
Logging is made available with `env_logger`:
`export RUST_LOG=info`
Other logging levels include `debug`, `warn` and `error`.
### Setup
Clone this repo:
`git clone https://github.com/peachcloud/peach-menu.git`
Move into the repo and compile:
`cd peach-menu`
`cargo build --release`
Run the binary:
`./target/target/peach-menu`
_Note: Will currently panic if `peach_buttons` is not running (connection to ws server fails)._
### Debian Packaging
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-menu` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
Install `cargo-deb`:
`cargo install cargo-deb`
Move into the repo:
`cd peach-menu`
Build the package:
`cargo deb`
The output will be written to `target/debian/peach-menu_0.2.1_arm64.deb` (or similar).
Build the package (aarch64):
`cargo deb --target aarch64-unknown-linux-gnu`
Install the package as follows:
`sudo dpkg -i target/debian/peach-menu_0.2.1_arm64.deb`
The service will be automatically enabled and started.
Uninstall the service:
`sudo apt-get remove peach-menu`
Remove configuration files (not removed with `apt-get remove`):
`sudo apt-get purge peach-menu`
### Resources
This work was made much, much easier by the awesome blog post titled [Pretty State Machine Patterns in Rust](https://hoverbear.org/2016/10/12/rust-state-machine-pattern/) by [hoverbear](https://hoverbear.org/about/). Thanks hoverbear!
### Licensing
AGPL-3.0

View File

@ -0,0 +1,15 @@
[Unit]
Description=Menu for monitoring and interacting with the PeachCloud device.
[Service]
Type=simple
User=peach-menu
Environment="RUST_LOG=error"
ExecStart=/usr/bin/peach-menu
Restart=always
Wants=peach-network.service peach-stats.service
Requires=peach-buttons.service peach-oled.service
After=peach-buttons.service peach-oled.service peach-network.service peach-stats.service
[Install]
WantedBy=multi-user.target

82
peach-menu/src/buttons.rs Normal file
View File

@ -0,0 +1,82 @@
use std::process;
use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use serde_json::json;
use ws::{CloseCode, Error, Handler, Handshake, Message, Sender};
#[derive(Debug, Deserialize)]
pub struct Press {
pub button_code: u8,
}
#[derive(Serialize, Deserialize)]
struct ButtonMsg {
jsonrpc: String,
method: String,
params: Vec<u8>,
}
/// Websocket client for `peach_buttons`.
#[derive(Debug)]
pub struct Client<'a> {
pub out: Sender,
pub s: &'a crossbeam_channel::Sender<u8>,
}
impl<'a> Handler for Client<'a> {
/// Sends request to `peach_buttons` to subscribe to emitted events.
fn on_open(&mut self, _: Handshake) -> ws::Result<()> {
info!("Subscribing to peach_buttons microservice over ws.");
let subscribe = json!({
"id":1,
"jsonrpc":"2.0",
"method":"subscribe_buttons"
});
let data = subscribe.to_string();
self.out.send(data)
}
/// Displays JSON-RPC request from `peach_buttons`.
fn on_message(&mut self, msg: Message) -> ws::Result<()> {
info!("Received ws message from peach_buttons.");
// button_code must be extracted from the request and passed to
// state_changer
let m: String = msg.into_text()?;
// distinguish button_press events from other received jsonrpc requests
if m.contains(r"params") {
// serialize msg string into a struct
let bm: ButtonMsg = serde_json::from_str(&m).unwrap_or_else(|err| {
error!("Problem serializing button_code msg: {}", err);
process::exit(1);
});
debug!("Sending button code to state_changer.");
// send the button_code parameter to state_changer
self.s.send(bm.params[0]).unwrap_or_else(|err| {
error!("Problem sending button_code over channel: {}", err);
process::exit(1);
});
}
Ok(())
}
/// Handles disconnection from websocket and displays debug data.
fn on_close(&mut self, code: CloseCode, reason: &str) {
match code {
CloseCode::Normal => {
info!("The client is done with the connection.");
}
CloseCode::Away => {
info!("The client is leaving the site.");
}
CloseCode::Abnormal => {
warn!("Closing handshake failed! Unable to obtain closing status from client.");
}
_ => error!("The client encountered an error: {}", reason),
}
}
fn on_error(&mut self, err: Error) {
error!("The server encountered an error: {:?}", err);
}
}

47
peach-menu/src/lib.rs Normal file
View File

@ -0,0 +1,47 @@
//! # peach-menu
//!
//! `peach_menu` is a collection of utilities and data structures for running
//! a menu state machine. I/O takes place using JSON-RPC 2.0 over websockets,
//! with `peach-buttons` providing GPIO input data and `peach-oled` receiving
//! output data for display.
//!
pub mod buttons;
pub mod state_machine;
mod states;
mod structs;
use std::env;
use crossbeam_channel::unbounded;
use log::{debug, info};
use ws::connect;
use crate::buttons::*;
use crate::state_machine::*;
/// Configures channels for message passing, launches the state machine
/// changer thread and connects to the `peach-buttons` JSON-RPC pubsub
/// service over websockets.
///
/// A Receiver is passed into `state_changer` and the corresponding Sender
/// is passed into the websockets client. This allows the `button_code` to
/// be extracted from the received websocket message and passed to the
/// state machine.
///
pub fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
info!("Starting up.");
debug!("Creating unbounded channel for message passing.");
let (s, r) = unbounded();
debug!("Spawning state-machine thread.");
state_changer(r);
let ws_addr = env::var("PEACH_BUTTONS_SERVER").unwrap_or_else(|_| "127.0.0.1:5111".to_string());
let ws_server = format!("ws://{}", ws_addr);
connect(ws_server, |out| Client { out, s: &s })?;
Ok(())
}

14
peach-menu/src/main.rs Normal file
View File

@ -0,0 +1,14 @@
use std::process;
use log::error;
fn main() {
// initialize the logger
env_logger::init();
// handle errors returned from `run`
if let Err(e) = peach_menu::run() {
error!("Application error: {:?}", e);
process::exit(1);
}
}

View File

@ -0,0 +1,245 @@
use std::{process, thread};
use crossbeam_channel::*;
use log::{error, info, warn};
use peach_lib::error::PeachError;
use peach_lib::oled_client;
use crate::states::*;
#[derive(Debug, Clone, Copy)]
/// The button press events.
pub enum Event {
Center,
Left,
Right,
Down,
Up,
A,
B,
Unknown,
}
#[derive(Debug, PartialEq)]
/// The states of the state machine.
pub enum State {
Home(u8),
Logo,
Network,
NetworkConf(u8),
NetworkMode(u8),
OledPower(u8),
Reboot,
Shutdown,
Stats,
}
/// Initializes the state machine, listens for button events and drives
/// corresponding state changes.
///
/// # Arguments
///
/// * `r` - An unbounded `crossbeam_channel::Receiver` for unsigned 8 byte int.
///
pub fn state_changer(r: Receiver<u8>) {
thread::spawn(move || {
info!("Initializing the state machine.");
let mut state = State::Logo;
match state.run() {
Ok(_) => (),
Err(e) => warn!("State machine error: {:?}", e),
};
loop {
let button_code = r.recv().unwrap_or_else(|err| {
error!("Problem receiving button code from server: {}", err);
process::exit(1);
});
let event = match button_code {
0 => Event::Center,
1 => Event::Left,
2 => Event::Right,
3 => Event::Up,
4 => Event::Down,
5 => Event::A,
6 => Event::B,
_ => Event::Unknown,
};
state = state.next(event);
match state.run() {
Ok(_) => (),
Err(e) => warn!("State machine error: {:?}", e),
};
}
});
}
// 0 - Home
// 1 - Networking
// 2 - System Stats
// 3 - Display Off
// 4 - Reboot
// 5 - Shutdown
// 0 - NetworkConf
// 1 - Client Mode
// 2 - Access Point
// NetworkMode
// 0 - Client Mode
// 1 - Access Point Mode
// OledPower
// 0 - Off
// 1 - On
impl State {
/// Determines the next state based on current state and event.
pub fn next(self, event: Event) -> State {
match (self, event) {
(State::Logo, Event::A) => State::Home(0),
(State::Home(_), Event::B) => State::Logo,
(State::Home(0), Event::Down) => State::Home(2),
(State::Home(0), Event::Up) => State::Home(5),
(State::Home(0), Event::A) => State::Network,
(State::Home(1), Event::Down) => State::Home(2),
(State::Home(1), Event::Up) => State::Home(5),
(State::Home(1), Event::A) => State::Network,
(State::Home(2), Event::Down) => State::Home(3),
(State::Home(2), Event::Up) => State::Home(1),
(State::Home(2), Event::A) => State::Stats,
(State::Home(3), Event::Down) => State::Home(4),
(State::Home(3), Event::Up) => State::Home(2),
(State::Home(3), Event::A) => State::OledPower(0),
(State::Home(4), Event::Down) => State::Home(5),
(State::Home(4), Event::Up) => State::Home(3),
(State::Home(4), Event::A) => State::Reboot,
(State::Home(5), Event::Down) => State::Home(1),
(State::Home(5), Event::Up) => State::Home(4),
(State::Home(5), Event::A) => State::Shutdown,
(State::Network, Event::A) => State::NetworkConf(0),
(State::Network, Event::B) => State::Home(0),
(State::NetworkConf(0), Event::A) => State::NetworkMode(0),
(State::NetworkConf(0), Event::B) => State::Network,
(State::NetworkConf(0), Event::Down) => State::NetworkConf(2),
(State::NetworkConf(0), Event::Up) => State::NetworkConf(2),
(State::NetworkConf(1), Event::A) => State::NetworkMode(0),
(State::NetworkConf(1), Event::B) => State::Network,
(State::NetworkConf(1), Event::Down) => State::NetworkConf(2),
(State::NetworkConf(1), Event::Up) => State::NetworkConf(2),
(State::NetworkConf(2), Event::A) => State::NetworkMode(1),
(State::NetworkConf(2), Event::B) => State::Network,
(State::NetworkConf(2), Event::Down) => State::NetworkConf(1),
(State::NetworkConf(2), Event::Up) => State::NetworkConf(1),
(State::NetworkMode(1), Event::B) => State::Network,
(State::NetworkMode(1), Event::Down) => State::NetworkConf(1),
(State::NetworkMode(1), Event::Up) => State::NetworkConf(1),
(State::NetworkMode(0), Event::B) => State::Network,
(State::NetworkMode(0), Event::Down) => State::NetworkConf(2),
(State::NetworkMode(0), Event::Up) => State::NetworkConf(2),
(State::OledPower(0), _) => State::OledPower(1),
(State::OledPower(1), Event::Down) => State::Home(4),
(State::OledPower(1), Event::Up) => State::Home(2),
(State::OledPower(1), Event::A) => State::OledPower(0),
(State::Stats, Event::B) => State::Home(0),
// return current state if combination is unmatched
(s, _) => s,
}
}
/// Executes state-specific logic for current state.
pub fn run(&self) -> Result<(), PeachError> {
match *self {
// home: root
State::Home(0) => {
info!("State changed to: Home 0.");
state_home(0)?;
}
// home: networking
State::Home(1) => {
info!("State changed to: Home 1.");
state_home(1)?;
}
// home: system stats
State::Home(2) => {
info!("State changed to: Home 2.");
state_home(2)?;
}
// home: display off
State::Home(3) => {
info!("State changed to: Home 3.");
state_home(3)?;
}
// home: reboot
State::Home(4) => {
info!("State changed to: Home 4.");
state_home(4)?;
}
// home: shutdown
State::Home(5) => {
info!("State changed to: Home 5.");
state_home(5)?;
}
// home: unknown
State::Home(_) => {
info!("State changed to: Home _.");
}
State::Logo => {
info!("State changed to: Logo.");
state_logo()?;
}
State::Network => {
info!("State changed to: Network.");
state_network()?;
}
State::NetworkConf(0) => {
info!("State changed to: NetworkConf 0.");
state_network_conf(0)?;
}
State::NetworkConf(1) => {
info!("State changed to: NetworkConf 1.");
state_network_conf(1)?;
}
State::NetworkConf(2) => {
info!("State changed to: NetworkConf 2.");
state_network_conf(2)?;
}
State::NetworkConf(_) => {
info!("State changed to: NetworkConf _.");
}
State::NetworkMode(0) => {
info!("State changed to: NetworkMode 0.");
state_network_mode(0)?;
}
State::NetworkMode(1) => {
info!("State changed to: NetworkMode 1.");
state_network_mode(1)?;
}
State::NetworkMode(_) => {
info!("State changed to: NetworkMode _.");
}
State::OledPower(0) => {
info!("State changed to: OledPower 0.");
oled_client::power(false)?;
}
State::OledPower(1) => {
info!("State changed to: OledPower 1.");
oled_client::power(true)?;
}
State::OledPower(_) => {
info!("State changed to: OledPower _.");
}
State::Reboot => {
info!("State changed to: Reboot.");
state_reboot()?;
}
State::Shutdown => {
info!("State changed to: Shutdown.");
state_shutdown()?;
}
State::Stats => {
info!("State changed to: Stats.");
state_stats()?;
}
}
Ok(())
}
}

337
peach-menu/src/states.rs Normal file
View File

@ -0,0 +1,337 @@
use std::{process, thread, time};
use chrono::{DateTime, Local};
use log::info;
use peach_lib::error::PeachError;
use peach_lib::network_client;
use peach_lib::oled_client;
use peach_lib::stats_client;
pub fn state_network_mode(mode: u8) -> Result<(), PeachError> {
match mode {
0 => {
oled_client::clear()?;
oled_client::write(24, 16, "ACTIVATING", "6x8")?;
oled_client::write(24, 27, "WIRELESS", "6x8")?;
oled_client::write(24, 38, "CONNECTION...", "6x8")?;
oled_client::flush()?;
network_client::activate_client()?;
oled_client::clear()?;
oled_client::write(0, 0, "> Client mode", "6x8")?;
oled_client::write(0, 9, " Access point mode", "6x8")?;
oled_client::flush()?;
Ok(())
}
1 => {
oled_client::clear()?;
oled_client::write(27, 16, "DEPLOYING", "6x8")?;
oled_client::write(27, 27, "ACCESS", "6x8")?;
oled_client::write(27, 38, "POINT...", "6x8")?;
oled_client::flush()?;
network_client::activate_ap()?;
oled_client::clear()?;
oled_client::write(0, 0, " Client mode", "6x8")?;
oled_client::write(0, 9, "> Access point mode", "6x8")?;
oled_client::flush()?;
Ok(())
}
_ => Ok(()),
}
}
pub fn state_home(selected: u8) -> Result<(), PeachError> {
// match on `selected`
match selected {
// Home: root
0 => {
let dt: DateTime<Local> = Local::now();
let t = format!("{}", dt.time().format("%H:%M"));
oled_client::clear()?;
oled_client::write(96, 0, &t, "6x8")?;
oled_client::write(0, 0, "PeachCloud", "6x8")?;
oled_client::write(0, 18, "> Networking", "6x8")?;
oled_client::write(0, 27, " System Stats", "6x8")?;
oled_client::write(0, 36, " Display Off", "6x8")?;
oled_client::write(0, 45, " Reboot", "6x8")?;
oled_client::write(0, 54, " Shutdown", "6x8")?;
oled_client::write(100, 54, "v0.2", "6x8")?;
oled_client::flush()?;
Ok(())
}
// Home: networking
1 => {
oled_client::write(0, 18, "> ", "6x8")?;
oled_client::write(0, 27, " ", "6x8")?;
oled_client::write(0, 36, " ", "6x8")?;
oled_client::write(0, 45, " ", "6x8")?;
oled_client::write(0, 54, " ", "6x8")?;
oled_client::flush()?;
Ok(())
}
// Home: system stats
2 => {
oled_client::write(0, 18, " ", "6x8")?;
oled_client::write(0, 27, "> ", "6x8")?;
oled_client::write(0, 36, " ", "6x8")?;
oled_client::write(0, 45, " ", "6x8")?;
oled_client::write(0, 54, " ", "6x8")?;
oled_client::flush()?;
Ok(())
}
// Home: display off
3 => {
oled_client::write(0, 18, " ", "6x8")?;
oled_client::write(0, 27, " ", "6x8")?;
oled_client::write(0, 36, "> ", "6x8")?;
oled_client::write(0, 45, " ", "6x8")?;
oled_client::write(0, 54, " ", "6x8")?;
oled_client::flush()?;
Ok(())
}
// Home: reboot
4 => {
oled_client::write(0, 18, " ", "6x8")?;
oled_client::write(0, 27, " ", "6x8")?;
oled_client::write(0, 36, " ", "6x8")?;
oled_client::write(0, 45, "> ", "6x8")?;
oled_client::write(0, 54, " ", "6x8")?;
oled_client::flush()?;
Ok(())
}
// Home: shutdown
5 => {
oled_client::write(0, 18, " ", "6x8")?;
oled_client::write(0, 27, " ", "6x8")?;
oled_client::write(0, 36, " ", "6x8")?;
oled_client::write(0, 45, " ", "6x8")?;
oled_client::write(0, 54, "> ", "6x8")?;
oled_client::flush()?;
Ok(())
}
// outlier
_ => Ok(()),
}
}
pub fn state_logo() -> Result<(), PeachError> {
let bytes = PEACH_LOGO.to_vec();
oled_client::clear()?;
oled_client::draw(bytes, 64, 64, 32, 0)?;
oled_client::flush()?;
Ok(())
}
pub fn state_network() -> Result<(), PeachError> {
let status = match network_client::state("wlan0") {
Ok(state) => state,
Err(_) => "Error".to_string(),
};
match status.as_ref() {
// wlan0 is up or dormant
// Network: Client mode
"up" | "dormant" => {
let show_status = format!("STATUS {}", status);
let ip = match network_client::ip("wlan0") {
Ok(ip) => ip,
Err(_) => "x.x.x.x".to_string(),
};
let show_ip = format!("IP {}", ip);
let ssid = match network_client::ssid("wlan0") {
Ok(ssid) => ssid,
Err(_) => "Not connected".to_string(),
};
let show_ssid = format!("NETWORK {}", ssid);
let rssi = match network_client::rssi("wlan0") {
Ok(rssi) => rssi,
Err(_) => "_".to_string(),
};
let show_rssi = format!("SIGNAL {}dBm", rssi);
let config = "> Configuration";
oled_client::clear()?;
oled_client::write(0, 0, "MODE Client", "6x8")?;
oled_client::write(0, 9, &show_status, "6x8")?;
oled_client::write(0, 18, &show_ssid, "6x8")?;
oled_client::write(0, 27, &show_ip, "6x8")?;
oled_client::write(0, 36, &show_rssi, "6x8")?;
oled_client::write(0, 54, config, "6x8")?;
oled_client::flush()?;
Ok(())
}
// wlan0 is down
// Network: AP mode
"down" => {
let status = match network_client::state("ap0") {
Ok(state) => state,
Err(_) => "Error".to_string(),
};
let show_status = format!("STATUS {}", status);
let ip = match network_client::ip("ap0") {
Ok(ip) => ip,
Err(_) => "x.x.x.x".to_string(),
};
let show_ip = format!("IP {}", ip);
let ssid = "peach";
let show_ssid = format!("NETWORK {}", ssid);
let config = "> Configuration";
oled_client::clear()?;
oled_client::write(0, 0, "MODE Access Point", "6x8")?;
oled_client::write(0, 9, &show_status, "6x8")?;
oled_client::write(0, 18, &show_ssid, "6x8")?;
oled_client::write(0, 27, &show_ip, "6x8")?;
oled_client::write(0, 54, config, "6x8")?;
oled_client::flush()?;
Ok(())
}
// outlier
// TODO: account for iface states other than 'up' and 'down'
_ => Ok(()),
}
}
pub fn state_network_conf(selected: u8) -> Result<(), PeachError> {
// match on `selected`
match selected {
// NetworkConf: root
0 => {
oled_client::clear()?;
oled_client::write(0, 0, "> Client Mode", "6x8")?;
oled_client::write(0, 9, " Access Point Mode", "6x8")?;
oled_client::flush()?;
Ok(())
}
// NetworkConf: client
1 => {
oled_client::write(0, 0, "> ", "6x8")?;
oled_client::write(0, 9, " ", "6x8")?;
oled_client::flush()?;
Ok(())
}
// NetworkConf: ap
2 => {
oled_client::write(0, 0, " ", "6x8")?;
oled_client::write(0, 9, "> ", "6x8")?;
oled_client::flush()?;
Ok(())
}
// outlier
_ => Ok(()),
}
}
pub fn state_reboot() -> Result<(), PeachError> {
oled_client::clear()?;
oled_client::write(27, 16, "REBOOTING", "6x8")?;
oled_client::write(27, 27, "DEVICE...", "6x8")?;
oled_client::flush()?;
let three_secs = time::Duration::from_millis(3000);
thread::sleep(three_secs);
oled_client::power(false)?;
info!("Rebooting device");
process::Command::new("sudo")
.arg("/sbin/shutdown")
.arg("-r")
.arg("now")
.output()
.expect("Failed to reboot");
Ok(())
}
pub fn state_shutdown() -> Result<(), PeachError> {
oled_client::clear()?;
oled_client::write(27, 16, "SHUTTING", "6x8")?;
oled_client::write(27, 27, "DOWN", "6x8")?;
oled_client::write(27, 38, "DEVICE...", "6x8")?;
oled_client::flush()?;
let three_secs = time::Duration::from_millis(3000);
thread::sleep(three_secs);
oled_client::power(false)?;
info!("Shutting down device");
process::Command::new("sudo")
.arg("/sbin/shutdown")
.arg("now")
.output()
.expect("Failed to shutdown");
Ok(())
}
pub fn state_stats() -> Result<(), PeachError> {
let cpu = stats_client::cpu_stats_percent()?;
let cpu_stats = format!(
"CPU {} us {} sy {} id",
cpu.user.round(),
cpu.system.round(),
cpu.idle.round()
);
let mem = stats_client::mem_stats()?;
let mem_stats = format!("MEM {}MB f {}MB u", mem.free / 1024, mem.used / 1024);
let load = stats_client::load_average()?;
let load_stats = format!("LOAD {} {} {}", load.one, load.five, load.fifteen);
let uptime = stats_client::uptime()?;
let uptime_stats = format!("UPTIME {} mins", uptime);
let traffic = network_client::traffic("wlan0")?;
let rx = traffic.received / 1024 / 1024;
let rx_stats = format!("DATA RX {}MB", rx);
let tx = traffic.transmitted / 1024 / 1024;
let tx_stats = format!("DATA TX {}MB", tx);
oled_client::clear()?;
oled_client::write(0, 0, &cpu_stats, "6x8")?;
oled_client::write(0, 9, &mem_stats, "6x8")?;
oled_client::write(0, 18, &load_stats, "6x8")?;
oled_client::write(0, 27, &uptime_stats, "6x8")?;
oled_client::write(0, 36, &rx_stats, "6x8")?;
oled_client::write(0, 45, &tx_stats, "6x8")?;
oled_client::flush()?;
Ok(())
}
const PEACH_LOGO: [u8; 512] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 0, 0, 0, 0, 0,
0, 3, 248, 14, 0, 0, 7, 0, 0, 15, 252, 63, 128, 0, 31, 192, 0, 63, 254, 127, 192, 0, 63, 224,
0, 127, 255, 127, 224, 0, 127, 240, 0, 63, 255, 255, 128, 0, 255, 240, 0, 31, 255, 255, 192,
31, 255, 248, 0, 15, 252, 64, 112, 63, 255, 248, 0, 24, 240, 96, 24, 127, 255, 255, 192, 48, 0,
48, 12, 127, 255, 255, 224, 96, 0, 24, 12, 255, 255, 255, 240, 64, 0, 8, 6, 255, 255, 255, 248,
64, 0, 12, 2, 255, 255, 255, 252, 192, 0, 4, 2, 255, 227, 255, 252, 192, 0, 4, 2, 127, 128,
255, 252, 128, 0, 4, 2, 63, 0, 127, 252, 128, 0, 6, 2, 126, 0, 63, 252, 128, 0, 6, 3, 252, 0,
63, 248, 128, 0, 6, 6, 0, 0, 1, 240, 192, 0, 6, 12, 0, 0, 0, 192, 192, 0, 6, 8, 0, 0, 0, 96,
64, 0, 4, 24, 0, 0, 0, 32, 64, 0, 4, 24, 0, 0, 0, 48, 96, 0, 4, 16, 0, 0, 0, 16, 32, 0, 4, 16,
0, 0, 0, 16, 48, 0, 12, 24, 0, 0, 0, 16, 24, 0, 8, 56, 0, 0, 0, 16, 12, 0, 24, 104, 0, 0, 0,
48, 7, 0, 0, 204, 0, 0, 0, 96, 1, 128, 3, 134, 0, 0, 0, 192, 0, 240, 6, 3, 128, 0, 1, 128, 0,
63, 28, 1, 255, 255, 255, 0, 0, 3, 240, 0, 31, 255, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];

53
peach-menu/src/structs.rs Normal file
View File

@ -0,0 +1,53 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct CpuStat {
pub user: u64,
pub system: u64,
pub idle: u64,
pub nice: u64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct CpuStatPercentages {
pub user: f32,
pub system: f32,
pub idle: f32,
pub nice: f32,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct DiskUsage {
pub filesystem: Option<String>,
pub one_k_blocks: u64,
pub one_k_blocks_used: u64,
pub one_k_blocks_free: u64,
pub used_percentage: u32,
pub mountpoint: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LoadAverage {
pub one: f32,
pub five: f32,
pub fifteen: f32,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct MemStat {
pub total: u64,
pub free: u64,
pub used: u64,
}
#[derive(Debug, Deserialize)]
pub struct Traffic {
pub received: u64,
pub transmitted: u64,
}
#[derive(Debug, Deserialize)]
pub struct Uptime {
pub secs: u64,
pub nanos: u32,
}

81
peach-menu/view_mockups Normal file
View File

@ -0,0 +1,81 @@
--------------------- ---------------------
|PeachCloud 21:27| |PeachCloud 11:11|
| | | |
|> Networking | | Networking |
| System Stats | |> System Stats |
| Preferences | | Preferences |
| Shutdown | | Shutdown |
| v0.1 | | v0.1 |
--------------------- ---------------------
[ A ] [ A ]
--------------------- ---------------------
|MODE Client | |CPU % 0.4 0.2 99.7 |
|STATUS Active | |LOAD 0.14 0.28 0.12 |
|NETWORK Home | |MEM MB 918 355 95 |
|IP 192.168.0.103 | |UPTIME 7532 HR |
|SIGNAL -48 dBm | | |
| | | |
| > Configuration | | |
--------------------- ---------------------
[ A ]
---------------------
| > Activate AP |
| Scan for Networks |
| Forget Network |
| |
| |
| |
| |
---------------------
-----
--------------------- ---------------------
|PeachCloud 21:27| |PeachCloud 11:11|
| | | |
|> Networking | | Networking |
| System Stats | |> System Stats |
| Shutdown | | Shutdown |
| | | |
|A - Select | B - Help| |A - Select | B - Help|
--------------------- ---------------------
[ A ] [ A ]
--------------------- ---------------------
|MODE Client | |SDA0 |
|STATUS Active | | USED 11.1 GB |
|NETWORK Home | | FREE 6.4 GB |
|IP 192.168.0.103 | |SDA1 |
|SIGNAL -48 dBm | | USED 1.2 GB |
| | | FREE 21.7 |
|A - Config | B - Back| |A - Config | B - Back|
--------------------- ---------------------
[ A ]
---------------------
|> Activate AP |
| Scan for Networks |
| Forget Network |
| |
| |
| |
|A - Config | B - Back|
---------------------
-----
---------------------
|BATTERY 83 % |
|CPU TEMP 45 C |
|UPTIME 1d 12m 33s |
| |
| |
| |
|A - Config | B - Back|
---------------------

4
peach-meta/.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.webm filter=lfs diff=lfs merge=lfs -text
*.svg filter=lfs diff=lfs merge=lfs -text

View File

@ -0,0 +1,146 @@
# ButtCloud :partly_sunny: :rainbow:
_an April 2018 #ssbc-grants proposal_
i'd like to work on better pub infrastructure and a hosted pub-as-a-service product.
## team
me! :smiley_cat:
## goals
- we build an [open source business](http://blog.dinosaur.is/workers-of-open-source-unite/) on Scuttlebutt!
- a pub is [your personal cloud device](%Gwqklkj0b2CBT5tPiz5170NWsPp3xiuLbOImEaG/e+4=.sha256) that is always available and publicly accessible
- a pub should be affordable (start pricing at ~$7/month per pub, try to get to ~$1/month per pub)
- we support open source infrastructure providers, like [OVH](https://www.ovh.com/world/public-cloud/) and [Catalyst](http://catalyst.net.nz/catalyst-cloud)
- the infrastructure code should easy for others to contribute and maintain
## the story so far
pubs are important ([1](%f6ZRXO2t0rwUw/lq5FpWCHtuHc9406Q37TB+lF9bUbc=.sha256), [2](https://writtenby.adriengiboire.com/2018/03/14/my-first-week-experience-with-scuttlebutt-and-patchwork/), [3](https://twitter.com/nicolasini/status/974364249219727360)), but [public pubs are a stop-gap we've used for too long](%MZCHPVkh8sNhTsevWgSVXNlL6dYgSnzBvB3hJcJZ/7k=.sha256), we need [private pubs for everyone](%Gwqklkj0b2CBT5tPiz5170NWsPp3xiuLbOImEaG/e+4=.sha256)!
[over the holidays](%1TVZigDql9VAQaZZVX/QegKan18urBuXQikOWE1uTMk=.sha256), i got maybe 80% of the way towards automated Scuttlebutt pub infrastructure using [Salt Stack](https://docs.saltstack.com/), hosted on [Open Stack](https://docs.openstack.org) using [OVH public cloud](https://www.ovh.com/world/public-cloud/). i've also been able to achieve _mostly_ reliable pubs using an [`ssb-pub`](https://github.com/ahdinosaur/ssb-pub) Docker image.
i want to throw away all the [Salt Stack](https://docs.saltstack.com/) work i did and start over with [Docker Swarm](https://docs.docker.com/engine/swarm/).
i expect this will take [longer than a month of full-time work to complete](%9ZzHJ2F0MHncqLLInC47Tp/OuHEUcHyRfWYierUpUKc=.sha256).
## sub-projects
there are three main sub-projects:
- provider service
- hub swarm
- pub service
## architecture
- provider service
- web app
- join
- land
- sign in
- create pub
- pay for pub
- start pub service
- monitor
- land
- sign in
- view pub
- see stats
- command
- land
- sign in
- view pub
- run command
- web api
- users
- id
- name
- email
- pub
- bots
- id
- userId
- name
- status (up, down, none)
- stats
- stream Docker stats
- commands
- relay commands to pub services
- orchestrator
- on schedule, check what pubs are up
- have 1 pub per 1 GB memory, 1 hub per 15 GB memory
- queue worker jobs to ensure correct swarm
- payment
- products
- plans
- customers
- subscriptions
- swarm worker
- manage hub [machines](https://docs.docker.com/machine/drivers/openstack/)
- create hub
- destroy hub
- manage pub [services](https://docs.docker.com/engine/swarm/swarm-tutorial/deploy-service/)
- ensure pub service is up
- ensure pub service is down
- mailer worker
- pub service
## stack
- provider service
- web api
- [@feathersjs/socketio](https://github.com/feathersjs/socketio)
- [@feathersjs/authentication](https://github.com/feathersjs/authentication)
- [@feathersjs/authentication-jwt](https://github.com/feathersjs/authentication-jwt)
- [feathers-stripe](https://github.com/feathersjs-ecosystem/feathers-stripe)
- [node-resque](https://github.com/taskrabbit/node-resque)
- [docker-remote-api](https://github.com/mafintosh/docker-remote-api)
- web app
- [next.js](https://github.com/zeit/next.js/)
- [ramda](http://ramdajs.com/docs/)
- [@feathersjs/socketio-client](https://github.com/feathersjs/socketio-client)
- [@feathersjs/authentication-client](https://github.com/feathersjs/authentication-client)
- [react](https://facebook.github.io/react)
- [react-hyperscript](https://github.com/mlmorg/react-hyperscript)
- [recompose](https://github.com/acdlite/recompose)
- [fela](https://github.com/rofrischmann/fela)
- [material-ui](https://material-ui.com/)
- [react-stripe-elements](https://github.com/stripe/react-stripe-elements)
- swarm worker
- [node-resque](https://github.com/taskrabbit/node-resque)
- [docker-remote-api](https://github.com/mafintosh/docker-remote-api)
- mailer worker
- [node-resque](https://github.com/taskrabbit/node-resque)
- [nodemailer](https://github.com/nodemailer/nodemailer)
- third-party: [sendgrid](https://sendgrid.com/)
- dev tool: [maildev](https://github.com/djfarrelly/maildev)
- [pub service](http://github.com/ahdinosaur/ssb-pub)
- [scuttlebot](https://github.com/ssbc/scuttlebot)
- [ssb-viewer](%MeCTQrz9uszf9EZoTnKCeFeIedhnKWuB3JHW2l1g9NA=.sha256)
- [git-ssb-web](%q5d5Du+9WkaSdjc8aJPZm+jMrqgo0tmfR+RcX5ZZ6H4=.sha256)
## roadmap
_rough draft, subject to change_
- upgrade pub service
- update `ssb-pub` to use docker-compose (so we can host multiple Scuttlebutt processes in the same service)
- add `ssb-viewer`
- add `git-ssb-web`
- prototype hub swarm
- setup single "hub manager" to be docker swarm manager
- run mock provider service on manager
- setup many "hub worker"s to be docker swarm workers
- create provider service
- upgrade mock provider service to include postgres and redis databases
- scaffold web api, web app, swarm worker, and mailer worker
- implement provider web app user flows
- automate swarm
- implement swarm orchestration functionality
- start business
- understand costs, determine prices, forecast profit/loss
- decide on company jurisdiction and legal structure
- [copy](https://getterms.io/) or create (with help) a Terms of Service & Privacy Policy

Some files were not shown because too many files have changed in this diff Show More