mirror of
				https://github.com/osmarks/random-stuff
				synced 2025-10-31 13:53:01 +00:00 
			
		
		
		
	reorganize LAN chat stuff
This commit is contained in:
		
							
								
								
									
										221
									
								
								lan-chat/bccplus.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								lan-chat/bccplus.py
									
									
									
									
									
										Normal 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 | ||||||
|  | """ | ||||||
		Reference in New Issue
	
	Block a user