#!/usr/bin/env bash # pilpil client installer v0.1 # # https://sharats.me/posts/shell-script-best-practices/ set -o errexit set -o nounset set -o pipefail if [[ "${TRACE-0}" == "1" ]]; then set -o xtrace fi #l10n . gettext.sh export TEXTDOMAINDIR="$PWD" export TEXTDOMAIN="$(basename "$0")" # Change to script dir cd "$(dirname "$0")" # Colored output #~ set +x bold=$(tput bold) function red(){ echo -e "${bold}\x1B[31m$1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[31m $($2) \x1B[0m" fi } function green(){ echo -e "${bold}\x1B[32m$1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[32m $($2) \x1B[0m" fi } function yellow(){ echo -e "${bold}\x1B[33m$1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[33m $($2) \x1B[0m" fi } # Display help if -h(elpf) used if [[ "${1-}" =~ ^-*h(elp)?$ ]]; then echo -e "`gettext \"Usage: ./pilpil-server.sh path_to_device This script will setup as much pilpil-clients as needed.\"`" exit fi # Set dry run if -d(ry) used DRY_RUN=0 if [[ "${1-}" =~ ^-*d(ry)?$ ]]; then DRY_RUN=1 yellow "`gettext \"!!! Running in dry mode. No modifications will be made.\n\"`" fi HOTSPOT=0 if [[ "${1-}" =~ ^-*w(ap)?$ ]]; then HOTSPOT=1 yellow "`gettext \"We'll create a Wireless AP with nmcli'.\n\"`" fi # Options # Device block to write on if [[ "$DRY_RUN" == 0 ]] && [[ "$HOTSPOT" == 0 ]]; then SDCARD="${1}" elif [[ "$DRY_RUN" == 1 ]] && [[ "$HOTSPOT" == 1 ]]; then SDCARD="${3-}" else SDCARD="${2-}" fi # TODO : accomodate for devices block with name mmcblk0p1|p2 if [[ "$SDCARD" == "" ]] || [[ ! -e "$SDCARD" ]] then red "`gettext \"Please specify an existing device block for your sd-card, e.g: '/dev/sda'.\"`" >&2 exit 0 fi DD_BS="256K" DISK_IMAGE="$HOME/niels/imgs/2023-01-05-pilpil.img.xz" if [[ ! -f "$DISK_IMAGE" ]]; then red "`gettext \"Disk image not found, aborting...\"`" >&2 exit 0 fi CONFIG_DIR="$HOME/niels/pilpil-server" if [[ ! -d "$CONFIG_DIR" ]]; then red "`gettext \"Config directory not found, aborting...\"`" >&2 exit 0 fi # Generate random http auth secret HTTP_SECRET=$(openssl rand -base64 12) PI_USER="pil" BOOT_MOUNT="/run/media/$USER/boot" ROOTFS_MOUNT="/run/media/$USER/rootfs" #~ LOCAL_MEDIA_DIR="$HOME/Videos" LOCAL_MEDIA_DIR="$HOME/niels/medias" if [[ ! -d "$LOCAL_MEDIA_DIR" ]]; then red "`gettext \"Medias directory not found, aborting...\"`" >&2 exit 0 fi REMOTE_MEDIA_DIR="$ROOTFS_MOUNT/home/$PI_USER/Videos" # WIFI AP config IP_RANGE="10.42.0.1" SSID="foo" PASSWD="bar" IFW="wlo1" #Band (bg = 2.4Ghz, a= 5Ghz) BAND="bg" # Hidden SSID HIDE="802-11-wireless.hidden false" # Set channel manually CHAN="802-11-wireless.channel 1" # # # 0. Create AP connection if -w flag used # green "`gettext \" * Creating hotspot connection in NetworkManager \n\"`" if [[ "$DRY_RUN" == 0 ]] && [[ "$HOTSPOT" == 1 ]]; then #~ # If connection exists, delete it if [[ "$(nmcli con show | grep "$SSID")" != "" ]];then nmcli con delete "$SSID" fi nmcli con add type wifi ifname $IFW con-name $SSID autoconnect yes ssid $SSID nmcli con modify $SSID 802-11-wireless.mode ap 802-11-wireless.band "${BAND-}" ${CHAN-} ${HIDE-} ipv4.method shared nmcli con modify $SSID wifi-sec.key-mgmt wpa-psk nmcli con modify $SSID 802-11-wireless-security.proto rsn nmcli con modify $SSID 802-11-wireless-security.pairwise ccmp nmcli con modify $SSID wifi-sec.psk $PASSWD fi # 0.a set IP range on server green "`eval_gettext \" * Setting IP range \\\$IP_RANGE/24 in /etc/NetworkManager/system-connections/\\\$SSID.nmconnection ... \n\"`" if [[ "$DRY_RUN" == 0 ]] && [[ "$HOTSPOT" == 1 ]]; then sudo sed -i "/method=shared/a address1=$IP_RANGE/24, $IP_RANGE" /etc/NetworkManager/system-connections/$SSID.nmconnection # Remove existing leases sudo rm /var/lib/NetworkManager/dnsmasq-$IFW.leases # restart NM sudo systemctl restart NetworkManager # Turn hotspot on nmcli radio wifi on nmcli con up $SSID fi # 0.b ask for number of clients # This will be used to determine static IP yellow "`gettext \"Number of clients to configure : \"`" read -n 4 CLIENT_NUMBER # Check input if [[ ! "${CLIENT_NUMBER}" =~ ^[0-9]+$ ]]; then red "`gettext \"\nInput was not a number, aborting.\n\"`" exit 0 fi if [[ "${CLIENT_NUMBER}" -lt 1 ]]; then red "`gettext \"\nInput was 0, nothing to do.\n\"`" exit 0 fi green "`eval_gettext \"Got \\\$CLIENT_NUMBER...\n\"`" # For some reason networkmanager finds it clever to offer only IPs in range 10-255 even when asked for a /24, /28, etc... # So IPs start at 10 # Get first IP in specified range #~ IP=$(echo $IP_RANGE | awk -F. '{print $4}') FIRST=1 IP=10 #~ echo -e "First IP is $(($RANGE_START)) ...\n" # Remove IP's last byte IP_RANGE_3B=$(echo $IP_RANGE | awk -F. '{print $1"."$2"."$3"."}') echo -e "`eval_gettext \"First IP is \\\$IP_RANGE_3B\\\$IP ...\n\"`" # Generate SSL cert with IPs in IP_RANGE IP_CNT=$IP IP_ARRAY=() while [[ "$IP_CNT" -lt $(("${CLIENT_NUMBER-}"+"${IP-}")) ]] do IP_ARRAY+=("IP:$IP_RANGE_3B$IP_CNT") ((IP_CNT++)) done # Convert array to string HOST_LIST="$(IFS=","; echo "${IP_ARRAY[*]}")" green "`eval_gettext \"Got host list : \\\$HOST_LIST \n\"`" # 5. Generate valid ssl cert/key for every IP in range # https://unix.stackexchange.com/questions/104171/create-ssl-certificate-non-interactively green "`eval_gettext \"Generating SSL crt/key for \\\$HOST_LIST...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then openssl req -new -newkey rsa:4096 -days 1825 -nodes -x509 \ -subj "/C=/ST=Denial/L=/O=/CN=$IP_RANGE$FIRST" \ -addext "subjectAltName=$HOST_LIST" \ -keyout "$CONFIG_DIR/selfCA.key" -out "$CONFIG_DIR/selfCA.crt" #sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout "$ROOTFS_MOUNT/etc/ssl/private/nginx-selfsigned.key" -out "$ROOTFS_MOUNT/etc/ssl/certs/nginx-selfsigned.crt" fi # Proceed with each host for HOST in "${IP_ARRAY[@]}" do HOST=$(echo $HOST | awk -F: '{print $2}') HOST_NAME="pilpil-$(echo $HOST | awk -F. '{print $4}')" # 1. Copy img to sd green "`eval_gettext \"1/14 : Imaging \\\$SDCARD with the file \\\$DISK_IMAGE ...\n\"`" red "`eval_gettext \"Are you sure you want to ERASE THE CONTENT of \\\$SDCARD ? Type uppercase 'yes' to confirm.\"`" read -n 4 GO_DD if [[ "$GO_DD" != "`gettext \"YES\"`" ]] then red "`gettext \"Answer was different from 'YES'. Aborting...\"`" >&2 break fi red "`eval_gettext \"Received answer \\\$GO_DD. Running dd on \\\$SDCARD in 5 seconds.\"`" if [[ "$DRY_RUN" == 0 ]]; then sleep 5 fi GO_DD=0 # unmount / remount new filesystem green "`eval_gettext \"2/14 : Unmounting \\\$BOOT_MOUNT and \\\$ROOTFS_MOUNT ...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then if $(mountpoint -q "$BOOT_MOUNT");then umount "$BOOT_MOUNT" fi if $(mountpoint -q "$ROOTFS_MOUNT");then umount "$ROOTFS_MOUNT" fi xzcat "$DISK_IMAGE" | sudo dd of=$SDCARD bs="$DD_BS" oflag=dsync status=progress && sync fi green "`eval_gettext \"3/14 : Remounting \\\$BOOT_MOUNT and \\\$ROOTFS_MOUNT ...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then sleep 1 #~ systemctl --user restart gvfs-udisks2-volume-monitor PARTS="$(lsblk "$SDCARD" --noheadings --raw -o NAME | tail -2)" PART_BOOT="$(echo $PARTS | awk '{print $1}')" PART_ROOTFS="$(echo $PARTS | awk '{print $2}')" udisksctl mount -b /dev/"$PART_BOOT" udisksctl mount -b /dev/"$PART_ROOTFS" sleep 3 fi green "`eval_gettext \"4/14 : Changing hostname to \\\$HOST_NAME in \\\$ROOTFS_MOUNT/etc/hostname and \\\$ROOTFS_MOUNT/etc/hosts ...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then # TODO : /etc/resolv.conf ? # Change hostname echo "$HOST_NAME" | sudo tee "$ROOTFS_MOUNT/etc/hostname" # Reflect that in /etc/hosts sudo sed -i "$ d" "$ROOTFS_MOUNT/etc/hosts" echo -e "127.0.1.1\t$HOST_NAME" | sudo tee -a "$ROOTFS_MOUNT/etc/hosts" fi ## Enable SSH green "`eval_gettext \"5/14 : Enabling SSH server on boot : \\\$BOOT_MOUNT/ssh ...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then touch "$BOOT_MOUNT/ssh" sync fi ## Generate SSH private/public key and install it - Disable passwd login green "`eval_gettext \"6/14 : Generating private/public SSH key as \\\$HOME/.ssh/\\\$HOST_NAME ...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then ssh-keygen -t ed25519 -f "$HOME/.ssh/$HOST_NAME" -N "" fi red "`eval_gettext \"New SSH key pair generated as \\\$HOME/.ssh/\\\$HOST_NAME. Add to ~/.ssh/config ? (y/n)\"`" read -n 2 ADD_SSH_CONF if [[ "$ADD_SSH_CONF" == "`gettext \"y\"`" ]];then green "`eval_gettext \"Adding \\\$HOST_NAME with ip \\\$HOST in \\\$HOME/.ssh/config\"`" if [[ "$DRY_RUN" == 0 ]]; then # Add to ~/.ssh/config echo -e "\nHost $HOST_NAME\n\tHostname $HOST\n\tIdentityFile ~/.ssh/$HOST_NAME\n\tUser $PI_USER" | tee -a "$HOME/.ssh/config" fi else yellow "`gettext \"\nAnswer was different from 'y', skipping...\"`" fi # Copy public key to rpi green "`eval_gettext \"7/14 : Installing public SSH key \\\$HOME/.ssh/\\\$HOST.pub in \\\$ROOTFS_MOUNT/home/\\\$PI_USER/.ssh/authorized_keys...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then sudo cp "$HOME/.ssh/$HOST_NAME.pub" "$ROOTFS_MOUNT/home/$PI_USER/.ssh/authorized_keys" sync fi # Disable PW login green "`eval_gettext \"8/14 : Disabling SSH password based login in \\\$ROOTFS_MOUNT/etc/ssh/sshd_config ...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then echo -e "PasswordAuthentication no\nChallengeResponseAuthentication no\nUsePAM no" | sudo tee -a "$ROOTFS_MOUNT/etc/ssh/sshd_config" sync fi # 3. Configure wifi with static IP green "`eval_gettext \"9/14 : Configuring wireless connection to \\\$SSID with pw \\\$PASSWD : ...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then echo "ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev update_config=1 country=FR network={ ssid=\"$SSID\" # Nom du réseau auquel on se connecte psk=\"$PASSWD\" # Mot de passe wifi # Optional parameters # scan_ssid=1 # hidden ssid # Specify 2.4 or 5G freq # https://w1.fi/cgit/hostap/tree/wpa_supplicant/wpa_supplicant.conf#n910 # https://fr.wikipedia.org/wiki/Liste_des_canaux_Wi-Fi#Bande_2,4_GHz # scan_freq=2412 2437 2462 }" | sudo tee "$ROOTFS_MOUNT/etc/wpa_supplicant/wpa_supplicant.conf" sync fi # Request specific IP to dhcp server green "`eval_gettext \"10/14 : Setting static IP \\\$HOST in \\\$ROOTFS_MOUNT/etc/dhcpcd.conf...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then echo -e "interface wlan0\nrequest $HOST" | sudo tee -a "$ROOTFS_MOUNT/etc/dhcpcd.conf" sync fi # 5. Install previously generated SSL key/crt green "`eval_gettext \"11/14 : Installing SSL : \\\$CONFIG_DIR/selfCA.crt and $CONFIG_DIR/selfCA.key in \\\$ROOTFS_MOUNT/etc/ssl/certs/ and \\\$ROOTFS_MOUNT/etc/ssl/private/nginx-selfsigned.key...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then sudo cp "$CONFIG_DIR/selfCA.crt" "$ROOTFS_MOUNT/etc/ssl/certs/nginx-selfsigned.crt" sudo cp "$CONFIG_DIR/selfCA.key" "$ROOTFS_MOUNT/etc/ssl/private/nginx-selfsigned.key" sync fi green "`eval_gettext \"12/14 : Installing http auth secrets in \\\$CONFIG_DIR/pilpil-server.toml, \\\$ROOTFS_MOUNT/home/pil/.config/systemd/user/vlc.service and \\\$ROOTFS_MOUNT/home/pil/pilpil-client/defaults.toml...\n\"`" if [[ "$DRY_RUN" == 0 ]]; then #~ Change VLC/pilpil http auth secret sed -i "s:secret:$HTTP_SECRET:g" "$CONFIG_DIR/pilpil-server.toml" sed -i "s:secret:$HTTP_SECRET:g" "$ROOTFS_MOUNT/home/pil/.config/systemd/user/vlc.service" sed -i "s:secret:$HTTP_SECRET:g" "$ROOTFS_MOUNT/home/pil/pilpil-client/defaults.toml" fi #~ # 6. Copy medias green "`eval_gettext \"13/14 : Syncing media folder \\\$LOCAL_MEDIA_DIR/ with \\\$REMOTE_MEDIA_DIR/ \n\"`" if [[ "$DRY_RUN" == 0 ]]; then # Remove filler file if [[ -f "$REMOTE_MEDIA_DIR/remove_me" ]]; then sudo rm "$REMOTE_MEDIA_DIR/remove_me" fi fi if [[ -d "$ROOTFS_MOUNT" ]]; then # Get available space on rootfs ROOTFS_AVAILABLE_SPACE=$(df -t ext4 -P "$ROOTFS_MOUNT" | tail -1 | awk '{print $4}') yellow "`eval_gettext \"Space available on rootfs : \\\$ROOTFS_AVAILABLE_SPACE sectors\"`" # Get Media folder size MEDIA_SIZE=$(du -c "$LOCAL_MEDIA_DIR" | tail -1 | awk '{print $1}') yellow "`eval_gettext \"Size of medias : \\\$MEDIA_SIZE sectors\"`" fi if [[ "$DRY_RUN" == 0 ]]; then # Only copy files if enough space available if [[ "$MEDIA_SIZE" -lt "$ROOTFS_AVAILABLE_SPACE" ]]; then USER_ID=$( cat "$ROOTFS_MOUNT/etc/passwd" | grep $PI_USER | awk -F: '{print $3}' ) GROUP_ID=$( cat "$ROOTFS_MOUNT/etc/passwd" | grep $PI_USER | awk -F: '{print $4}' ) sudo cp "$LOCAL_MEDIA_DIR/"* "$REMOTE_MEDIA_DIR/" sudo chown -R "$USER_ID:$GROUP_ID" "$REMOTE_MEDIA_DIR" sync else red "`eval_gettext \"Not enough space on \\\$ROOTFS_MOUNT, skipping...\"`" fi fi # Unmount FS green "`gettext \"14/14 : Unmounting filesystems\"`" if [[ "$DRY_RUN" == 0 ]]; then if $(mountpoint -q "$BOOT_MOUNT");then umount "$BOOT_MOUNT" fi if $(mountpoint -q "$ROOTFS_MOUNT");then umount "$ROOTFS_MOUNT" fi fi yellow "`eval_gettext \"Client $(($IP-9))/$CLIENT_NUMBER.\"`" #~ echo "$IP / $(($IP_CNT-1))" if [[ "$IP" -lt "$(($IP_CNT-1))" ]]; then red "`gettext \"Please swap sd card in reader and enter uppercase 'yes' then Return to proceed with next client or directly hit Return to exit:\"`" read -n 4 GO_ON if [ "$GO_ON" != "`gettext \"YES\"`" ] then red "`gettext \"Answer was different from 'YES'. Aborting...\n\"`" >&2 break fi GO_ON=0 ((IP++)) else green "`gettext \"All done !\"`" # Remove SSL private key #~ rm -f "$CONFIG_DIR/selfCA.key" break fi done # Remove SSL private key rm -f "$CONFIG_DIR/selfCA.key" yellow "`gettext \"Nothing more to do.\"`" exit 1