Thursday 3 April 2014

The poor man's 'static' IP

I recently discovered that to access my Calibre ebook collection on the go was as simple as hitting the "Start Content Server" button, and turning my laptop into a private ebook server.

Actually it wasn't quite that simple.

The problem: I wanted this server to run off my laptop, which is behind a router, behind an ISP. The router gives out dynamic IP addresses to the subnet, and the ISP assigns a dynamic IP address to the router.

The solution:

The first part was easy. I logged into the router and reserved an IP address for my laptop's MAC address. So far so good; now I only had one dynamic IP to worry about.

There are a couple of services which offer to take care of this part for you. For example: no-ip.com or dyn.com/dns. But they either want to charge you money or offer you a watered-down free version of their products. So I went back to the drawing board, which for me is generally a blank Python script. First step, getting my public ip address:

from urllib2 import urlopen
my_public_ip = urlopen("http://wtfismyip.com/text").read()
print my_public_ip.strip()

The homepage of wtfismyip.com is worth a look too.

Now we can run this as a scheduled task or cron job, and send it somewhere accessible. An Email? But we don't want to spam ourselves with an email every five minutes. The easiest way to check if the IP has changed is to save it to a local text file, check the current IP against the stored one, and only send the email if it has changed (I haven't done any error checking, but you should probably add some).

import smtplib
from urllib2 import urlopen

fromaddr = 'yourgmailaccount@gmail.com'
toaddr  = 'yourgmailaccount@gmail.com'

with open("ip.txt") as f:
    old_ip = f.read()

current_ip = urlopen("http://wtfismyip.com/text").read().strip()

if current_ip != old_ip:
    with open("ip.txt", 'w') as f:
        f.write(current_ip)
    msg = ('Subject: IP Change\n\n%s' % current_ip)
    username = 'yourgmailaccount@gmail.com'
    password = 'yourgmailpassword'
    server = smtplib.SMTP('smtp.gmail.com', '587')
    server.starttls()
    server.login(username,password)
    server.sendmail(fromaddr, toaddrs, msg)
    server.quit()

And now whenever my public ip changes, I'll get an email notification and be able to still log in to the ebook server.

Actually, I do have a static IP address from digitalocean.com, but I can't use it for the ebook server due to space restrictions. But they offer a VPS at just $5/month, with a 20GB SSD, 1TB bandwidth and 512MB Ram. Also, I get free stuff if you use the link above ;)

So instead of email, my solution was using my favourite web development combo: a Flask app with MongoDB. Obviously this would be overkill if it weren't all set up already, but for interest's sake, it took very few lines of code to simulate the wtfismyip.com functionality, and to insert my public IP into the mongo database when it changed (using a similar scheduled task to that described above, but without the need to send emails.) Then when visiting the /ebookip route, it returns a hyperlink to my laptop's public ip (with Calibre running on port 8080).

# c is the connection to the Mongo database
@app.route("/updateip")
def update_ip():
        c.ips.remove()
        c.ips.insert({"ip":request.remote_addr})
        return request.remote_addr

@app.route("/ebookip")
def ebookip():
        ip = c.ips.find_one().get("ip")
        return "<a href='http://%s:8080'>%s:8080</a>" % (ip, ip)

@app.route("/whatismyip")
def whatismyip():
        return request.remote_addr

Obviously this is horrible from a security point of view. If anyone visits the /updateip route, it assume that my server is now hosted at their IP (until my scheduled task runs again and resets it to mine). The simplest and worst way around this is to rely on security through obscurity and make the /updateip route much longer. I would not recommend this. Better, add user account authentication, which is surprisingly easy in Flask.

And that's that. If nothing else, I hope that at least the Python Gmail script is useful.