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

Reply via email to