1
0
mirror of https://github.com/osmarks/random-stuff synced 2025-09-07 04:47:56 +00:00

reorganize LAN chat stuff

This commit is contained in:
2021-06-14 22:27:54 +01:00
parent cf7ba3da11
commit 78602eabd6
3 changed files with 221 additions and 0 deletions

31
lan-chat/bcc.py Normal file
View File

@@ -0,0 +1,31 @@
import socket
import threading
import time
import sys
import os
BROADCAST_IP = "255.255.255.255"
PORT = 44718
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
s.bind(("", PORT))
def recv():
while True:
try:
data, (ip, port) = s.recvfrom(2048)
print(ip + ": " + data.decode("utf8"))
except Exception as e:
print("parse error", e)
threading.Thread(target=recv).start()
try:
while True:
msg = input("> ").encode("utf8")
s.sendto(msg, (BROADCAST_IP, PORT))
time.sleep(0.05)
except KeyboardInterrupt:
os._exit(0)

221
lan-chat/bccplus.py Normal file
View File

@@ -0,0 +1,221 @@
#!/usr/bin/env python3
import socket
import threading
import time
import sys
import subprocess
import os
import collections
import getpass
import re
import random
import ipaddress
import itertools
import struct
PORT = 44718
IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6") if "IPPROTO_IPV6" in dir(socket) else 41 # workaround for weird Windows/old Python quirk
maddr = ("ff15::aeae", 44718)
def configure_multicast(maddr, ifn):
mip, port = maddr
haddr = socket.getaddrinfo("::", port, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1]
maddr = socket.getaddrinfo(mip, port, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1]
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, "SO_REUSEPORT"):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1)
sock.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 5)
ifn = struct.pack("I", ifn)
sock.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifn)
group = socket.inet_pton(socket.AF_INET6, mip) + ifn
sock.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, group)
sock.bind(haddr)
return sock, maddr
def chunks(l, n):
n = max(1, n)
return [l[i:i+n] for i in range(0, len(l), n)]
def recv(q, iface):
s, _ = configure_multicast(maddr, iface)
while True:
data, peer = s.recvfrom(2048)
peer = peer[0].split("%")[0], peer[1]
q.put((data, peer))
def normalize_ip(ip):
return str(ipaddress.ip_address(ip))
def shorthex(x): return "{:04x}".format(x)
def encode_packet(ty, nick, content):
return struct.pack("!BH16s", ty, local_id, nick.encode("utf-8")) + content.encode("utf-8")
def decode_packet(pkt):
ty, local_id, nick = struct.unpack("!BH16s", pkt[:19])
content = pkt[19:]
return ty, local_id, nick.rstrip(b"\0").decode("utf-8"), content.decode("utf-8")
MTY_PING = 0
MTY_MESSAGE = 1
if __name__ == "__main__":
mynick = getpass.getuser() + "@" + socket.gethostname()
peers = {}
local_id = random.randint(0, 0xFFFF)
# dark horrors, TODO refactor into more consistent interface (no pun intended)
if sys.platform.startswith("win32"):
out = subprocess.check_output(["ipconfig"])
match = re.search(b"\n +Link-local IPv6 Address[ .]: ([a-f0-9:])%([0-9]+)", out)
own_ips = {match.group(1)}
iface = int(match.group(2))
for match in re.findall(b"\n +IPv6 Address[ .]: ([a-f0-9:])", out):
own_ips.add(match.group(1))
else:
try:
raise PermissionError
addrs = collections.defaultdict(set)
for line in open("/proc/net/if_inet6").readlines():
addr, ifnum, _, _, _, ifname = line.split()
ifnum = int(ifnum, 16)
addr = normalize_ip(":".join(chunks(addr, 4)))
addrs[ifnum].add(addr)
if ("wlan" in ifname or ifname.startswith("en") or ifname.startswith("eth")) and addr.startswith("fe80"):
iface = ifnum
if not iface: raise SystemExit("No suitable interface found, suffer")
own_ips = addrs[iface]
except PermissionError:
out = subprocess.check_output(["ip", "addr", "show"]).decode("ascii")
addrs = collections.defaultdict(set)
for line in out.split("\n"):
match = re.match("([0-9]+): ([A-Za-z0-9-_]+):", line)
if match:
num, ifname = int(match.group(1)), match.group(2)
current_if = num
if "wlan" in ifname or ifname.startswith("en") or ifname.startswith("eth"):
iface = num
match = re.match(" +inet6 ([a-z0-9:]+)/", line)
if match:
addrs[current_if].add(match.group(1))
own_ips = addrs[iface]
print("IP:", own_ips, "Iface:", ifname or iface, "LocID:", shorthex(local_id))
proc = None
if sys.platform.startswith("win32"):
from multiprocessing import Process, Queue
packet_queue = Queue()
proc = Process(target=recv, args=(packet_queue, iface))
proc.start()
else:
import queue
packet_queue = queue.Queue()
thread = threading.Thread(target=recv, args=(packet_queue, iface)).start()
def queuereader():
while True:
data, (remote_addr, _) = packet_queue.get()
try:
ty, remote_local_id, nick, content = decode_packet(data)
if remote_addr in own_ips and remote_local_id == local_id: continue
peer_id = remote_addr + "/" + shorthex(remote_local_id)
try:
peer = peers[peer_id]
peer["ping_countdown"] = 5
if nick != peer["nick"]:
print("! %s (%s) is now %s" % (peer["nick"], peer_id, nick))
peer["nick"] = nick
except KeyError:
print("! %s (%s) now exists" % (nick, peer_id))
peers[peer_id] = { "nick": nick, "ping_countdown": 5 }
if ty == MTY_MESSAGE:
print(nick + ": " + content)
except Exception as e:
print("Parse error", e)
s, dest = configure_multicast(maddr, iface)
def pinger():
while True:
s.sendto(encode_packet(MTY_PING, mynick, ""), dest)
for id, peer in list(peers.items()):
peer["ping_countdown"] -= 1
if peer["ping_countdown"] <= 0:
del peers[id]
print("! %s (%s) no longer exists" % (peer["nick"], id))
time.sleep(1)
threading.Thread(target=queuereader).start()
threading.Thread(target=pinger).start()
try:
while True:
msg = input("> ")
if msg.startswith("/nick "):
newnick = msg[6:]
if len(newnick.encode("utf-8")) > 16:
print("! Max nick length is 16 bytes")
else:
print("! You are now", newnick)
mynick = newnick
elif msg.startswith("/peer"):
p = ["%s (%s)" % (i, p["nick"]) for i, p in peers.items()]
print("! Peers:", " ".join(p))
else:
s.sendto(encode_packet(MTY_MESSAGE, mynick, msg), dest)
time.sleep(0.05)
except KeyboardInterrupt:
if proc: proc.terminate()
os._exit(0)
# in case of things
"""
AF_INET AddressFamily.AF_INET
AF_INET6 AddressFamily.AF_INET6
IPPROTO_IPV6 41
IPV6_CHECKSUM 7
IPV6_DONTFRAG 62
IPV6_DSTOPTS 59
IPV6_HOPLIMIT 52
IPV6_HOPOPTS 54
IPV6_JOIN_GROUP 20
IPV6_LEAVE_GROUP 21
IPV6_MULTICAST_HOPS 18
IPV6_MULTICAST_IF 17
IPV6_MULTICAST_LOOP 19
IPV6_NEXTHOP 9
IPV6_PATHMTU 61
IPV6_PKTINFO 50
IPV6_RECVDSTOPTS 58
IPV6_RECVHOPLIMIT 51
IPV6_RECVHOPOPTS 53
IPV6_RECVPATHMTU 60
IPV6_RECVPKTINFO 49
IPV6_RECVRTHDR 56
IPV6_RECVTCLASS 66
IPV6_RTHDR 57
IPV6_RTHDRDSTOPTS 55
IPV6_RTHDR_TYPE_0 0
IPV6_TCLASS 67
IPV6_UNICAST_HOPS 16
IPV6_V6ONLY 26
SOL_ALG 279
SOL_HCI 0
SOL_IP 0
SOL_RDS 276
SOL_SOCKET 1
SOL_TCP 6
SOL_TIPC 271
SOL_UDP 17
"""

172
lan-chat/mcc.py Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env python3
import socket
import sys
import select
import time
import struct
import getpass
import asyncio
import prompt_toolkit, prompt_toolkit.widgets, prompt_toolkit.layout.containers, prompt_toolkit.application, prompt_toolkit.layout.layout, prompt_toolkit.key_binding, prompt_toolkit.document
import collections
import random
import netifaces
maddr = ("ff15::aeae", 44718)
def configure_multicast(maddr, ifn):
mip, port = maddr
haddr = socket.getaddrinfo("::", port, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1]
maddr = socket.getaddrinfo(mip, port, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1]
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, "SO_REUSEPORT"):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 5)
ifn = struct.pack("I", ifn)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifn)
group = socket.inet_pton(socket.AF_INET6, mip) + ifn
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, group)
sock.bind(haddr)
sock.setblocking(False)
return sock, maddr
ta = prompt_toolkit.widgets.TextArea()
async def do_inputs(inq, out_callback):
output = prompt_toolkit.widgets.TextArea()
inp = prompt_toolkit.widgets.TextArea(height=1, prompt="[] ", multiline=False, wrap_lines=False)
def accept(buffer):
asyncio.get_event_loop().create_task(out_callback(buffer.text))
inp.accept_handler = accept
container = prompt_toolkit.layout.containers.HSplit([
output,
inp
])
kb = prompt_toolkit.key_binding.KeyBindings()
@kb.add("c-c")
@kb.add("c-q")
def _(event):
sys.exit(0)
event.app.exit()
async def queue_watcher():
while True:
text = output.text + await inq.get() + "\n"
if text.count("\n") > 100:
text = "\n".join(text.split("\n")[1:])
output.buffer.document = prompt_toolkit.document.Document(
text=text, cursor_position=len(text) - 1
)
asyncio.get_event_loop().create_task(queue_watcher())
await prompt_toolkit.Application(
layout=prompt_toolkit.layout.layout.Layout(container, focused_element=inp),
mouse_support=False,
full_screen=False,
key_bindings=kb
).run_async()
loop = asyncio.new_event_loop()
TYPE_PING = 0
TYPE_MESSAGE = 1
local_id = random.randint(0, 0xFFFF)
def shorthex(x): return "{:04x}".format(x)
def encode_packet(ty, nick, content):
return struct.pack("!BH16s", ty, local_id, nick.encode("utf-8")) + content.encode("utf-8")
def decode_packet(pkt):
ty, local_id, nick = struct.unpack("!BH16s", pkt[:19])
content = pkt[19:]
return ty, local_id, nick.rstrip(b"\0").decode("utf-8"), content.decode("utf-8")
Peer = collections.namedtuple("Peer", ["nick", "timeout_counter"])
async def run():
try:
interface = sys.argv[1]
addrs = []
except:
for x in netifaces.interfaces():
if "lo" not in x and netifaces.AF_INET6 in netifaces.ifaddresses(x):
interface = x
break
if not interface:
print("No valid network interface found")
sys.exit(1)
print("Using", interface)
addrs = [a["addr"] for a in netifaces.ifaddresses(interface)[netifaces.AF_INET6]]
ifn = socket.if_nametoindex(interface)
sock, addr = configure_multicast(maddr, ifn)
own_nick = f"{getpass.getuser()}@{socket.gethostname()}"[:16]
peers = {}
def cb():
buf, remote = sock.recvfrom(2048)
ip = remote[0]
try:
ty, lid, nick, content = decode_packet(buf)
if lid == local_id and (addrs == [] or ip in addrs): return
peer_id = ip + "/" + shorthex(lid)
try:
peer = peers[peer_id]
if peer.nick != nick:
inq.put_nowait(f"! {peer.nick} ({peer_id}) is now {nick}")
except KeyError:
inq.put_nowait(f"! {nick} ({peer_id}) exists")
peers[peer_id] = Peer(nick, 10)
if ty == TYPE_MESSAGE:
inq.put_nowait(f"{nick}: {content}")
except Exception as e:
inq.put_nowait(f"! Parse error {e} from {ip}")
loop.add_reader(sock, cb)
inq = asyncio.Queue()
async def outq(s):
if s.startswith("/nick "):
newnick = s[6:]
if len(newnick.encode("utf8")) > 16:
inq.put_nowait("! Max nick length is 16 bytes")
return
inq.put_nowait(f"! You are now {newnick}")
nonlocal own_nick
own_nick = newnick
return
elif s.startswith("/peer"):
p = [f"{repr(p.nick)} ({i})" for i, p in peers.items()]
inq.put_nowait(f"! Peers: {', '.join(p)}")
return
inq.put_nowait(f"{own_nick}: {s}")
sock.sendto(encode_packet(TYPE_MESSAGE, own_nick, s), addr)
loop.create_task(do_inputs(inq, outq))
while True:
sock.sendto(encode_packet(TYPE_PING, own_nick, ""), addr)
rem_queue = []
for peer_id, peer in peers.items():
count = peer.timeout_counter - 1
if count == 0:
rem_queue.append(peer_id)
inq.put_nowait(f"! {peer.nick} ({peer_id}) no longer exists")
else:
peers[peer_id] = Peer(nick=peer.nick, timeout_counter=count)
for r in rem_queue: del peers[r]
await asyncio.sleep(1)
if __name__ == "__main__":
loop.run_until_complete(run())