This commit is contained in:
OniriCorpe 2024-09-01 06:16:42 +02:00
commit f4e079fb21
14 changed files with 315 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
__pycache__
venv
config.py
settings.toml

29
LICENSE Normal file
View file

@ -0,0 +1,29 @@
"i'm so tired" software license 1.0
copyright (c) 2024 OniriCorpe
this is anti-capitalist, anti-bigotry software, made by people who are tired of ill-intended organisations and individuals, and would rather not have those around their creations.
permission is granted, free of charge, to any user (be they a person or an organisation) obtaining a copy of this software, to use it for personal, commercial, or educational purposes, subject to the following conditions:
1. the above copyright notice and this permission notice shall be included in all copies or modified versions of this software.
2. the user is one of the following:
a. an individual person, labouring for themselves
b. a non-profit organisation
c. an educational institution
d. an organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor
3. if the user is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote.
4. if the user is an organization, then the user is not law enforcement or military, or working for or under either.
5. the user does not use the software for ill-intentioned reasons, as determined by the authors of the software. said reasons include but are not limited to:
a. bigotry, including but not limited to racism, xenophobia, homophobia, transphobia, ableism, sexism, antisemitism, religious intolerance
b. pedophilia, zoophilia, and/or incest
c. support for cops and/or the military
d. any blockchain-related technology, including but not limited to cryptocurrencies
6. the user does not promote or engage with any of the activities listed in the previous item, and is not affiliated with any group that promotes or engages with any of such activities.
this software is provided as is, without any warranty or condition. in no event shall the authors be liable to anyone for any damages related to this software or this license, under any kind of legal claim.

32
README.md Normal file
View file

@ -0,0 +1,32 @@
# bisitariak & koloretsua
those scripts are used to made a lamp who illuminates with various colors (generated based on visitor's IP address) when someone visit my websites (detection based on nginx logs)
to use them, you need a MQTT broker
if you want to use a public broker: <https://www.maqiatto.com/>
'bisitariak' means 'visitors' and 'koloretsua' means 'colorful' in Basque
## bisitariak
this script monitors nginx access logs and advertises any new visits to a MQTT broker
it comes with it's sibbling script (`koloretsua`) wich turn on a LED strip for each visitor, using an ESP microcontroller
TODO:
- [x] properly detect any new visits
- [ ] FIXME: to test properly
- [x] extract their associated IP address
- [x] generate a color based on this IP address
- [x] advertise new visitors (and their associated color) on the MQTT broker
## koloretsua
this script is using [CircuitPython](https://circuitpython.org/) and [MiniMQTT](https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT)
### install
- flash an ESP card with CircuitPython (i used a Wemos S2 mini)
- put the files from the `koloretsua/lib` directory in the `lib` directory of your ESP card
- put the `code.py` file in the root of your ESP
- copy the `settings.toml.example` file into the root of your ESP, rename it to `settings.toml` and configure it

102
bisitariak.py Normal file
View file

@ -0,0 +1,102 @@
#! /usr/bin/python
from os import path
from dateutil import parser
import datetime
import json
import re
import hashlib
import paho.mqtt.publish as publish
from watchfiles import Change, watch
import config
# config stuff
LOG_DIR = config.LOG_DIR
BROKER_HOST = config.BROKER_HOST
BROKER_PORT = config.BROKER_PORT
BROKER_ACCOUNT = config.BROKER_ACCOUNT
BROKER_PASSWORD = config.BROKER_PASSWORD
BROKER_TOPIC = config.BROKER_TOPIC
last_parse = datetime.datetime.now()
def filter_logs(change: Change, path: str) -> bool:
return path.endswith("access.log")
def parse_nginx_log(log):
lineformat = re.compile(
r"""(?P<ipaddress>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))) - - \[(?P<dateandtime>\d{2}\/[a-z]{3}\/\d{4}:\d{2}:\d{2}:\d{2} (\+|\-)\d{4})\] ((\"(GET|POST|HEAD|PUT|DELETE) )(?P<url>.+)(http\/(1\.1|2\.0)")) (?P<statuscode>\d{3}) (?P<bytessent>\d+) (?P<refferer>-|"([^"]+)") (["](?P<useragent>[^"]+)["])""",
re.IGNORECASE,
)
visits = ()
global last_parse
try:
logfile = open(log)
except Exception as e:
print(f"Error while trying to open the logfile: {e}")
for line in logfile.readlines():
data = re.search(lineformat, line)
if data:
datadict = data.groupdict()
ip = datadict["ipaddress"]
datetimestring = datadict["dateandtime"]
date = parser.parse(datetimestring, fuzzy=True, ignoretz=True)
if last_parse > date:
# if a visitors is from the past (before the script launch or already processed), ignore it
continue
if ip in visits:
# ignore IP adresses that are already seen
continue
# add the IP address to a tuple
visits = (*visits, ip)
# save the parsing datetime for later
last_parse = datetime.datetime.now()
return visits
def to_color(ip_address):
# converts an IP (or any string) to a color code (hex value)
hash = hashlib.shake_256(ip_address.encode(), usedforsecurity=False).digest(3)
# concatenate the 3 hex shit into one ('0x87', '0xc3', '0xd2' to '0x87c3d2')
# #cursedCode
# return hex(((hash[0] << 8) | hash[1]) << 8 | hash[2])
return (int(hash[0]), int(hash[1]), int(hash[2]))
def to_mqtt(payload):
try:
publish.single(
topic=f"{BROKER_TOPIC}/visits",
payload=json.dumps({"color": payload}),
qos=0,
hostname=BROKER_HOST,
port=BROKER_PORT,
auth={"username": BROKER_ACCOUNT, "password": BROKER_PASSWORD},
client_id="bisitariak",
)
except Exception as e:
print(f"Error while trying to publish on the broker: {e}")
### actual script:
for changes in watch(LOG_DIR, watch_filter=filter_logs):
for log in changes:
logfile = log[1]
for ip in parse_nginx_log(logfile):
color = to_color(ip)
to_mqtt(color)
print(f"published color '{color}' for IP '{ip}'")

7
config.py.example Normal file
View file

@ -0,0 +1,7 @@
BROKER_HOST = "broker.example.com" # string, IP or domain-name of your mqtt broker
BROKER_PORT = 1883 # int, port of your mqtt broker
BROKER_ACCOUNT = "account-name" # string
BROKER_PASSWORD = "account-password" # string
BROKER_TOPIC = "bisitariak" # string
LOG_DIR = "/var/log/nginx" # string

127
koloretsua/code.py Normal file
View file

@ -0,0 +1,127 @@
# somewhat based on https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT/blob/main/examples/native_networking/minimqtt_adafruitio_native_networking.py
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
import board
import json
from math import log
import neopixel
import os
import socketpool
import ssl
import time
import wifi
import adafruit_minimqtt.adafruit_minimqtt as MQTT
# config stuff
BROKER_HOST = os.getenv("BROKER_HOST")
BROKER_PORT = os.getenv("BROKER_PORT")
BROKER_ACCOUNT = os.getenv("BROKER_ACCOUNT")
BROKER_PASSWORD = os.getenv("BROKER_PASSWORD")
BROKER_TOPIC = os.getenv("BROKER_TOPIC")
LED_STRIP_NUMBER = os.getenv("LED_STRIP_NUMBER")
strip = neopixel.NeoPixel(board.IO16, LED_STRIP_NUMBER)
global last_color
last_color = (0, 0, 0)
global strip_on
strip_on = 0
print("Connecting to %s" % os.getenv("CIRCUITPY_WIFI_SSID"))
wifi.radio.connect(
os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")
)
print("Connected to %s!" % os.getenv("CIRCUITPY_WIFI_SSID"))
def color_strip(color):
color = tuple(list(color)) # convert string tuple to tuple tuple
strip.fill(color)
# remember the color
global last_color
last_color = color
# remember the time when the strip was enabled
global strip_on
strip_on = time.monotonic()
def fade_color(color):
if color == (0, 0, 0):
return (0, 0, 0)
faded = tuple()
for i in range(len(color)):
if color[i] < 5:
faded_color = 0
elif color[i] > 0:
faded_color = color[i] / log(color[i])
faded = faded + (faded_color,)
return faded
# Define callback methods which are called when events occur
def connected(client, userdata, flags, rc):
print("Connected to MQTT Broker!")
def disconnected(client, userdata, rc):
print("Disconnected from MQTT Broker!")
def subscribe(client, userdata, topic, granted_qos):
print(f"Subscribed to {topic} with QOS level {granted_qos}")
def unsubscribe(client, userdata, topic, pid):
print(f"Unsubscribed from {topic} with PID {pid}")
def on_message(client, topic, message):
print(f"New message by {client} on topic {topic}: {message}")
message = json.loads(message)
if ("color" in message) and (message["color"]):
color_strip(message["color"])
# Create a socket pool
pool = socketpool.SocketPool(wifi.radio)
ssl_context = ssl.create_default_context()
# Set up a MiniMQTT Client
client = MQTT.MQTT(
broker=BROKER_HOST,
port=BROKER_PORT,
username=BROKER_ACCOUNT,
password=BROKER_PASSWORD,
socket_pool=pool,
ssl_context=ssl_context,
)
# Setup the callback methods above
client.on_connect = connected
client.on_disconnect = disconnected
client.on_subscribe = subscribe
client.on_unsubscribe = unsubscribe
client.on_message = on_message
# Connect the client to the MQTT broker.
print("Connecting to MQTT broker...")
client.connect()
# Subscribe to the configured topic
client.subscribe(f"{BROKER_TOPIC}/visits", 0)
# Start a blocking message loop...
# NOTE: NO code below this loop will execute
while True:
client.loop(timeout=1)
if time.monotonic() - strip_on < 5:
# turn off the led strip after ~5 seconds
continue
strip.fill(last_color)
last_color = fade_color(last_color)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
koloretsua/lib/neopixel.mpy Normal file

Binary file not shown.

View file

@ -0,0 +1,10 @@
CIRCUITPY_WIFI_SSID = "WIFI_SSID"
CIRCUITPY_WIFI_PASSWORD = "WIFI_PASSWORD"
BROKER_HOST = "broker.example.com" # string, IP or domain-name of your mqtt broker
BROKER_PORT = 1883 # int, port of your mqtt broker
BROKER_ACCOUNT = "account-name" # string
BROKER_PASSWORD = "account-password" # string
BROKER_TOPIC = "bisitariak" # string
LED_STRIP_NUMBER = 8 # int, number of LED on the strip

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
python-dateutil
re
paho-mqtt
watchfiles