v1
This commit is contained in:
commit
f4e079fb21
14 changed files with 315 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
__pycache__
|
||||
venv
|
||||
config.py
|
||||
settings.toml
|
||||
29
LICENSE
Normal file
29
LICENSE
Normal 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
32
README.md
Normal 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
102
bisitariak.py
Normal 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
7
config.py.example
Normal 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
127
koloretsua/code.py
Normal 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)
|
||||
BIN
koloretsua/lib/adafruit_connection_manager.mpy
Normal file
BIN
koloretsua/lib/adafruit_connection_manager.mpy
Normal file
Binary file not shown.
0
koloretsua/lib/adafruit_minimqtt/__init__.py
Normal file
0
koloretsua/lib/adafruit_minimqtt/__init__.py
Normal file
BIN
koloretsua/lib/adafruit_minimqtt/adafruit_minimqtt.mpy
Normal file
BIN
koloretsua/lib/adafruit_minimqtt/adafruit_minimqtt.mpy
Normal file
Binary file not shown.
BIN
koloretsua/lib/adafruit_minimqtt/matcher.mpy
Normal file
BIN
koloretsua/lib/adafruit_minimqtt/matcher.mpy
Normal file
Binary file not shown.
BIN
koloretsua/lib/adafruit_ticks.mpy
Normal file
BIN
koloretsua/lib/adafruit_ticks.mpy
Normal file
Binary file not shown.
BIN
koloretsua/lib/neopixel.mpy
Normal file
BIN
koloretsua/lib/neopixel.mpy
Normal file
Binary file not shown.
10
koloretsua/settings.toml.example
Normal file
10
koloretsua/settings.toml.example
Normal 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
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
python-dateutil
|
||||
re
|
||||
paho-mqtt
|
||||
watchfiles
|
||||
Loading…
Add table
Add a link
Reference in a new issue