diff --git a/MANIFEST.in b/MANIFEST.in index 8bf5cfb..4c36686 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,10 @@ exclude *.pyc .DS_Store .gitignore MANIFEST.in +include requirements.txt include setup.py -include distribute_setup.py include README.md include LICENSE recursive-include bin * +recursive-include builder/data * recursive-include pwnagotchi *.py recursive-include pwnagotchi *.yml recursive-include pwnagotchi *.* diff --git a/Makefile b/Makefile index f555a60..97017cc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,31 @@ -PACKER_VERSION=1.7.8 -PWN_HOSTNAME=pwnagotchi -PWN_VERSION=master +PACKER_VERSION := 1.8.3 +PWN_HOSTNAME := pwnagotchi +# PWN_VERSION := $(shell cut -d"'" -f2 < pwnagotchi/_version.py) +PWN_VERSION := $(or ${PWN_VERSION},$(shell cut -d"'" -f2 < pwnagotchi/_version.py)) +PWN_RELEASE := pwnagotchi-$(PWN_VERSION) + +MACHINE_TYPE := $(shell uname -m) +ifneq (,$(filter x86_64,$(MACHINE_TYPE))) +GOARCH := amd64 +else ifneq (,$(filter i686,$(MACHINE_TYPE))) +GOARCH := 386 +else ifneq (,$(filter arm64% aarch64%,$(MACHINE_TYPE))) +GOARCH := arm64 +else ifneq (,$(filter arm%,$(MACHINE_TYPE))) +GOARCH := arm +else +GOARCH := amd64 +$(warning Unable to detect CPU arch from machine type $(MACHINE_TYPE), assuming $(GOARCH)) +endif + +# The Ansible part of the build can inadvertently change the active hostname of +# the build machine while updating the permanent hostname of the build image. +# If the unshare command is available, use it to create a separate namespace +# so hostname changes won't affect the build machine. +UNSHARE := $(shell command -v unshare) +ifneq (,$(UNSHARE)) +UNSHARE := $(UNSHARE) --uts +endif all: clean install image @@ -8,23 +33,47 @@ langs: @for lang in pwnagotchi/locale/*/; do\ echo "compiling language: $$lang ..."; \ ./scripts/language.sh compile $$(basename $$lang); \ - done + done install: - curl https://releases.hashicorp.com/packer/$(PACKER_VERSION)/packer_$(PACKER_VERSION)_linux_amd64.zip -o /tmp/packer.zip - unzip /tmp/packer.zip -d /tmp - sudo mv /tmp/packer /usr/bin/packer - git clone https://github.com/solo-io/packer-builder-arm-image /tmp/packer-builder-arm-image - cd /tmp/packer-builder-arm-image && go get -d ./... && go build - sudo cp /tmp/packer-builder-arm-image/packer-plugin-arm-image /usr/bin +PACKER := /tmp/pwnagotchi/packer +PACKER_URL := https://releases.hashicorp.com/packer/$(PACKER_VERSION)/packer_$(PACKER_VERSION)_linux_$(GOARCH).zip +$(PACKER): + mkdir -p $(@D) + curl -L "$(PACKER_URL)" -o $(PACKER).zip + unzip $(PACKER).zip -d $(@D) + rm $(PACKER).zip + chmod +x $@ -image: - cd builder && sudo /usr/bin/packer build -var "pwn_hostname=$(PWN_HOSTNAME)" -var "pwn_version=$(PWN_VERSION)" pwnagotchi.json - sudo mv builder/output-pwnagotchi/image pwnagotchi-raspbian-lite-$(PWN_VERSION).img - sudo sha256sum pwnagotchi-raspbian-lite-$(PWN_VERSION).img > pwnagotchi-raspbian-lite-$(PWN_VERSION).sha256 - sudo zip pwnagotchi-raspbian-lite-$(PWN_VERSION).zip pwnagotchi-raspbian-lite-$(PWN_VERSION).sha256 pwnagotchi-raspbian-lite-$(PWN_VERSION).img +SDIST := dist/pwnagotchi-$(PWN_VERSION).tar.gz +$(SDIST): setup.py pwnagotchi + python3 setup.py sdist + +# Building the image requires packer, but don't rebuild the image just because packer updated. +$(PWN_RELEASE).img: | $(PACKER) + +# If the packer or ansible files are updated, rebuild the image. +$(PWN_RELEASE).img: $(SDIST) builder/pwnagotchi.json builder/pwnagotchi.yml $(shell find builder/data -type f) + sudo $(PACKER) plugins install github.com/solo-io/arm-image + cd builder && sudo $(UNSHARE) $(PACKER) build -var "pwn_hostname=$(PWN_HOSTNAME)" -var "pwn_version=$(PWN_VERSION)" pwnagotchi.json + sudo chown -R $$USER:$$USER builder/output-pwnagotchi + mv builder/output-pwnagotchi/image $@ + +# If any of these files are updated, rebuild the checksums. +$(PWN_RELEASE).sha256: $(PWN_RELEASE).img + sha256sum $^ > $@ + +# If any of the input files are updated, rebuild the archive. +$(PWN_RELEASE).zip: $(PWN_RELEASE).img $(PWN_RELEASE).sha256 + zip $(PWN_RELEASE).zip $^ + +.PHONY: image +image: $(PWN_RELEASE).zip clean: - sudo rm -rf /tmp/packer-builder-arm-image - sudo rm -f pwnagotchi-raspbian-lite-*.zip pwnagotchi-raspbian-lite-*.img pwnagotchi-raspbian-lite-*.sha256 - sudo rm -rf builder/output-pwnagotchi builder/packer_cache + - python3 setup.py clean --all + - rm -rf dist pwnagotchi.egg-info + - rm -f $(PACKER) + - rm -f $(PWN_RELEASE).* + - sudo rm -rf builder/output-pwnagotchi builder/packer_cache + diff --git a/builder/data/etc/network/interfaces.d/wlan0-cfg b/builder/data/etc/network/interfaces.d/wlan0-cfg index f542569..0b6a7fb 100644 --- a/builder/data/etc/network/interfaces.d/wlan0-cfg +++ b/builder/data/etc/network/interfaces.d/wlan0-cfg @@ -1,2 +1,4 @@ allow-hotplug wlan0 -iface wlan0 inet static \ No newline at end of file +iface wlan0 inet manual + pre-up ifconfig $IFACE up + post-down ifconfig $IFACE down diff --git a/builder/data/usr/bin/bettercap-launcher b/builder/data/usr/bin/bettercap-launcher index 3bc2185..ff3052c 100755 --- a/builder/data/usr/bin/bettercap-launcher +++ b/builder/data/usr/bin/bettercap-launcher @@ -19,7 +19,12 @@ if ! check_brcm; then fi # start mon0 -start_monitor_interface +if ! is_interface_up 'mon0'; then + start_monitor_interface +else + stop_monitor_interface + start_monitor_interface +fi if is_auto_mode_no_delete; then /usr/bin/bettercap -no-colors -caplet pwnagotchi-auto -iface mon0 diff --git a/builder/data/usr/bin/pwnlib b/builder/data/usr/bin/pwnlib index 0e45d25..4be47b2 100755 --- a/builder/data/usr/bin/pwnlib +++ b/builder/data/usr/bin/pwnlib @@ -3,12 +3,30 @@ # well ... it blinks the led blink_led() { for i in $(seq 1 "$1"); do - echo 0 >/sys/class/leds/led0/brightness + if [ -d /sys/class/leds/led0 ] + then + echo 0 | tee /sys/class/leds/led0/brightness + else + echo 0 | tee /sys/class/leds/ACT/brightness + fi sleep 0.3 - echo 1 >/sys/class/leds/led0/brightness + + if [ -d /sys/class/leds/led0 ] + then + echo 1 | tee /sys/class/leds/led0/brightness + else + echo 1 | tee /sys/class/leds/ACT/brightness + fi sleep 0.3 + done - echo 0 >/sys/class/leds/led0/brightness + + if [ -d /sys/class/leds/led0 ] + then + echo 0 | tee /sys/class/leds/led0/brightness + else + echo 0 | tee /sys/class/leds/ACT/brightness + fi sleep 0.3 } @@ -33,20 +51,31 @@ reload_brcm() { # starts mon0 start_monitor_interface() { + rfkill unblock all + iw dev wlan0 set power_save off + + ifconfig wlan0 up + iw phy "$(iw phy | head -1 | cut -d" " -f2)" interface add mon0 type monitor && ifconfig mon0 up + + # If wlan0 is NOT taken down after bringing up mon0, then when switching to AUTO you will get: + # error 400: error while initializing mon0 to channel 1: iw: out=command failed: Device or resource busy (-16) err=exit status 240 + ifconfig wlan0 down } # stops mon0 stop_monitor_interface() { ifconfig mon0 down && iw dev mon0 del + ifconfig wlan0 up } # returns 0 if the specificed network interface is up is_interface_up() { if grep -qi 'up' /sys/class/net/$1/operstate; then return 0 + else + return 1 fi - return 1 } # returns 0 if conditions for AUTO mode are met diff --git a/builder/pwnagotchi.json b/builder/pwnagotchi.json index 8ca1e1f..ce54305 100644 --- a/builder/pwnagotchi.json +++ b/builder/pwnagotchi.json @@ -3,95 +3,34 @@ { "name": "pwnagotchi", "type": "arm-image", - "iso_url": "https://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2020-02-14/2020-02-13-raspbian-buster-lite.zip", - "iso_checksum": "12ae6e17bf95b6ba83beca61e7394e7411b45eba7e6a520f434b0748ea7370e8", - "target_image_size": 6442450944 + "iso_url": "https://downloads.raspberrypi.org/raspios_oldstable_lite_armhf/images/raspios_oldstable_lite_armhf-2023-05-03/2023-05-03-raspios-buster-armhf-lite.img.xz", + "iso_checksum": "3d210e61b057de4de90eadb46e28837585a9b24247c221998f5bead04f88624c", + "target_image_size": 9368709120, + "qemu_args": ["-cpu", "arm1176"] } ], "provisioners": [ { "type": "shell", "inline": [ - "sed -i 's/^\\([^#]\\)/#\\1/g' /etc/ld.so.preload", + "mv /etc/ld.so.preload /etc/ld.so.preload.DISABLED", + "uname -a", "dpkg-architecture", - "apt -y update --allow-releaseinfo-change", - "apt install -y ansible" + "mkdir -p /usr/local/src/pwnagotchi" ] }, { "type": "file", - "source": "data/usr/bin/pwnlib", - "destination": "/usr/bin/pwnlib" - }, - { - "type": "file", - "source": "data/usr/bin/bettercap-launcher", - "destination": "/usr/bin/bettercap-launcher" - }, - { - "type": "file", - "source": "data/usr/bin/pwnagotchi-launcher", - "destination": "/usr/bin/pwnagotchi-launcher" - }, - { - "type": "file", - "source": "data/usr/bin/monstop", - "destination": "/usr/bin/monstop" - }, - { - "type": "file", - "source": "data/usr/bin/monstart", - "destination": "/usr/bin/monstart" - }, - { - "type": "file", - "source": "data/usr/bin/hdmion", - "destination": "/usr/bin/hdmion" - }, - { - "type": "file", - "source": "data/usr/bin/hdmioff", - "destination": "/usr/bin/hdmioff" - }, - { - "type": "file", - "source": "data/etc/network/interfaces.d/lo-cfg", - "destination": "/etc/network/interfaces.d/lo-cfg" - }, - { - "type": "file", - "source": "data/etc/network/interfaces.d/wlan0-cfg", - "destination": "/etc/network/interfaces.d/wlan0-cfg" - }, - { - "type": "file", - "source": "data/etc/network/interfaces.d/usb0-cfg", - "destination": "/etc/network/interfaces.d/usb0-cfg" - }, - { - "type": "file", - "source": "data/etc/network/interfaces.d/eth0-cfg", - "destination": "/etc/network/interfaces.d/eth0-cfg" - }, - { - "type": "file", - "source": "data/etc/systemd/system/pwngrid-peer.service", - "destination": "/etc/systemd/system/pwngrid-peer.service" - }, - { - "type": "file", - "source": "data/etc/systemd/system/pwnagotchi.service", - "destination": "/etc/systemd/system/pwnagotchi.service" - }, - { - "type": "file", - "source": "data/etc/systemd/system/bettercap.service", - "destination": "/etc/systemd/system/bettercap.service" + "sources": [ + "../dist/pwnagotchi-{{user `pwn_version`}}.tar.gz" + ], + "destination": "/usr/local/src/pwnagotchi/" }, { "type": "shell", "inline": [ - "chmod +x /usr/bin/*" + "apt-get -y --allow-releaseinfo-change update", + "apt-get install -y --no-install-recommends ansible" ] }, { @@ -103,7 +42,7 @@ { "type": "shell", "inline": [ - "sed -i 's/^#\\(.+\\)/\\1/g' /etc/ld.so.preload" + "mv /etc/ld.so.preload.DISABLED /etc/ld.so.preload" ] } ] diff --git a/builder/pwnagotchi.yml b/builder/pwnagotchi.yml index 8fd1237..34d9390 100644 --- a/builder/pwnagotchi.yml +++ b/builder/pwnagotchi.yml @@ -1,18 +1,17 @@ --- - hosts: - 127.0.0.1 + gather_facts: yes become: yes vars: pwnagotchi: hostname: "{{ lookup('env', 'PWN_HOSTNAME') | default('pwnagotchi', true) }}" version: "{{ lookup('env', 'PWN_VERSION') | default('master', true) }}" system: - boot_options4: - - "dtoverlay=disable-wifi" - - "arm_freq=800" - boot_optionsall: + boot_options: - "dtoverlay=dwc2" - "dtoverlay=spi1-3cs" + - "dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4" - "dtparam=spi=on" - "dtparam=i2c_arm=on" - "dtparam=i2c1=on" @@ -39,7 +38,8 @@ - dnsmasq.service packages: bettercap: - url: "https://github.com/bettercap/bettercap/releases/download/v2.31.0/bettercap_linux_armhf_v2.31.0.zip" + # We will install bettercap v2.32 from source + # url: "https://github.com/bettercap/bettercap/releases/download/v2.31.0/bettercap_linux_armhf_v2.31.0.zip" ui: "https://github.com/bettercap/ui/releases/download/v1.3.0/ui.zip" pwngrid: url: "https://github.com/evilsocket/pwngrid/releases/download/v1.10.3/pwngrid_linux_armhf_v1.10.3.zip" @@ -56,12 +56,13 @@ - triggerhappy - wpa_supplicant - nfs-common + # Remove every golang package because we will install go-1.20.2 + - golang* - python2* install: - rsync - vim - screen - - golang - git - build-essential - python3-pip @@ -72,6 +73,7 @@ - libopenmpi-dev - libatlas-base-dev - libjasper-dev + - libgtk-3-0 - libqtgui4 - libqt4-test - libopenjp2-7 @@ -89,10 +91,6 @@ - libnetfilter-queue-dev - libopenmpi3 - dphys-swapfile - - kalipi-kernel - - kalipi-bootloader - - kalipi-re4son-firmware - - kalipi-kernel-headers - libraspberrypi0 - libraspberrypi-dev - libraspberrypi-doc @@ -109,10 +107,33 @@ - fonts-ipaexfont-gothic - cryptsetup - dnsmasq - - python3-rpi.gpio - - firmware-ralink + - aircrack-ng + - raspberrypi-kernel-headers + - libgmp3-dev + - qpdf + - bison + - flex + - make + - autoconf + - libtool + - texinfo + - binutils + - lnav + - p7zip-full + + environment: + ARCHFLAGS: "-arch armv7l" tasks: + - name: System details + debug: + msg="{{ item }}" + with_items: + - "{{ ansible_distribution }}" + - "{{ ansible_distribution_version }}" + - "{{ ansible_distribution_major_version }}" + - "{{ ansible_architecture }}" + - "{{ ansible_machine }}" - name: change hostname hostname: name: "{{pwnagotchi.hostname}}" @@ -134,26 +155,6 @@ line: 'ExecStart=/usr/lib/bluetooth/bluetoothd --noplugin=sap' state: present - - name: Add re4son-kernel repo key - apt_key: - keyserver: pgp.mit.edu - id: 11764EE8AC24832F - - - name: Add re4son-kernel repository - apt_repository: - repo: deb http://http.re4son-kernel.com/re4son/ kali-pi main - state: present - - - name: create /etc/apt/preferences.d/kali.pref - copy: - dest: /etc/apt/preferences.d/kali.pref - force: yes - content: | - # ensure kali packages that are installed take precedence - Package: * - Pin: release n=kali-pi - Pin-Priority: 999 - - name: add firmware packages to hold dpkg_selections: name: "{{ item }}" @@ -172,17 +173,26 @@ - name: upgrade apt distro apt: - upgrade: full + upgrade: dist - name: install packages apt: name: "{{ packages.apt.install }}" state: present + - name: Update .bashrc (root) + blockinfile: + dest: /root/.bashrc + state: present + block: | + export MAKEFLAGS=-j$(nproc) + insertafter: EOF + - name: configure dphys-swapfile - file: + lineinfile: path: /etc/dphys-swapfile - content: "CONF_SWAPSIZE=1024" + regexp: "^CONF_SWAPSIZE=.*$" + line: "CONF_SWAPSIZE=512" - name: clone papirus repository git: @@ -214,67 +224,55 @@ regexp: "#EPD_SIZE=2.0" line: "EPD_SIZE=2.0" - - name: collect python pip package list - command: "pip3 list" - register: pip_output - - - name: set python pip package facts - set_fact: - pip_packages: > - {{ pip_packages | default({}) | combine( { item.split()[0]: item.split()[1] } ) }} - with_items: "{{ pip_output.stdout_lines }}" - - - name: acquire python3 pip target - command: "python3 -c 'import sys;print(sys.path.pop())'" - register: pip_target - - - name: clone pwnagotchi repository - git: - repo: https://git.chadwaltercummings.me/scifijunkie/pwnagotchi.git - dest: /usr/local/src/pwnagotchi - register: pwnagotchigit - - - name: create /usr/local/share/pwnagotchi/ folder + - name: Delete papirus content & directory file: - path: /usr/local/share/pwnagotchi/ + state: absent + path: /usr/local/src/gratis + when: gratisgit.changed + + # pip v20.3 uses a newer dependency resolver that better handles our unique situation. + # Specifically, it handles mismatches between direct requirements without extras and + # indirect requirements that do want extras (e.g. gym vs stable-baselines->gym[atari]). + - name: Upgrade pip and install rpi-hardware-pwm + pip: + name: + - pip>=20.3 + - rpi-hardware-pwm + + # We need the --ignore-installed option so that pip simply overwrites/upgrades existing + # packages instead of trying to uninstall them first. While this sounds dangerous, + # this matches the legacy behavior of pip. This is required to prevent pip from trying + # (and failing) to uninstall python packages that were originally installed via apt. + - name: Install pwnagotchi from source archive + pip: + name: /usr/local/src/pwnagotchi/pwnagotchi-{{ pwnagotchi.version }}.tar.gz + extra_args: --verbose --prefer-binary --ignore-installed --retries 50 --index-url https://nexus.chadwaltercummings.me/repository/pypi.org/simple --extra-index-url https://nexus.chadwaltercummings.me/repository/www.piwheels.org/simple + + - name: create custom plugin directory + file: + path: /usr/local/share/pwnagotchi/custom-plugins/ state: directory - name: clone pwnagotchi plugins repository git: repo: https://git.chadwaltercummings.me/scifijunkie/pwnagotchi-plugins-contrib.git - dest: /usr/local/share/pwnagotchi/availaible-plugins + dest: /usr/local/share/pwnagotchi/available-plugins - - name: fetch pwnagotchi version - set_fact: - pwnagotchi_version: "{{ lookup('file', '/usr/local/src/pwnagotchi/pwnagotchi/_version.py') | regex_replace('.*__version__.*=.*''([0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]*)''.*', '\\1') }}" + - name: Copy aircrackonly.py + copy: + src: /usr/local/share/pwnagotchi/available-plugins/aircrackonly.py + dest: /usr/local/share/pwnagotchi/custom-plugins/aircrackonly.py + owner: root + group: root + mode: '644' - - name: pwnagotchi version found - debug: - msg: "{{ pwnagotchi_version }}" - - - name: build pwnagotchi wheel - command: "python3 setup.py sdist bdist_wheel" - args: - chdir: /usr/local/src/pwnagotchi - when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi_version) - - - name: install opencv-python - pip: - name: "https://www.piwheels.org/simple/opencv-python/opencv_python-3.4.3.18-cp37-cp37m-linux_armv6l.whl" - extra_args: "--no-deps --no-cache-dir --platform=linux_armv6l --only-binary=:all: --target={{ pip_target.stdout }}" - when: (pip_packages['opencv-python'] is undefined) or (pip_packages['opencv-python'] != '3.4.3.18') - - - name: install tensorflow - pip: - name: "https://www.piwheels.org/simple/tensorflow/tensorflow-1.13.1-cp37-none-linux_armv6l.whl" - extra_args: "--no-deps --no-cache-dir --platform=linux_armv6l --only-binary=:all: --target={{ pip_target.stdout }}" - when: (pip_packages['tensorflow'] is undefined) or (pip_packages['tensorflow'] != '1.13.1') - - - name: install pwnagotchi wheel and dependencies - pip: - name: "{{ lookup('fileglob', '/usr/local/src/pwnagotchi/dist/pwnagotchi*.whl') }}" - extra_args: "--no-cache-dir" - when: (pwnagotchigit.changed) or (pip_packages['pwnagotchi'] is undefined) or (pip_packages['pwnagotchi'] != pwnagotchi_version) + - name: Copy handshakes-dl.py + copy: + src: /usr/local/share/pwnagotchi/available-plugins/handshakes-dl.py + dest: /usr/local/share/pwnagotchi/custom-plugins/handshakes-dl.py + owner: root + group: root + mode: '644' - name: download and install pwngrid unarchive: @@ -283,15 +281,33 @@ remote_src: yes mode: 0755 - - name: download and install bettercap + # Install go-1.21.5 + - name: Install go-1.21.5 unarchive: - src: "{{ packages.bettercap.url }}" - dest: /usr/bin + src: https://go.dev/dl/go1.21.5.linux-armv6l.tar.gz + dest: /usr/local remote_src: yes - exclude: - - README.md - - LICENSE.md - mode: 0755 + register: golang + + - name: Update .bashrc for go-1.21.5 (pi) + blockinfile: + dest: /home/pi/.bashrc + state: present + block: | + export GOPATH=$HOME/go + export PATH=/usr/local/go/bin:$PATH:$GOPATH/bin + insertafter: EOF + when: golang.changed + + - name: Install bettercap v2.32 + shell: "export GOPATH=$HOME/go && export PATH=/usr/local/go/bin:$PATH:$GOPATH/bin && go env -w GO111MODULE=off && go get github.com/bettercap/bettercap && cd $GOPATH/src/github.com/bettercap/bettercap && make build && make install" + args: + executable: /bin/bash + register: bettercap + + - name: Link bettercap v2.32 + command: ln -s /usr/local/bin/bettercap /usr/bin/bettercap + when: bettercap.changed - name: clone bettercap caplets git: @@ -312,6 +328,230 @@ remote_src: yes mode: 0755 + # Install nexmon to fix wireless scanning (takes 2.5G of space) + - name: clone nexmon repository + git: + repo: https://github.com/seemoo-lab/nexmon.git + dest: /usr/local/src/nexmon +# version: bfb3fe90c881498d7ee245b38f16722c1de26fa1 + register: nexmongit + + - name: configure libisl + command: chdir=/usr/local/src/nexmon/buildtools/isl-0.10/ ./configure + + - name: make libisl + command: chdir=/usr/local/src/nexmon/buildtools/isl-0.10/ make + + - name: install libisl + command: chdir=/usr/local/src/nexmon/buildtools/isl-0.10/ make install + + - name: link libisl + command: ln -s /usr/local/lib/libisl.so /usr/lib/arm-linux-gnueabihf/libisl.so.10 + + - name: autoreconf libmpfr + command: chdir=/usr/local/src/nexmon/buildtools/mpfr-3.1.4/ autoreconf -f -i + + - name: configure libmpfr + command: chdir=/usr/local/src/nexmon/buildtools/mpfr-3.1.4/ ./configure + + - name: make libmpfr + command: chdir=/usr/local/src/nexmon/buildtools/mpfr-3.1.4/ make + + - name: install libmpfr + command: chdir=/usr/local/src/nexmon/buildtools/mpfr-3.1.4/ make install + + - name: link libmpfr + command: ln -s /usr/local/lib/libmpfr.so /usr/lib/arm-linux-gnueabihf/libmpfr.so.4 + + - name: make firmware + shell: "source ./setup_env.sh && make" + args: + executable: /bin/bash + chdir: /usr/local/src/nexmon/ + + - name: choose the right kernel version (bcm43436b0) + replace: + dest: /usr/local/src/nexmon/patches/bcm43436b0/9_88_4_65/nexmon/Makefile + backup: no + regexp: "KERNEL_VERSION = .*$" + replace: "KERNEL_VERSION = 5.10" + + - name: choose the right kernel release (variable) (bcm43436b0) + lineinfile: + dest: /usr/local/src/nexmon/patches/bcm43436b0/9_88_4_65/nexmon/Makefile + insertafter: "DRIVER_FOLDER_NAME = .*$" + line: "KERNEL_RELEASE = 5.10.103-v7+" + + - name: choose the right kernel release (replace string) (bcm43436b0) + replace: + dest: /usr/local/src/nexmon/patches/bcm43436b0/9_88_4_65/nexmon/Makefile + backup: no + regexp: "shell uname -r" + replace: "KERNEL_RELEASE" + + - name: make firmware patch (bcm43436b0) + shell: "source ./setup_env.sh && cd /usr/local/src/nexmon/patches/bcm43436b0/9_88_4_65/nexmon/ && make" + args: + executable: /bin/bash + chdir: /usr/local/src/nexmon/ + + # - name: backup original firmware + # shell: "source ./setup_env.sh && cd /usr/local/src/nexmon/patches/bcm43436b0/9_88_4_65/nexmon/ && make backup-firmware" + # args: + # executable: /bin/bash + # chdir: /usr/local/src/nexmon/ + + # - name: install new firmware + # shell: "source ./setup_env.sh && cd /usr/local/src/nexmon/patches/bcm43436b0/9_88_4_65/nexmon/ && make install-firmware" + # args: + # executable: /bin/bash + # chdir: /usr/local/src/nexmon/ + + - name: install new firmware (bcm43436b0) + copy: + src: /usr/local/src/nexmon/patches/bcm43436b0/9_88_4_65/nexmon/brcmfmac43436-sdio.bin + dest: /lib/firmware/brcm/brcmfmac43436-sdio.bin + + - name: choose the right kernel version (bcm43430a1) + replace: + dest: /usr/local/src/nexmon/patches/bcm43430a1/7_45_41_46/nexmon/Makefile + backup: no + regexp: "KERNEL_VERSION = .*$" + replace: "KERNEL_VERSION = 5.10" + + - name: choose the right kernel release (variable) (bcm43430a1) + lineinfile: + dest: /usr/local/src/nexmon/patches/bcm43430a1/7_45_41_46/nexmon/Makefile + insertafter: "DRIVER_FOLDER_NAME = .*$" + line: "KERNEL_RELEASE = 5.10.103-v7+" + + - name: choose the right kernel release (replace string) (bcm43430a1) + replace: + dest: /usr/local/src/nexmon/patches/bcm43430a1/7_45_41_46/nexmon/Makefile + backup: no + regexp: "shell uname -r" + replace: "KERNEL_RELEASE" + + - name: make firmware patch (bcm43430a1) + shell: "source ./setup_env.sh && cd /usr/local/src/nexmon/patches/bcm43430a1/7_45_41_46/nexmon/ && make" + args: + executable: /bin/bash + chdir: /usr/local/src/nexmon/ + + # - name: backup original firmware + # shell: "source ./setup_env.sh && cd /usr/local/src/nexmon/patches/bcm43430a1/7_45_41_46/nexmon/ && make backup-firmware" + # args: + # executable: /bin/bash + # chdir: /usr/local/src/nexmon/ + + # - name: install new firmware + # shell: "source ./setup_env.sh && cd /usr/local/src/nexmon/patches/bcm43430a1/7_45_41_46/nexmon/ && make install-firmware" + # args: + # executable: /bin/bash + # chdir: /usr/local/src/nexmon/ + + - name: install new firmware (bcm43430a1) + copy: + src: /usr/local/src/nexmon/patches/bcm43430a1/7_45_41_46/nexmon/brcmfmac43430-sdio.bin + dest: /lib/firmware/brcm/brcmfmac43430-sdio.bin + + - name: Delete the firmware blob to avoid it crashing + file: + state: absent + path: /lib/firmware/brcm/brcmfmac43430-sdio.clm_blob + + - name: Delete the RPiZW firmware blob to avoid it crashing + file: + state: absent + path: /lib/firmware/brcm/brcmfmac43430-sdio.raspberrypi,model-zero-w.clm_blob + + - name: Delete the RPi3 firmware blob to avoid it crashing + file: + state: absent + path: /lib/firmware/brcm/brcmfmac43430-sdio.raspberrypi,3-model-b.clm_blob + + - name: choose the right kernel version (bcm43455c0) + replace: + dest: /usr/local/src/nexmon/patches/bcm43455c0/7_45_206/nexmon/Makefile + backup: no + regexp: "KERNEL_VERSION = .*$" + replace: "KERNEL_VERSION = 5.10" + + - name: choose the right kernel release (variable) (bcm43455c0) + lineinfile: + dest: /usr/local/src/nexmon/patches/bcm43455c0/7_45_206/nexmon/Makefile + insertafter: "DRIVER_FOLDER_NAME = .*$" + line: "KERNEL_RELEASE = 5.10.103-v7+" + + - name: choose the right kernel release (replace string) (bcm43455c0) + replace: + dest: /usr/local/src/nexmon/patches/bcm43455c0/7_45_206/nexmon/Makefile + backup: no + regexp: "shell uname -r" + replace: "KERNEL_RELEASE" + + - name: make firmware patch (bcm43455c0) + shell: "source ./setup_env.sh && cd /usr/local/src/nexmon/patches/bcm43455c0/7_45_206/nexmon/ && make" + args: + executable: /bin/bash + chdir: /usr/local/src/nexmon/ + + # - name: backup original firmware + # shell: "source ./setup_env.sh && cd /usr/local/src/nexmon/patches/bcm43455c0/7_45_206/nexmon/ && make backup-firmware" + # args: + # executable: /bin/bash + # chdir: /usr/local/src/nexmon/ + + # - name: install new firmware + # shell: "source ./setup_env.sh && cd /usr/local/src/nexmon/patches/bcm43455c0/7_45_206/nexmon/ && make install-firmware" + # args: + # executable: /bin/bash + # chdir: /usr/local/src/nexmon/ + + - name: install new firmware (bcm43455c0) + copy: + src: /usr/local/src/nexmon/patches/bcm43455c0/7_45_206/nexmon/brcmfmac43455-sdio.bin + dest: /lib/firmware/brcm/brcmfmac43455-sdio.bin + + - name: make nexutil + command: chdir=/usr/local/src/nexmon/utilities/nexutil/ make + + - name: make install nexutil + command: chdir=/usr/local/src/nexmon/utilities/nexutil/ make install + + # - name: copy modified driver + # shell: "cd /usr/local/src/nexmon/patches/driver/brcmfmac_5.10.y-nexmon/ && cp brcmfmac.ko /lib/modules/5.10.103-v7+/kernel/drivers/net/wireless/broadcom/brcm80211/brcmfmac/brcmfmac.ko && depmod -a" + # args: + # executable: /bin/bash + + - name: copy modified driver (everyone but RPiZW) + copy: + src: /usr/local/src/nexmon/patches/driver/brcmfmac_5.10.y-nexmon/brcmfmac.ko + dest: /lib/modules/5.10.103-v7+/kernel/drivers/net/wireless/broadcom/brcm80211/brcmfmac/brcmfmac.ko + + - name: ensure depmod runs on reboot to load modified driver (brcmfmac) + lineinfile: + dest: /etc/rc.local + line: "/sbin/depmod -a" + + # To shrink the final image, remove the nexmon directory (takes 2.5G of space) post build and installation + - name: Delete nexmon content & directory + file: + state: absent + path: /usr/local/src/nexmon/ + + - name: Add pwnlog alias + lineinfile: + dest: /home/pi/.bashrc + line: "\nalias pwnlog='tail -f -n300 /var/log/pwn*.log | sed --unbuffered \"s/,[[:digit:]]\\{3\\}\\]//g\" | cut -d \" \" -f 2-'" + insertafter: EOF + + - name: install bettercap caplets + make: + chdir: /tmp/caplets + target: install + when: capletsgit.changed + - name: add HDMI powersave to rc.local blockinfile: path: /etc/rc.local @@ -341,24 +581,41 @@ # ui.display.type = "waveshare_2" when: not user_config.stat.exists +# - name: append commented out parameters for usb_hat_c.py +# lineinfile: +# dest: /etc/pwnagotchi/config.toml +# line: "# main.plugins.ups_hat_c.enabled = true\n# main.plugins.ups_hat_c.label_on = true # show BAT label or just percentage\n# main.plugins.ups_hat_c.shutdown = 5 # battery percent at which the device will turn off\n# main.plugins.ups_hat_c.bat_x_coord = 140\n# main.plugins.ups_hat_c.bat_y_coord = 0" +# insertafter: EOF + + #bizzarely changing the plugin code directly reverts to the old string + - name: Reconfigure auto-update to point to the scifijunk repo + replace: + dest: /usr/local/lib/python3.7/dist-packages/pwnagotchi/plugins/default/auto-update.py + backup: no + regexp: "evilsocket/pwnagotchi" + replace: "scifijunk/pwnagotchi" + + - name: Delete unnecessary large folder to save space (/root/go) + file: + state: absent + path: /root/go + + - name: Delete unnecessary large folder to save space (/root/.cache) + file: + state: absent + path: /root/.cache + - name: enable ssh on boot file: path: /boot/ssh state: touch - - - name: adjust [pi4] /boot/config.txt - lineinfile: - dest: /boot/config.txt - insertafter: max_framebuffers=2 - line: '{{ item }}' - with_items: "{{system.boot_options4}}" - - name: adjust [all] /boot/config.txt + - name: adjust /boot/config.txt lineinfile: dest: /boot/config.txt insertafter: EOF line: '{{ item }}' - with_items: "{{system.boot_optionsall}}" + with_items: "{{system.boot_options}}" - name: adjust /etc/modules lineinfile: @@ -415,9 +672,14 @@ You learn more about me at https://pwnagotchi.ai/ when: hostname.changed + # Ansible's apt module has an "autoclean" option but it only removes packages + # that can no longer be downloaded. Ansible v2.13 added the "clean" option + # which actually purges the apt cache, but that's newer than what we can + # install from the RasPiOS repos. Instead, we'll manually clean the cache. - name: clean apt cache - apt: - autoclean: yes + command: "apt-get clean" + args: + warn: false - name: remove dependencies that are no longer required apt: diff --git a/pwnagotchi/__init__.py b/pwnagotchi/__init__.py index ca37a1e..54c3820 100644 --- a/pwnagotchi/__init__.py +++ b/pwnagotchi/__init__.py @@ -41,7 +41,7 @@ def set_name(new_name): fp.write(patched) os.system("hostname '%s'" % new_name) - reboot() + pwnagotchi.reboot() def name(): @@ -119,7 +119,7 @@ def shutdown(): from pwnagotchi import fs for m in fs.mounts: m.sync() - + os.system("sync") os.system("halt") @@ -133,6 +133,7 @@ def restart(mode): os.system("touch /root/.pwnagotchi-manual") os.system("service bettercap restart") + time.sleep(2) os.system("service pwnagotchi restart") diff --git a/pwnagotchi/_version.py b/pwnagotchi/_version.py index 63a5878..828d4aa 100644 --- a/pwnagotchi/_version.py +++ b/pwnagotchi/_version.py @@ -1 +1 @@ -__version__ = '1.5.5' +__version__='1.8.4' diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py index 7fe6a76..6b666a4 100644 --- a/pwnagotchi/agent.py +++ b/pwnagotchi/agent.py @@ -324,6 +324,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer): found_handshake = False jmsg = json.loads(msg) + # give plugins access to all raw bettercap events + try: + plugins.on('bcap_%s' % re.sub(r"[^a-z0-9_]+", "_", jmsg['tag'].lower()), self, jmsg) + except Exception as err: + logging.error("Processing event: %s" % err) + if jmsg['tag'] == 'wifi.client.handshake': filename = jmsg['data']['file'] sta_mac = jmsg['data']['station'] diff --git a/pwnagotchi/bettercap.py b/pwnagotchi/bettercap.py index 1f9217d..557e6ad 100644 --- a/pwnagotchi/bettercap.py +++ b/pwnagotchi/bettercap.py @@ -2,9 +2,20 @@ import json import logging import requests import websockets +import asyncio +import random from requests.auth import HTTPBasicAuth +from time import sleep +requests.adapters.DEFAULT_RETRIES = 5 # increase retries number + +ping_timeout = 180 +ping_interval = 15 +max_queue = 10000 + +min_sleep = 0.5 +max_sleep = 5.0 def decode(r, verbose_errors=True): try: @@ -31,25 +42,78 @@ class Client(object): self.websocket = "ws://%s:%s@%s:%d/api" % (username, password, hostname, port) self.auth = HTTPBasicAuth(username, password) - def session(self): - r = requests.get("%s/session" % self.url, auth=self.auth) + # session takes optional argument to pull a sub-dictionary + # ex.: "session/wifi", "session/ble" + def session(self, sess="session"): + r = requests.get("%s/%s" % (self.url, sess), auth=self.auth) return decode(r) async def start_websocket(self, consumer): s = "%s/events" % self.websocket + + # More modern version of the approach below + # logging.info("Creating new websocket...") + # async for ws in websockets.connect(s): + # try: + # async for msg in ws: + # try: + # await consumer(msg) + # except Exception as ex: + # logging.debug("Error while parsing event (%s)", ex) + # except websockets.exceptions.ConnectionClosedError: + # sleep_time = max_sleep*random.random() + # logging.warning('Retrying websocket connection in {} sec'.format(sleep_time)) + # await asyncio.sleep(sleep_time) + # continue + + # restarted every time the connection fails while True: - try: - async with websockets.connect(s, ping_interval=60, ping_timeout=90) as ws: - async for msg in ws: + logging.info("creating new websocket...") + try: + async with websockets.connect(s, ping_interval=ping_interval, ping_timeout=ping_timeout, max_queue=max_queue) as ws: + # listener loop + while True: try: - await consumer(msg) - except Exception as ex: - logging.debug("Error while parsing event (%s)", ex) - except websockets.exceptions.ConnectionClosedError: - logging.debug("Lost websocket connection. Reconnecting...") - except websockets.exceptions.WebSocketException as wex: - logging.debug("Websocket exception (%s)", wex) + async for msg in ws: + try: + await consumer(msg) + except Exception as ex: + logging.debug("error while parsing event (%s)", ex) + except websockets.exceptions.ConnectionClosedError: + try: + pong = await ws.ping() + await asyncio.wait_for(pong, timeout=ping_timeout) + logging.warning('ping OK, keeping connection alive...') + continue + except: + sleep_time = min_sleep + max_sleep*random.random() + logging.warning('ping error - retrying connection in {} sec'.format(sleep_time)) + await asyncio.sleep(sleep_time) + break + except ConnectionRefusedError: + sleep_time = min_sleep + max_sleep*random.random() + logging.warning('nobody seems to be listening at the bettercap endpoint...') + logging.warning('retrying connection in {} sec'.format(sleep_time)) + await asyncio.sleep(sleep_time) + continue + except OSError: + sleep_time = min_sleep + max_sleep*random.random() + logging.warning('connection to the bettercap endpoint failed...') + logging.warning('retrying connection in {} sec'.format(sleep_time)) + await asyncio.sleep(sleep_time) + continue + def run(self, command, verbose_errors=True): - r = requests.post("%s/session" % self.url, auth=self.auth, json={'cmd': command}) + while True: + try: + r = requests.post("%s/session" % self.url, auth=self.auth, json={'cmd': command}) + except requests.exceptions.ConnectionError as e: + sleep_time = min_sleep + max_sleep*random.random() + logging.warning("can't run my request... connection to the bettercap endpoint failed...") + logging.warning('retrying run in {} sec'.format(sleep_time)) + sleep(sleep_time) + else: + break + return decode(r, verbose_errors=verbose_errors) diff --git a/pwnagotchi/defaults.toml b/pwnagotchi/defaults.toml index cdbb4ce..007e058 100644 --- a/pwnagotchi/defaults.toml +++ b/pwnagotchi/defaults.toml @@ -1,9 +1,9 @@ main.name = "" main.lang = "en" main.confd = "/etc/pwnagotchi/conf.d/" -main.custom_plugins = "" +main.custom_plugins = "/usr/local/share/pwnagotchi/custom-plugins" main.custom_plugin_repos = [ - "https://github.com/evilsocket/pwnagotchi-plugins-contrib/archive/master.zip" + "https://git.chadwaltercummings.me/scifijunkie/pwnagotchi-plugins-contrib.git" ] main.iface = "mon0" main.mon_start_cmd = "/usr/bin/monstart" @@ -18,6 +18,8 @@ main.whitelist = [ ] main.filter = "" +main.log.debug = false + main.plugins.grid.enabled = true main.plugins.grid.report = false main.plugins.grid.exclude = [ @@ -33,7 +35,7 @@ main.plugins.net-pos.api_key = "test" main.plugins.gps.enabled = false main.plugins.gps.speed = 19200 -main.plugins.gps.device = "/dev/ttyUSB0" +main.plugins.gps.device = "/dev/ttyUSB0" # for GPSD: "localhost:2947" main.plugins.webgpsmap.enabled = false @@ -47,6 +49,7 @@ main.plugins.wpa-sec.enabled = false main.plugins.wpa-sec.api_key = "" main.plugins.wpa-sec.api_url = "https://wpa-sec.stanev.org" main.plugins.wpa-sec.download_results = false +main.plugins.wpa-sec.download_interval = 3600 main.plugins.wpa-sec.whitelist = [] main.plugins.wigle.enabled = false @@ -83,7 +86,10 @@ main.plugins.memtemp.scale = "celsius" main.plugins.memtemp.orientation = "horizontal" main.plugins.paw-gps.enabled = false -main.plugins.paw-gps.ip = "" +main.plugins.paw-gps.ip = "192.168.44.1:8080" + +main.plugins.ups_lite.enabled = false +main.plugins.ups_lite.shutdown = 2 main.plugins.gpio_buttons.enabled = false diff --git a/pwnagotchi/log.py b/pwnagotchi/log.py index f13e5d2..e25b345 100644 --- a/pwnagotchi/log.py +++ b/pwnagotchi/log.py @@ -221,7 +221,7 @@ def setup_logging(args, config): formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s") root = logging.getLogger() - root.setLevel(logging.DEBUG if args.debug else logging.INFO) + root.setLevel(logging.DEBUG if args.debug or cfg['debug']==True else logging.INFO) if filename: # since python default log rotation might break session data in different files, @@ -307,3 +307,5 @@ def do_rotate(filename, stats, cfg): with open(log_filename, 'rb') as src: with gzip.open(archive_filename, 'wb') as dst: dst.writelines(src) + + os.remove(log_filename) diff --git a/pwnagotchi/plugins/__init__.py b/pwnagotchi/plugins/__init__.py index 53ee934..f43a414 100644 --- a/pwnagotchi/plugins/__init__.py +++ b/pwnagotchi/plugins/__init__.py @@ -1,17 +1,17 @@ import os import glob -import _thread import threading import importlib, importlib.util import logging - - +from concurrent.futures import ThreadPoolExecutor default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default") loaded = {} database = {} locks = {} +THREAD_POOL_SIZE = 10 +executor = ThreadPoolExecutor(max_workers=THREAD_POOL_SIZE) class Plugin: @classmethod @@ -30,7 +30,6 @@ class Plugin: if cb is not None and callable(cb): locks["%s::%s" % (plugin_name, attr_name)] = threading.Lock() - def toggle_plugin(name, enable=True): """ Load or unload a plugin @@ -69,12 +68,10 @@ def toggle_plugin(name, enable=True): return False - def on(event_name, *args, **kwargs): for plugin_name in loaded.keys(): one(plugin_name, event_name, *args, **kwargs) - def locked_cb(lock_name, cb, *args, **kwargs): global locks @@ -84,7 +81,6 @@ def locked_cb(lock_name, cb, *args, **kwargs): with locks[lock_name]: cb(*args, *kwargs) - def one(plugin_name, event_name, *args, **kwargs): global loaded @@ -96,12 +92,11 @@ def one(plugin_name, event_name, *args, **kwargs): try: lock_name = "%s::%s" % (plugin_name, cb_name) locked_cb_args = (lock_name, callback, *args, *kwargs) - _thread.start_new_thread(locked_cb, locked_cb_args) + executor.submit(locked_cb, *locked_cb_args) except Exception as e: logging.error("error while running %s.%s : %s" % (plugin_name, cb_name, e)) logging.error(e, exc_info=True) - def load_from_file(filename): logging.debug("loading %s" % filename) plugin_name = os.path.basename(filename.replace(".py", "")) @@ -110,7 +105,6 @@ def load_from_file(filename): spec.loader.exec_module(instance) return plugin_name, instance - def load_from_path(path, enabled=()): global loaded, database logging.debug("loading plugins from %s - enabled: %s" % (path, enabled)) @@ -126,7 +120,6 @@ def load_from_path(path, enabled=()): return loaded - def load(config): enabled = [name for name, options in config['main']['plugins'].items() if 'enabled' in options and options['enabled']] diff --git a/pwnagotchi/plugins/cmd.py b/pwnagotchi/plugins/cmd.py index eb87cb9..561191d 100644 --- a/pwnagotchi/plugins/cmd.py +++ b/pwnagotchi/plugins/cmd.py @@ -10,7 +10,7 @@ from pwnagotchi.utils import download_file, unzip, save_config, parse_version, m from pwnagotchi.plugins import default_path -SAVE_DIR = '/usr/local/share/pwnagotchi/availaible-plugins/' +SAVE_DIR = '/usr/local/share/pwnagotchi/available-plugins/' DEFAULT_INSTALL_PATH = '/usr/local/share/pwnagotchi/installed-plugins/' diff --git a/pwnagotchi/plugins/default/auto-update.py b/pwnagotchi/plugins/default/auto-update.py index cf39f54..bc0388a 100644 --- a/pwnagotchi/plugins/default/auto-update.py +++ b/pwnagotchi/plugins/default/auto-update.py @@ -7,14 +7,15 @@ import platform import shutil import glob from threading import Lock +import time import pwnagotchi import pwnagotchi.plugins as plugins from pwnagotchi.utils import StatusFile, parse_version as version_to_tuple -def check(version, repo, native=True): - logging.debug("checking remote version for %s, local is %s" % (repo, version)) +def check_remote_version(version, repo, native=True): + logging.debug("Checking remote version for %s, local is %s" % (repo, version)) info = { 'repo': repo, 'current': version, @@ -24,81 +25,92 @@ def check(version, repo, native=True): 'arch': platform.machine() } - resp = requests.get("https://api.github.com/repos/%s/releases/latest" % repo) - latest = resp.json() - info['available'] = latest_ver = latest['tag_name'].replace('v', '') - is_arm = info['arch'].startswith('arm') + try: + resp = requests.get(f"https://api.github.com/repos/{repo}/releases/latest") + resp.raise_for_status() + latest = resp.json() + info['available'] = latest_ver = latest['tag_name'].replace('v', '') - local = version_to_tuple(info['current']) - remote = version_to_tuple(latest_ver) - if remote > local: - if not native: - info['url'] = "https://github.com/%s/archive/%s.zip" % (repo, latest['tag_name']) - else: - # check if this release is compatible with arm6 - for asset in latest['assets']: - download_url = asset['browser_download_url'] - if download_url.endswith('.zip') and ( - info['arch'] in download_url or (is_arm and 'armhf' in download_url)): - info['url'] = download_url - break + is_arm = info['arch'].startswith('arm') + local = version_to_tuple(info['current']) + remote = version_to_tuple(latest_ver) + + if remote > local: + if not native: + info['url'] = f"https://github.com/{repo}/archive/{latest['tag_name']}.zip" + else: + for asset in latest['assets']: + download_url = asset['browser_download_url'] + if download_url.endswith('.zip') and ( + info['arch'] in download_url or (is_arm and 'armhf' in download_url)): + info['url'] = download_url + break + except Exception as e: + logging.error(f"Error checking remote version for {repo}: {e}") return info def make_path_for(name): path = os.path.join("/tmp/updates/", name) - if os.path.exists(path): - logging.debug("[update] deleting %s" % path) - shutil.rmtree(path, ignore_errors=True, onerror=None) - os.makedirs(path) + try: + if os.path.exists(path): + logging.debug("[update] Deleting %s" % path) + shutil.rmtree(path, ignore_errors=True, onerror=None) + os.makedirs(path) + except Exception as e: + logging.error(f"Error creating path for {name}: {e}") return path def download_and_unzip(name, path, display, update): - target = "%s_%s.zip" % (name, update['available']) + target = f"{name}_{update['available']}.zip" target_path = os.path.join(path, target) - logging.info("[update] downloading %s to %s ..." % (update['url'], target_path)) - display.update(force=True, new_data={'status': 'Downloading %s %s ...' % (name, update['available'])}) + try: + logging.info("[update] Downloading %s to %s ..." % (update['url'], target_path)) + display.update(force=True, new_data={'status': f'Downloading {name} {update["available"]} ...'}) + subprocess.run(['wget', '-q', update['url'], '-O', target_path], check=True) - os.system('wget -q "%s" -O "%s"' % (update['url'], target_path)) + logging.info("[update] Extracting %s to %s ..." % (target_path, path)) + display.update(force=True, new_data={'status': f'Extracting {name} {update["available"]} ...'}) + subprocess.run(['unzip', target_path, '-d', path], check=True) - logging.info("[update] extracting %s to %s ..." % (target_path, path)) - display.update(force=True, new_data={'status': 'Extracting %s %s ...' % (name, update['available'])}) - - os.system('unzip "%s" -d "%s"' % (target_path, path)) + except Exception as e: + logging.error(f"Error downloading and unzipping {name} update: {e}") def verify(name, path, source_path, display, update): - display.update(force=True, new_data={'status': 'Verifying %s %s ...' % (name, update['available'])}) + display.update(force=True, new_data={'status': f'Verifying {name} {update["available"]} ...'}) - checksums = glob.glob("%s/*.sha256" % path) - if len(checksums) == 0: - if update['native']: - logging.warning("[update] native update without SHA256 checksum file") - return False + try: + checksums = glob.glob(f"{path}/*.sha256") + if len(checksums) == 0: + if update['native']: + logging.warning("[update] Native update without SHA256 checksum file") + return False + else: + checksum = checksums[0] + logging.info(f"[update] Verifying {checksum} for {source_path} ...") - else: - checksum = checksums[0] + with open(checksum, 'rt') as fp: + expected = fp.read().split('=')[1].strip().lower() - logging.info("[update] verifying %s for %s ..." % (checksum, source_path)) + real = subprocess.getoutput(f'sha256sum "{source_path}"').split(' ')[0].strip().lower() - with open(checksum, 'rt') as fp: - expected = fp.read().split('=')[1].strip().lower() + if real != expected: + logging.warning(f"[update] Checksum mismatch for {source_path}: expected={expected} got={real}") + return False - real = subprocess.getoutput('sha256sum "%s"' % source_path).split(' ')[0].strip().lower() - - if real != expected: - logging.warning("[update] checksum mismatch for %s: expected=%s got=%s" % (source_path, expected, real)) - return False + except Exception as e: + logging.error(f"Error verifying {name} update: {e}") + return False return True def install(display, update): name = update['repo'].split('/')[1] - path = make_path_for(name) download_and_unzip(name, path, display, update) @@ -107,37 +119,70 @@ def install(display, update): if not verify(name, path, source_path, display, update): return False - logging.info("[update] installing %s ..." % name) - display.update(force=True, new_data={'status': 'Installing %s %s ...' % (name, update['available'])}) + try: + logging.info("[update] Installing %s ..." % name) + display.update(force=True, new_data={'status': f'Installing {name} {update["available"]} ...'}) - if update['native']: - dest_path = subprocess.getoutput("which %s" % name) - if dest_path == "": - logging.warning("[update] can't find path for %s" % name) - return False + if update['native']: + dest_path = subprocess.getoutput(f"which {name}") + if dest_path == "": + logging.warning(f"[update] Can't find path for {name}") + return False - logging.info("[update] stopping %s ..." % update['service']) - os.system("service %s stop" % update['service']) - os.system("mv %s %s" % (source_path, dest_path)) - logging.info("[update] restarting %s ..." % update['service']) - os.system("service %s start" % update['service']) - else: - if not os.path.exists(source_path): - source_path = "%s-%s" % (source_path, update['available']) + logging.info(f"[update] Stopping {update['service']} ...") + subprocess.run(["service", update['service'], "stop"], check=True) - # setup.py is going to install data files for us - os.system("cd %s && pip3 install ." % source_path) + subprocess.run(["mv", source_path, dest_path], check=True) + logging.info(f"[update] Restarting {update['service']} ...") + subprocess.run(["service", update['service'], "start"], check=True) + else: + if not os.path.exists(source_path): + source_path = f"{source_path}-{update['available']}" + + subprocess.run(["cd", source_path, "&&", "pip3", "install", "."], check=True, shell=True) + + except Exception as e: + logging.error(f"Error installing {name} update: {e}") + return False return True def parse_version(cmd): - out = subprocess.getoutput(cmd) - for part in out.split(' '): - part = part.replace('v', '').strip() - if re.search(r'^\d+\.\d+\.\d+.*$', part): - return part - raise Exception('could not parse version from "%s": output=\n%s' % (cmd, out)) + try: + out = subprocess.getoutput(cmd) + for part in out.split(' '): + part = part.replace('v', '').strip() + if re.search(r'^\d+\.\d+\.\d+.*$', part): + return part + except Exception as e: + logging.error(f"Error parsing version from '{cmd}': {e}") + raise Exception(f'Could not parse version from "{cmd}": output=\n{out}') + + +def check_remote_version_with_retry(version, repo, native=True, max_retries=3): + retries = 0 + while retries < max_retries: + try: + resp = requests.get(f"https://api.github.com/repos/{repo}/releases/latest") + resp.raise_for_status() + latest = resp.json() + return check_remote_version(version, repo, native) + except requests.exceptions.HTTPError as e: + if e.response.status_code == 403: + wait_time = 2 ** retries + print(f"Rate limit exceeded. Retrying after {wait_time} seconds...") + time.sleep(wait_time) + retries += 1 + else: + print(f"Error checking remote version for {repo}: {e}") + raise e + except requests.exceptions.ConnectionError as ce: + wait_time = 2 ** retries + print(f"Connection error. Retrying after {wait_time} seconds...") + time.sleep(wait_time) + retries += 1 + raise Exception(f"Failed to check remote version for {repo} after {max_retries} retries.") class AutoUpdate(plugins.Plugin): @@ -157,23 +202,23 @@ class AutoUpdate(plugins.Plugin): logging.error("[update] main.plugins.auto-update.interval is not set") return self.ready = True - logging.info("[update] plugin loaded.") + logging.info("[update] Plugin loaded.") def on_internet_available(self, agent): if self.lock.locked(): return with self.lock: - logging.debug("[update] internet connectivity is available (ready %s)" % self.ready) + logging.debug("[update] Internet connectivity is available (ready %s)" % self.ready) if not self.ready: return if self.status.newer_then_hours(self.options['interval']): - logging.debug("[update] last check happened less than %d hours ago" % self.options['interval']) + logging.debug("[update] Last check happened less than %d hours ago" % self.options['interval']) return - logging.info("[update] checking for updates ...") + logging.info("[update] Checking for updates ...") display = agent.view() prev_status = display.get('status') @@ -189,11 +234,10 @@ class AutoUpdate(plugins.Plugin): ] for repo, local_version, is_native, svc_name in to_check: - info = check(local_version, repo, is_native) + info = check_remote_version_with_retry(local_version, repo, is_native) if info['url'] is not None: logging.warning( - "update for %s available (local version is '%s'): %s" % ( - repo, info['current'], info['url'])) + f"Update for {repo} available (local version is '{info['current']}'): {info['url']}") info['service'] = svc_name to_install.append(info) @@ -207,9 +251,9 @@ class AutoUpdate(plugins.Plugin): if install(display, update): num_installed += 1 else: - prev_status = '%d new update%c available!' % (num_updates, 's' if num_updates > 1 else '') + prev_status = f"{num_updates} new update{'s' if num_updates > 1 else ''} available!" - logging.info("[update] done") + logging.info("[update] Done") self.status.update() diff --git a/pwnagotchi/plugins/default/gps.py b/pwnagotchi/plugins/default/gps.py index 888282d..6167177 100644 --- a/pwnagotchi/plugins/default/gps.py +++ b/pwnagotchi/plugins/default/gps.py @@ -25,7 +25,7 @@ class GPS(plugins.Plugin): logging.info(f"gps plugin loaded for {self.options['device']}") def on_ready(self, agent): - if os.path.exists(self.options["device"]): + if os.path.exists(self.options["device"]) or ":" in self.options["device"]: logging.info( f"enabling bettercap's gps module for {self.options['device']}" ) diff --git a/pwnagotchi/plugins/default/paw-gps.py b/pwnagotchi/plugins/default/paw-gps.py index 58d257e..c4837b1 100644 --- a/pwnagotchi/plugins/default/paw-gps.py +++ b/pwnagotchi/plugins/default/paw-gps.py @@ -10,25 +10,30 @@ GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android class PawGPS(plugins.Plugin): __author__ = 'leont' - __version__ = '1.0.0' + __version__ = '1.0.1' __name__ = 'pawgps' __license__ = 'GPL3' - __description__ = 'Saves GPS coordinates whenever an handshake is captured. The GPS data is get from PAW on android ' + __description__ = 'Saves GPS coordinates whenever an handshake is captured. The GPS data is get from PAW on android.' def on_loaded(self): - logging.info("PAW-GPS loaded") - if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None): - logging.info("PAW-GPS: No IP Address in the config file is defined, it uses the default (192.168.44.1:8080)") + logging.info("[paw-gps] plugin loaded") + if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None) or (len('ip' in self.options and self.options['ip']) is 0): + logging.info("[paw-gps] no IP Address defined in the config file, will uses paw server default (192.168.44.1:8080)") def on_handshake(self, agent, filename, access_point, client_station): - if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None): + if 'ip' not in self.options or ('ip' in self.options and self.options['ip'] is None or (len('ip' in self.options and self.options['ip']) is 0)): ip = "192.168.44.1:8080" else: ip = self.options['ip'] - gps = requests.get('http://' + ip + '/gps.xhtml') - gps_filename = filename.replace('.pcap', '.paw-gps.json') - - logging.info("saving GPS to %s (%s)" % (gps_filename, gps)) - with open(gps_filename, 'w+t') as f: - f.write(gps.text) + try: + gps = requests.get('http://' + ip + '/gps.xhtml') + try: + gps_filename = filename.replace('.pcap', '.paw-gps.json') + logging.info("[paw-gps] saving GPS data to %s" % (gps_filename)) + with open(gps_filename, 'w+t') as f: + f.write(gps.text) + except Exception as error: + logging.error(f"[paw-gps] encountered error while saving gps data: {error}") + except Exception as error: + logging.error(f"[paw-gps] encountered error while getting gps data: {error}") diff --git a/pwnagotchi/plugins/default/ups_lite.py b/pwnagotchi/plugins/default/ups_lite.py index c439636..de14f77 100644 --- a/pwnagotchi/plugins/default/ups_lite.py +++ b/pwnagotchi/plugins/default/ups_lite.py @@ -11,6 +11,7 @@ # To display external power supply status you need to bridge the necessary pins on the UPS-Lite board. See instructions in the UPS-Lite repo. import logging import struct +import subprocess import RPi.GPIO as GPIO @@ -28,11 +29,16 @@ class UPS: import smbus # 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1) self._bus = smbus.SMBus(1) + # Version v1.1 and v1.2 + self.address = 0x36 + if subprocess.run(['i2cget', '-y', '1', '0x62']).returncode == 0: + # Version v1.3 + self.address = 0X62 + self._bus.write_word_data(self.address, 0X0A, 0x30) def voltage(self): try: - address = 0x36 - read = self._bus.read_word_data(address, 2) + read = self._bus.read_word_data(self.address, 2) swapped = struct.unpack("H", read))[0] return swapped * 1.25 / 1000 / 16 except: @@ -40,8 +46,7 @@ class UPS: def capacity(self): try: - address = 0x36 - read = self._bus.read_word_data(address, 4) + read = self._bus.read_word_data(self.address, 4) swapped = struct.unpack("H", read))[0] return swapped / 256 except: @@ -60,7 +65,7 @@ class UPSLite(plugins.Plugin): __author__ = 'evilsocket@gmail.com' __version__ = '1.0.0' __license__ = 'GPL3' - __description__ = 'A plugin that will add a voltage indicator for the UPS Lite v1.1' + __description__ = 'A plugin that will add a voltage indicator for the UPS Lite v1.1, v1.2, v1.3' def __init__(self): self.ups = None diff --git a/pwnagotchi/plugins/default/wigle.py b/pwnagotchi/plugins/default/wigle.py index c8c4b38..9ea4f75 100644 --- a/pwnagotchi/plugins/default/wigle.py +++ b/pwnagotchi/plugins/default/wigle.py @@ -85,7 +85,6 @@ def _send_to_wigle(lines, api_key, donate=True, timeout=30): 'Accept': 'application/json'} data = {'donate': 'on' if donate else 'false'} payload = {'file': dummy, 'type': 'text/csv'} - try: res = requests.post('https://api.wigle.net/api/v2/file/upload', data=data, @@ -141,7 +140,7 @@ class Wigle(plugins.Plugin): all_files = os.listdir(handshake_dir) all_gps_files = [os.path.join(handshake_dir, filename) for filename in all_files - if filename.endswith('.gps.json' or filename.endswith('.paw-gps.json') or filename.endswith('.geo.json')] + if filename.endswith('.gps.json') or filename.endswith('.paw-gps.json') or filename.endswith('.geo.json')] all_gps_files = remove_whitelisted(all_gps_files, self.options['whitelist']) new_gps_files = set(all_gps_files) - set(reported) - set(self.skip) diff --git a/pwnagotchi/plugins/default/wpa-sec.py b/pwnagotchi/plugins/default/wpa-sec.py index 6b6163b..4c62615 100644 --- a/pwnagotchi/plugins/default/wpa-sec.py +++ b/pwnagotchi/plugins/default/wpa-sec.py @@ -132,7 +132,8 @@ class WpaSec(plugins.Plugin): cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile') if os.path.exists(cracked_file): last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file)) - if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1: + download_interval = int(self.options['download_interval']) + if last_check is not None and ((datetime.now() - last_check).seconds / download_interval) < 1: return try: self._download_from_wpasec(os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')) diff --git a/pwnagotchi/ui/display.py b/pwnagotchi/ui/display.py index ab0f3a1..5180bb6 100644 --- a/pwnagotchi/ui/display.py +++ b/pwnagotchi/ui/display.py @@ -40,9 +40,15 @@ class Display(View): def is_waveshare_v3(self): return self._implementation.name == 'waveshare_3' + def is_waveshare_v4(self): + return self._implementation.name == 'waveshare_4' + def is_waveshare27inch(self): return self._implementation.name == 'waveshare27inch' + def is_waveshare27inchv2(self): + return self._implementation.name == 'waveshare27inchv2' + def is_waveshare29inch(self): return self._implementation.name == 'waveshare29inch' @@ -67,15 +73,24 @@ class Display(View): def is_waveshare213d(self): return self._implementation.name == 'waveshare213d' + def is_waveshare213g(self): + return self._implementation.name == 'waveshare213g' + def is_waveshare213bc(self): return self._implementation.name == 'waveshare213bc' + def is_waveshare213inb_v4(self): + return self._implementation.name == 'waveshare213inb_v4' + def is_waveshare35lcd(self): return self._implementation.name == 'waveshare35lcd' def is_spotpear24inch(self): return self._implementation.name == 'spotpear24inch' + def is_displayhatmini(self): + return self._implementation.name == 'displayhatmini' + def is_waveshare_any(self): return self.is_waveshare_v1() or self.is_waveshare_v2() diff --git a/pwnagotchi/ui/hw/__init__.py b/pwnagotchi/ui/hw/__init__.py index eb9beec..f70fc77 100644 --- a/pwnagotchi/ui/hw/__init__.py +++ b/pwnagotchi/ui/hw/__init__.py @@ -7,14 +7,19 @@ from pwnagotchi.ui.hw.dfrobot2 import DFRobotV2 from pwnagotchi.ui.hw.waveshare1 import WaveshareV1 from pwnagotchi.ui.hw.waveshare2 import WaveshareV2 from pwnagotchi.ui.hw.waveshare3 import WaveshareV3 +from pwnagotchi.ui.hw.waveshare4 import WaveshareV4 from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch +from pwnagotchi.ui.hw.waveshare27inchv2 import Waveshare27inchV2 from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch from pwnagotchi.ui.hw.waveshare144lcd import Waveshare144lcd from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch from pwnagotchi.ui.hw.waveshare213d import Waveshare213d +from pwnagotchi.ui.hw.waveshare213g import Waveshare213g from pwnagotchi.ui.hw.waveshare213bc import Waveshare213bc +from pwnagotchi.ui.hw.waveshare213inb_v4 import Waveshare213bV4 from pwnagotchi.ui.hw.waveshare35lcd import Waveshare35lcd from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch +from pwnagotchi.ui.hw.displayhatmini import DisplayHatMini def display_for(config): # config has been normalized already in utils.load_config @@ -45,9 +50,15 @@ def display_for(config): elif config['ui']['display']['type'] == 'waveshare_3': return WaveshareV3(config) + elif config['ui']['display']['type'] == 'waveshare_4': + return WaveshareV4(config) + elif config['ui']['display']['type'] == 'waveshare27inch': return Waveshare27inch(config) + elif config['ui']['display']['type'] == 'waveshare27inchv2': + return Waveshare27inchV2(config) + elif config['ui']['display']['type'] == 'waveshare29inch': return Waveshare29inch(config) @@ -60,11 +71,20 @@ def display_for(config): elif config['ui']['display']['type'] == 'waveshare213d': return Waveshare213d(config) + elif config['ui']['display']['type'] == 'waveshare213g': + return Waveshare213g(config) + elif config['ui']['display']['type'] == 'waveshare213bc': return Waveshare213bc(config) + elif config['ui']['display']['type'] == 'waveshare213inb_v4': + return Waveshare213bV4(config) + elif config['ui']['display']['type'] == 'waveshare35lcd': return Waveshare35lcd(config) elif config['ui']['display']['type'] == 'spotpear24inch': return Spotpear24inch(config) + + elif config['ui']['display']['type'] == 'displayhatmini': + return DisplayHatMini(config) diff --git a/pwnagotchi/ui/hw/base.py b/pwnagotchi/ui/hw/base.py index 72793b0..f4cdd8a 100644 --- a/pwnagotchi/ui/hw/base.py +++ b/pwnagotchi/ui/hw/base.py @@ -3,6 +3,8 @@ import pwnagotchi.ui.fonts as fonts class DisplayImpl(object): def __init__(self, config, name): + if fonts.Medium is None: + fonts.init(config) self.name = name self.config = config['ui']['display'] self._layout = { diff --git a/pwnagotchi/ui/hw/libs/waveshare/v3/epd2in13_V3.py b/pwnagotchi/ui/hw/libs/waveshare/v3/epd2in13_V3.py index ba5eb89..7d43041 100644 --- a/pwnagotchi/ui/hw/libs/waveshare/v3/epd2in13_V3.py +++ b/pwnagotchi/ui/hw/libs/waveshare/v3/epd2in13_V3.py @@ -46,7 +46,7 @@ class EPD: self.cs_pin = epdconfig.CS_PIN self.width = EPD_WIDTH self.height = EPD_HEIGHT - + lut_partial_update= [ 0x0,0x40,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, 0x80,0x80,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, @@ -90,7 +90,7 @@ class EPD: 0x22,0x22,0x22,0x22,0x22,0x22,0x0,0x0,0x0, 0x22,0x17,0x41,0x0,0x32,0x36, ] - + ''' function :Hardware reset parameter: @@ -124,7 +124,7 @@ class EPD: epdconfig.digital_write(self.cs_pin, 0) epdconfig.spi_writebyte([data]) epdconfig.digital_write(self.cs_pin, 1) - + ''' function :Wait until the busy_pin goes LOW parameter: @@ -144,7 +144,7 @@ class EPD: self.send_data(0xC7) self.send_command(0x20) # Activate Display Update Sequence self.ReadBusy() - + ''' function : Turn On Display Part parameter: @@ -154,7 +154,7 @@ class EPD: self.send_data(0x0f) # fast:0x0c, quality:0x0f, 0xcf self.send_command(0x20) # Activate Display Update Sequence self.ReadBusy() - + ''' function : Set lut parameter: @@ -165,7 +165,7 @@ class EPD: for i in range(0, 153): self.send_data(lut[i]) self.ReadBusy() - + ''' function : Send lut data and configuration parameter: @@ -183,7 +183,7 @@ class EPD: self.send_data(lut[157]) # VSL self.send_command(0x2c) # VCOM self.send_data(lut[158]) - + ''' function : Setting the display window parameter: @@ -197,7 +197,7 @@ class EPD: # x point must be the multiple of 8 or the last 3 bits will be ignored self.send_data((x_start>>3) & 0xFF) self.send_data((x_end>>3) & 0xFF) - + self.send_command(0x45) # SET_RAM_Y_ADDRESS_START_END_POSITION self.send_data(y_start & 0xFF) self.send_data((y_start >> 8) & 0xFF) @@ -214,11 +214,11 @@ class EPD: self.send_command(0x4E) # SET_RAM_X_ADDRESS_COUNTER # x point must be the multiple of 8 or the last 3 bits will be ignored self.send_data(x & 0xFF) - + self.send_command(0x4F) # SET_RAM_Y_ADDRESS_COUNTER self.send_data(y & 0xFF) self.send_data((y >> 8) & 0xFF) - + ''' function : Initialize the e-Paper register parameter: @@ -228,7 +228,7 @@ class EPD: return -1 # EPD hardware init start self.reset() - + self.ReadBusy() self.send_command(0x12) #SWRESET self.ReadBusy() @@ -237,25 +237,25 @@ class EPD: self.send_data(0xf9) self.send_data(0x00) self.send_data(0x00) - + self.send_command(0x11) #data entry mode self.send_data(0x03) self.SetWindow(0, 0, self.width-1, self.height-1) self.SetCursor(0, 0) - + self.send_command(0x3c) self.send_data(0x05) self.send_command(0x21) # Display update control self.send_data(0x00) self.send_data(0x80) - + self.send_command(0x18) self.send_data(0x80) - + self.ReadBusy() - + self.SetLut(self.lut_full_update) return 0 @@ -279,7 +279,7 @@ class EPD: buf = bytearray(img.tobytes('raw')) return buf - + ''' function : Sends the image buffer in RAM to e-Paper and displays parameter: @@ -296,7 +296,7 @@ class EPD: for i in range(0, linewidth): self.send_data(image[i + j * linewidth]) self.TurnOnDisplay() - + ''' function : Sends the image buffer in RAM to e-Paper and partial refresh parameter: @@ -311,7 +311,7 @@ class EPD: epdconfig.digital_write(self.reset_pin, 0) epdconfig.delay_ms(1) epdconfig.digital_write(self.reset_pin, 1) - + self.SetLut(self.lut_partial_update) self.send_command(0x37) self.send_data(0x00) @@ -335,7 +335,7 @@ class EPD: self.SetWindow(0, 0, self.width - 1, self.height - 1) self.SetCursor(0, 0) - + self.send_command(0x24) # WRITE_RAM for j in range(0, self.height): for i in range(0, linewidth): @@ -357,13 +357,13 @@ class EPD: for j in range(0, self.height): for i in range(0, linewidth): self.send_data(image[i + j * linewidth]) - + self.send_command(0x26) for j in range(0, self.height): for i in range(0, linewidth): self.send_data(image[i + j * linewidth]) self.TurnOnDisplay() - + ''' function : Clear screen parameter: @@ -374,12 +374,12 @@ class EPD: else: linewidth = int(self.width/8) + 1 # logger.debug(linewidth) - + self.send_command(0x24) for j in range(0, self.height): for i in range(0, linewidth): self.send_data(color) - + self.TurnOnDisplay() ''' @@ -389,7 +389,7 @@ class EPD: def sleep(self): self.send_command(0x10) #enter deep sleep self.send_data(0x01) - + epdconfig.delay_ms(2000) epdconfig.module_exit() diff --git a/pwnagotchi/ui/hw/waveshare3.py b/pwnagotchi/ui/hw/waveshare3.py index f75ee2c..cb9e658 100644 --- a/pwnagotchi/ui/hw/waveshare3.py +++ b/pwnagotchi/ui/hw/waveshare3.py @@ -10,7 +10,7 @@ class WaveshareV3(DisplayImpl): self._display = None def layout(self): - fonts.setup(10, 8, 10, 25, 25, 9) + fonts.setup(10, 8, 10, 35, 25, 9) self._layout['width'] = 250 self._layout['height'] = 122 self._layout['face'] = (0, 40) diff --git a/pwnagotchi/ui/web/templates/base.html b/pwnagotchi/ui/web/templates/base.html index 7097691..e4eed4a 100644 --- a/pwnagotchi/ui/web/templates/base.html +++ b/pwnagotchi/ui/web/templates/base.html @@ -4,6 +4,7 @@ {% block meta %} + {% endblock %} @@ -15,6 +16,8 @@ {% block styles %} + + {% endblock %} diff --git a/pwnagotchi/ui/web/templates/plugins.html b/pwnagotchi/ui/web/templates/plugins.html index 41970fb..3bd16ee 100644 --- a/pwnagotchi/ui/web/templates/plugins.html +++ b/pwnagotchi/ui/web/templates/plugins.html @@ -8,54 +8,61 @@ Plugins {% block styles %} {{ super() }} {% endblock %} {% block script %} -$(function(){ - $('input[type=checkbox]').change(function(e) { + $(function(){ + $('input[type=checkbox]').change(function(e) { var checkbox = $(this); var form = checkbox.closest('form'); var url = form.attr('action'); $.ajax({ - type: 'POST', - url: url, - data: form.serialize(), - success: function(data) { - if( data.indexOf('failed') != -1 ) { + type: 'POST', + url: url, + data: form.serialize(), + success: function(data) { + if (data.indexOf('failed') != -1) { alert('Could not be toggled.'); } - } - }); - }); -}); + } + }); + }); + }); {% endblock %} + {% block content %}
{% for name in database.keys() | sort %} @@ -65,6 +72,11 @@ $(function(){

{{name}}

+ {% if has_info %} + {% if loaded[name].__version__ is defined %} +

v{{ loaded[name].__version__ }}

+ {% endif %} + {% endif %} {% if has_info %} {{ loaded[name].__description__ }} {% else %} diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index 29c3632..3610f0d 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -251,9 +251,15 @@ def load_config(args): elif config['ui']['display']['type'] in ('ws_3', 'ws3', 'waveshare_3', 'waveshare3'): config['ui']['display']['type'] = 'waveshare_3' + elif config['ui']['display']['type'] in ('ws_4', 'ws4', 'waveshare_4', 'waveshare4'): + config['ui']['display']['type'] = 'waveshare_4' + elif config['ui']['display']['type'] in ('ws_27inch', 'ws27inch', 'waveshare_27inch', 'waveshare27inch'): config['ui']['display']['type'] = 'waveshare27inch' + elif config['ui']['display']['type'] in ('ws_27inchv2', 'ws27inchv2', 'waveshare_27inchv2', 'waveshare27inchv2'): + config['ui']['display']['type'] = 'waveshare27inchv2' + elif config['ui']['display']['type'] in ('ws_29inch', 'ws29inch', 'waveshare_29inch', 'waveshare29inch'): config['ui']['display']['type'] = 'waveshare29inch' @@ -275,15 +281,24 @@ def load_config(args): elif config['ui']['display']['type'] in ('ws_213d', 'ws213d', 'waveshare_213d', 'waveshare213d'): config['ui']['display']['type'] = 'waveshare213d' + elif config['ui']['display']['type'] in ('ws_213g', 'ws213g', 'waveshare_213g', 'waveshare213g'): + config['ui']['display']['type'] = 'waveshare213g' + elif config['ui']['display']['type'] in ('ws_213bc', 'ws213bc', 'waveshare_213bc', 'waveshare213bc'): config['ui']['display']['type'] = 'waveshare213bc' + elif config['ui']['display']['type'] in ('ws_213bv4', 'ws213bv4', 'waveshare_213bv4', 'waveshare213inb_v4'): + config['ui']['display']['type'] = 'waveshare213inb_v4' + elif config['ui']['display']['type'] in ('waveshare35lcd'): config['ui']['display']['type'] = 'waveshare35lcd' elif config['ui']['display']['type'] in ('spotpear24inch'): config['ui']['display']['type'] = 'spotpear24inch' + elif config['ui']['display']['type'] in ('displayhatmini'): + config['ui']['display']['type'] = 'displayhatmini' + else: print("unsupported display type %s" % config['ui']['display']['type']) sys.exit(1) diff --git a/requirements.txt b/requirements.txt index 295f7e2..223c792 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,27 +1,194 @@ -pycryptodome==3.9.4 -requests==2.21.0 -PyYAML==5.3.1 -scapy==2.4.3 -gym==0.14.0 -scipy==1.3.1 -stable-baselines==2.7.0 -tensorflow==1.13.1 -tensorflow-estimator==1.14.0 -tweepy==3.7.0 +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --output-file=requirements.txt --pip-args='--retries 50' --resolver=backtracking --strip-extras requirements.in +# +--index-url https://nexus.chadwaltercummings.me/repository/www.piwheels.org/simple +--extra-index-url https://nexus.chadwaltercummings.me/repository/pypi.org/simple + +absl-py==2.1.0 + # via + # tensorboard + # tensorflow +astor==0.8.1 + # via tensorflow +atari-py==0.2.6 + # via gym +certifi==2024.2.2 + # via requests +charset-normalizer==3.3.2 + # via requests +click==7.1.2 + # via flask +cloudpickle==1.6.0 + # via + # gym + # stable-baselines +cycler==0.11.0 + # via matplotlib +dbus-python==1.3.2 + # via -r requirements.in file-read-backwards==2.0.0 -numpy==1.20.2 -inky==1.2.0 -smbus2==0.3.0 -Pillow==6.2.0 -spidev==3.4 -gast==0.2.2 -flask==2.0.1 -flask-cors==3.0.7 -flask-wtf==0.14.3 -dbus-python==1.2.12 -toml==0.10.0 -python-dateutil==2.8.1 + # via -r requirements.in +flask==1.1.4 + # via + # -r requirements.in + # flask-cors + # flask-wtf +flask-cors==3.0.10 + # via -r requirements.in +flask-wtf==1.1.1 + # via -r requirements.in +fonttools==4.38.0 + # via matplotlib +gast==0.5.4 + # via tensorflow +google-pasta==0.2.0 + # via tensorflow +grpcio==1.62.2 + # via + # tensorboard + # tensorflow +gym==0.19.0 + # via + # -r requirements.in + # stable-baselines +h5py==3.8.0 + # via keras-applications +idna==3.7 + # via requests +importlib-metadata==6.7.0 + # via + # gym + # markdown +inky==1.5.0 + # via -r requirements.in +itsdangerous==1.1.0 + # via + # flask + # flask-wtf +jinja2==2.11.3 + # via flask +joblib==1.3.2 + # via stable-baselines +keras-applications==1.0.8 + # via tensorflow +keras-preprocessing==1.1.2 + # via tensorflow +kiwisolver==1.4.5 + # via matplotlib +markdown==3.4.4 + # via tensorboard +markupsafe==2.0.1 + # via + # -r requirements.in + # jinja2 + # wtforms +matplotlib==3.5.3 + # via stable-baselines +numpy==1.21.4 + # via + # -r requirements.in + # atari-py + # gym + # h5py + # inky + # keras-applications + # keras-preprocessing + # matplotlib + # opencv-python + # pandas + # scipy + # stable-baselines + # tensorboard + # tensorflow +opencv-python==4.7.0.72 + # via + # gym + # stable-baselines +packaging==24.0 + # via matplotlib +pandas==1.3.5 + # via stable-baselines +pillow==9.5.0 + # via + # -r requirements.in + # matplotlib +protobuf==3.20.3 + # via + # -r requirements.in + # tensorboard + # tensorflow +pycryptodome==3.20.0 + # via -r requirements.in +pyglet==2.0.10 + # via gym +pyparsing==3.1.2 + # via matplotlib +python-dateutil==2.9.0.post0 + # via + # -r requirements.in + # matplotlib + # pandas +pytz==2024.1 + # via pandas +pyyaml==6.0.1 + # via -r requirements.in +requests==2.31.0 + # via -r requirements.in +scapy==2.5.0 + # via -r requirements.in +scipy==1.7.3 + # via stable-baselines +six==1.16.0 + # via + # atari-py + # flask-cors + # google-pasta + # keras-preprocessing + # python-dateutil + # tensorboard + # tensorflow +smbus2==0.4.3 + # via + # -r requirements.in + # inky +spidev==3.6 + # via + # -r requirements.in + # inky +stable-baselines==2.10.2 + # via -r requirements.in +tensorboard==1.13.1 + # via tensorflow +tensorflow==1.13.1 + # via -r requirements.in +tensorflow-estimator==1.14.0 + # via tensorflow +termcolor==2.3.0 + # via tensorflow +toml==0.10.2 + # via -r requirements.in +typing-extensions==4.7.1 + # via + # importlib-metadata + # kiwisolver +urllib3==2.0.7 + # via requests websockets==8.1 -RPi.GPIO -Werkzeug==2.0.0 -jinja2==3.0.3 + # via -r requirements.in +werkzeug==1.0.1 + # via + # flask + # tensorboard +wheel==0.42.0 + # via + # tensorboard + # tensorflow +wrapt==1.16.0 + # via tensorflow +wtforms==3.0.1 + # via flask-wtf +zipp==3.15.0 + # via importlib-metadata diff --git a/scripts/preview.py b/scripts/preview.py index 4890eca..c564ae2 100755 --- a/scripts/preview.py +++ b/scripts/preview.py @@ -101,6 +101,10 @@ def main(): main: lang: {lang} ui: + font: + name: 'DejaVuSansMono' + size_offset: 0 + size: 0 fps: 0.3 display: enabled: false @@ -110,9 +114,8 @@ def main(): type: {display} web: enabled: true - address: "0.0.0.0" + address: '::' port: 8080 - faces: look_r: '( ⚆_⚆)' look_l: '(☉_☉ )' diff --git a/setup.py b/setup.py index 34df297..82c4ee7 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from setuptools import setup, find_packages -from distutils.util import strtobool -import os +from setuptools.command.install import install import glob -import shutil +import logging +import os import re +import shutil +import warnings +log = logging.getLogger(__name__) def install_file(source_filename, dest_filename): # do not overwrite network configuration if it exists already # https://github.com/evilsocket/pwnagotchi/issues/483 if dest_filename.startswith('/etc/network/interfaces.d/') and os.path.exists(dest_filename): - print("%s exists, skipping ..." % dest_filename) + log.info(f"{dest_filename} exists, skipping ...") return - print("installing %s to %s ..." % (source_filename, dest_filename)) - try: - dest_folder = os.path.dirname(dest_filename) - if not os.path.isdir(dest_folder): - os.makedirs(dest_folder) + log.info(f"installing {source_filename} to {dest_filename} ...") + dest_folder = os.path.dirname(dest_filename) + if not os.path.isdir(dest_folder): + os.makedirs(dest_folder) - shutil.copyfile(source_filename, dest_filename) - except Exception as e: - print("error installing %s: %s" % (source_filename, e)) + shutil.copyfile(source_filename, dest_filename) + if dest_filename.startswith("/usr/bin/"): + os.chmod(dest_filename, 0o755) def install_system_files(): @@ -35,31 +37,54 @@ def install_system_files(): dest_filename = source_filename.replace(data_path, '') install_file(source_filename, dest_filename) + +def restart_services(): # reload systemd units os.system("systemctl daemon-reload") - -def installer(): - install_system_files() # for people updating https://github.com/evilsocket/pwnagotchi/pull/551/files os.system("systemctl enable fstrim.timer") -def version(version_file): - with open(version_file, 'rt') as vf: - version_file_content = vf.read() - version_match = re.search(r"__version__\s*=\s*[\"\']([^\"\']+)", version_file_content) - if version_match: - return version_match.groups()[0] +class CustomInstall(install): + def run(self): + super().run() + if os.geteuid() != 0: + warnings.warn( + "Not running as root, can't install pwnagotchi system files!" + ) + return + install_system_files() + restart_services() + + +def version(version_file): + #with open(version_file, 'rt') as vf: + #version_file_content = vf.read() + + #version_match = re.search(r"__version__\s*=\s*[\"\']([^\"\']+)", version_file_content) + #if version_match: + #return version_match.groups()[0] + + if "PWN_VERSION" in os.environ: + return os.environ["PWN_VERSION"] + else: + with open(version_file, 'rt') as vf: + version_file_content = vf.read() + + version_match = re.search(r"__version__\s*=\s*[\"\']([^\"\']+)", version_file_content) + + if version_match: + return version_match.groups()[0] return None - -if strtobool(os.environ.get("PWNAGOTCHI_ENABLE_INSTALLER", "1")): - installer() - with open('requirements.txt') as fp: - required = [line.strip() for line in fp if line.strip() != ""] + required = [ + line.strip() + for line in fp + if line.strip() and not line.startswith("--") + ] VERSION_FILE = 'pwnagotchi/_version.py' pwnagotchi_version = version(VERSION_FILE) @@ -72,8 +97,11 @@ setup(name='pwnagotchi', url='https://pwnagotchi.ai/', license='GPL', install_requires=required, + cmdclass={ + "install": CustomInstall, + }, scripts=['bin/pwnagotchi'], - package_data={'pwnagotchi': ['defaults.yml', 'pwnagotchi/defaults.yml', 'locale/*/LC_MESSAGES/*.mo']}, + package_data={'pwnagotchi': ['defaults.toml', 'pwnagotchi/defaults.toml', 'locale/*/LC_MESSAGES/*.mo']}, include_package_data=True, packages=find_packages(), classifiers=[