Fluffy McDeath's Pythonry
Simple ClipSync server

A few years ago, after I got my first Android device, I thought it would be fun to try developing an app (it's not that much fun) and one idea that I was rather taken with was the idea of sharing my clip board between my Android and my computer - pretty much exclusively in that direction for some reason. In particular I wanted to move URLs to my computer so I could read web pages on a better screen. I have since found times when I'd like to transfer clips to my phone but those occasions are fewer. I have a cute hack for that too, but that is for another time.

So, I had this cool idea - and when I have a cool idea that I would like to do, experience has taught me that usually someone else has already had that cool idea and it's just a lot easier to use their solution. Sure enough, a moments searching in the Google store brought up a free app named "ClipSync" which did exactly what I wanted. It was not only a free app but it was also an app that made no use of the "cloud". I don't like the cloud very much and that is for two reasons. The first is that whenever you give your information to the cloud you lose control over it. Once it travels over the internet the data could be anybody's. It's not like my notes and web URLs are particularly incendiary, but google and the web tracking companies are furiously mining my habits for sales opportunities and why volunteer that to yet another company just to share my own information with myself. It also involves sending your information much further than it needs to go just to get from your phone to your PC passing through servers that could even be in different countries before it gets back to you.

ClipSync has a cute video that explains itself quite well.


There was, however, a downside. It would only work to share your clipboard with a PC running Windows! I do not run Windows. I don't want to, and I don't need to, so I don't. But, have no fear, the ClipSync web site declared that the author was working on a linux version so I figured I would just wait. And I waited. Then I waited. And after a while I kind of forgot.

Recently the issue became so infuriating to me once again that I went back to the web site to see if any progress had occurred and absolutely nothing had changed. That's when I wrote an email to the author (as near as I could tell) asking if he would let me know the protocol so I could write the linux server to work with his app. No reply - but then I didn't really wait very long. Instead I downloaded wireshark and borrowed a Windows machine to install the PC version of the server on and then set about watching the conversation on the network.

It turned out that the protocol was unencrypted (and this is good for me and bad if you are cutting and pasting secrets I suppose) and very straight forward. In an hour or so I had a prototype running in tcl (a language I barely know) but it lacked the autodiscovery feature which required UDP and that was not standard as part of the tcl language. I decided to rewrite the server in a language that wouldn't need anyone using it to download all sorts of extra bits and bobs to make it work so I went with Python which is pretty feature rich for programming things and also fairly simple to work in.

What I ended up with is the following version which works just fine for all I need it for though it lacks graceful shut-down and doesn't echo the clip back out to connected clients, but, like I said, it does what I need it to do just fine and so I find little motivation to improve it.

If you are using linux and you currently have python 2.7.something installed in /usr/bin then this should work for you pretty straightforwardly. If you have python installed on MAC or Windows it should work for you as well but perhaps some brave soul could let me know.

First, download this zip file. I know a zip isn't very linuxy but most of these compression schemes are much alike and "zip" is nice and well known among non-linux types and I think pretty unpackable across platforms.

Once you have downloaded the file then you should extract the Clipsy.py file somewhere - I just have it in my home directory. To run it I just open a terminal (which you can do by pressing Ctrl-Alt-T in Ubuntu) and invoke it by typing ./Clipsy.py. It will then wait for a client to attach and print what it receives into the terminal window. Now you can just hide the terminal (don't close it) if you need that bit of screen and start using the program just the same as if you had the ClipSync server running on a PC.

If you want to just see the code without downloading it then take a look below.

This is a reverse engineered quickie bit of code to implement a server compatible with ClipSync

#! /usr/bin/python

# Messages to the server are prepended by two bytes (msb lsb) giving the size of the message
# Messages from the server do not have a size prepended

import socket
import select
import string
import gtk
from struct import *


#clipboard.connect('owner-change', handle_owner_change)

ClipsyMark = '$$898|@'
ClipStart = ClipsyMark + '\'('
ClipEnd = ClipsyMark + '\')'
SrvStart = ClipsyMark + '*['
SrvEnd = ClipsyMark + '*]'

UDP_Query = 'CLIPSYNC SERVER! WHERE ARE YOU !?'
UDP_Response = socket.gethostname()


ServerQuery = 'Can you add me to your clients?'
ServerResponse = 'I can add you to my clients.'

def readSocket(sock):
    msg = ""
    # length is 2 bytes so do some fancy stuff and then
    length = sock.recv(2)
    if length and len(length) == 2:
        length = int(unpack('!h',length)[0]) # '!' means bytes are network order
        print 'Need to read '+str(length)+' characters'
        # We should read with a fairly small buffer so we may have to read repeatedly
        while (len(msg) < length):
            chunk = sock.recv(1024)
            if not chunk:
                break
            msg = msg + chunk
        print 'Msg is '+msg
    else:
        length = -1
    return length - len(msg), msg
    

def snapSrv(msg):
    print 'Snapping server message'
    start = string.find(msg, SrvStart)
    end = string.rfind(msg, SrvEnd)
    print 'start , end is '+str(start)+','+str(end)
    if (end > 0 and start >= 0):
        print 'got srv msg'
        return msg[start + len(SrvStart) : end]
    return ""

def snapClip(msg):
    print 'snapping clip msg'
    start = string.find(msg, ClipStart)
    end = string.rfind(msg, ClipEnd)
    if (end > 0 and start >= 0):
        print 'got clip msg'
        return msg[start + len(ClipStart) : end]
    return ""

ActiveClients={}

def broadcastClip(clip):
    for s in ActiveClients:
        if s != discovery_socket and s != accept_socket:
            s.send(clip)

UDP_PORT = 22985
UDP_REPLY_PORT = 22984
TCP_PORT = 22983

# UDP socket listens for queries from ClipSync clients
discovery_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
discovery_sock.bind(('', UDP_PORT)) #empty string means my address. May not be IPv6 complient
discovery_sock.setblocking(0)

# TCP socket that accepts connections
accept_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
accept_sock.bind(('', TCP_PORT))
accept_sock.setblocking(0)
accept_sock.listen(0)

# stick these on the select list
ActiveClients = [discovery_sock, accept_sock]
Writables=[]
#and wait for a shout out

while (1):
    readable, writable, exceptional = select.select(ActiveClients, Writables, ActiveClients)
    for s in readable:
        if s is discovery_sock:
            in_msg, address = discovery_sock.recvfrom(1024)
            # if it's a clipy request broadcast a response
            s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            discovery_sock.sendto(ClipStart + UDP_Response + ClipEnd, ('255.255.255.255', UDP_REPLY_PORT))
            break

        if s is accept_sock:
            # accept the connection and respond then put the new connection in the list
            new_sock, addr = accept_sock.accept()
            n, in_msg = readSocket(new_sock)
            if (n == 0):
                # message is right length
                msg = snapSrv(in_msg)
                print 'got '+msg
                if (msg == ServerQuery):
                    print 'Replying to ' + str(addr)
                    new_sock.send(SrvStart + ServerResponse + SrvEnd)
                    ActiveClients.append(new_sock)
                    break
            new_sock.close()
            break

        n, in_msg = readSocket(s)
        if (n == 0):
            clip = snapClip(in_msg)
            if (clip):
                # Put the text in the clip board and resend the clip to all servers
                print clip
                cb = gtk.clipboard_get()
                cb.set_text(str(clip))
                cb.store()
                # Not actually sure if this is a good idea - and I haven't added the 
                # "let later starting servers find out if they should be clients" idea
                # AND I don't know if the Clipsy protocol allows symetrical clip setting, else
                # all the other clients would be listen only - therefore broadcastClip is commented out!
                #broadcastClip(clip)

            else:
                msg = snapSrv(in_msg)
                if (msg == "quit"):
                    # we have to take s out of the list and close it
                    ActiveClients.remove(s)
                    s.close()

        else:
            # There is a problem with the bytes read. Connection closed?
            # Just make sure socket is properly closed.
            ActiveClients.remove(s)
            s.close()

# When the server starts up it should first find out if there is another server.
# If there is then it will become a client instead.
Things that could yet be done? I could grab another android and see if it allows reflecting the clip back to a second device. I could make a plugin for Glipper so it integrates with my other clipboard manager on the desktop.