This is a pair of Python programs to use to copy files around on the LAN. The receiver uses IP multicast on the LAN to advertise its interest in the file; the sender then connects to it via TCP to send it.
To send the file, you run in the shell of one machine: $ bccpo.py filetopublish To receive the file, once the sender is running, you run, on the other machine: $ bccpi.py When you’re done, kill the sending process with a control-C. This is very convenient. You only have to type the filename once, on the sending side, where you can use tab-completion. You don’t have to look up or type IP addresses or domain names. You just have to be connected to the same local-area network. Obviously this is not very secure. When you receive a file, you just receive any old file advertised on the local network. The file sender is willing to send the file to anybody who asks. I’ve been thinking I’d make it more secure. Probably like this: 1. Instead of starting the sender first and the receiver later, I’d start the receiver first and have it run until you stop it. The sender would announce its file, transfer it to anybody listening, and then exit. 2. Instead of exiting with an error when an unpleasant filename was found, the receiver would rename the file to something else. 3. Both the sender and the receiver would display secure hashes of the file content, so that you could tell which file you were receiving. 4. Optionally, the connection would be authenticated and encrypted using a challenge-response password protocol such as SRP, so that only authorized receivers could receive and only authorized senders could send. But I haven’t implemented that yet. Here’s `bccpo.py`: #!/usr/bin/python # -*- coding: utf-8 -*- # based on the example from http://wiki.python.org/moin/UdpCommunication import socket, sys, sha, random, thread, struct, os def listen(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', 25456)) # 'cp', 99*256+112 # This looks like it ought to be reasonably portable. The struct # in question holds two 32-bit IPv4 addresses, and Python’s # documentation <http://docs.python.org/library/struct.html> seems # to claim that “l” is always 32 bits in “=” mode. mreq = struct.pack("=4sl", socket.inet_aton("224.3.99.112"), socket.INADDR_ANY) sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) return sock class SurprisingConnectionLoss(Exception): pass class SurprisingResponseString(Exception): pass def get_hello(socket): "Ensure we’re talking to one of our own kind." hello = 'Would you like to play a game?\n' buf = [] while sum(len(x) for x in buf) < len(hello): data = socket.recv(len(hello)) if data == '': raise SurprisingConnectionLoss(socket) buf.append(data) response = ''.join(buf) if response != hello: raise SurprisingResponseString(response) class WrapSock: def __init__(self, sock): self.sock = sock def send(self, data): print '%r >> %r' % (self, data) self.sock.send(data) def recv(self, size): data = self.sock.recv(size) print '%r << %r' % (self, data) return data def connect(ip, port): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((ip, port)) # sock = WrapSock(sock) return sock class Reader: def __init__(self, fileobj): self.fileobj = fileobj self.lock = thread.allocate_lock() def read_at(self, byteoffset, length): self.lock.acquire() try: self.fileobj.seek(byteoffset) return self.fileobj.read(length) finally: self.lock.release() def read(fileobj, byteoffset): fileobj.seek(byteoffset) def netstring(data): return '%d:%s,' % (len(data), data) def answer(filename, reader, ip, packet): def answer_thread(): print 'connecting to %s... ' % ip socket = connect(ip, port=int(packet)) get_hello(socket) print 'connection established to %s ' % ip socket.send(netstring(filename)) byteoffset = 0 while True: data = reader.read_at(byteoffset, 16384) byteoffset += len(data) socket.send(netstring(data)) if data == '': break thread.start_new_thread(answer_thread, ()) def serve(filename, fileobj): sock = listen() print "waiting for requests... " reader = Reader(fileobj) while True: (packet, (ip, port)) = sock.recvfrom(10240) answer(filename, reader, ip, packet) if __name__ == '__main__': filename = sys.argv[1] fileobj = open(filename) serve(os.path.basename(filename), fileobj) (End of `bccpo.py`.) And here’s `bccpi.py`: #!/usr/bin/python # example from http://wiki.python.org/moin/UdpCommunication # 233.252.0.0-233.252.0.255 MCAST-TEST-NET import socket, os class ScaryFilename(Exception): pass class SurprisingConnectionLoss(Exception): pass class SurprisingNonNetstringContent(Exception): pass class UnterminatedNetstring(Exception): pass class FileAlreadyExists(Exception): pass def parse_netstring(buf): if buf == '': return None if buf[0] not in '0123456789': raise SurprisingNonNetstringContent(buf) pos = None for idx in range(1, len(buf)): if buf[idx] == ':': pos = idx break elif buf[idx] not in '0123456789': raise SurprisingNonNetstringContent(buf) if pos is None: # colon not yet received return None length = int(buf[:pos]) if len(buf) < pos + len(':') + length + 1: return None if buf[pos + len(':') + length] != ',': raise SurprisingNonNetstringContent(buf) return (buf[pos+len(':') : pos+len(':')+length], buf[pos+len(':')+length+len(','):]) assert parse_netstring('3:abc,def') == ('abc', 'def') class NetstringConn: def __init__(self, conn): self.conn = conn self.buf = '' def __iter__(self): return self def next(self): results = parse_netstring(self.buf) while not results: data = self.conn.recv(4096) if data == '': if self.buf != '': raise UnterminatedNetstring(self.buf) else: raise StopIteration self.buf += data results = parse_netstring(self.buf) rv, self.buf = results return rv def get_file(): tcp_listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_listener.listen(5) ip, port = tcp_listener.getsockname() mcast_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # TTL=1 for local Ethernet. 0 seems to be localhost-only. mcast_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1) mcast_sock.sendto(str(port), ("224.3.99.112", 25456)) # 'cp' # XXX retry conn, _ = tcp_listener.accept() conn.send('Would you like to play a game?\n') netstrings = NetstringConn(conn) filename = netstrings.next() if '/' in filename: raise ScaryFilename(filename) if os.path.exists(filename): raise FileAlreadyExists(filename) try: get_file_named(netstrings, filename) except: os.unlink(filename) raise def get_file_named(netstrings, filename): print "getting:" print " " + filename # XXX TOCTOU vulnerability here outfile = open(filename, 'w') eof = False for netstring in netstrings: outfile.write(netstring) if netstring == '': eof = True if not eof: raise SurprisingConnectionLoss outfile.close() if __name__ == '__main__': get_file() (End of `bccpi.py`.) This software is available via git clone http://canonical.org/~kragen/sw/inexorable-misc.git (or in <http://canonical.org/~kragen/sw/inexorable-misc>) in the files `bccpo.py` and `bccpi.py`. Like everything else posted to kragen-hacks without a notice to the contrary, this software is in the public domain. -- To unsubscribe: http://lists.canonical.org/mailman/listinfo/kragen-hacks