2014-05-12

Due to a limitation of my new horribly wretched Internet Service Provider, I no longer have a static IP address, nor do I have an accessible dynamic IP, and thus my ability to easily access my home network while I am "oot and aboot" is non-existent. If only there were someway I could hack together a software solution to solve my problem...

Oh, surprise surprise, that is exactly what I did. ;) Yea, I put emoticons in blog posts.

The plan went something like this:

  1. setup an HTTP basic_auth server that will let me add a command to a list by using the POST method
  2. run a script on a machine inside my home network that will:

    1. pull the list of commands from the server
    2. check if the commands match a script in a specific directory
    3. run the commands and make a Jezra happy

The most difficult part of this plan seemed to be the creating of the basic_auth server, however, in Ruby there is a really nice standard library module called Webrick that makes it very easy to make a webserver without the need of a third party library, and webrick also has basic_auth support. sweet sauce!

After a quick bit of hacking, I had a working solution.

Enter the Ruby

#!/usr/bin/env ruby
require 'webrick'
require 'json'
require 'yaml'

#commands will be stored in a list
@@commands = []

## build some basic authentication ##
#
#start creating the config
config = { :Realm => 'myCmdProxy' }
#create an Htpasswd database
htpasswd WEBrick::HTTPAuth::Htpasswd.new 'my_password_file'
#add two users to the password config
htpasswd.set_passwd config[:Realm], 'writing_user''writing_password'
htpasswd.set_passwd config[:Realm], 'reading_user''reading_password'
#flush the database (write it to file)
htpasswd.flush
#add the database to the config
config[:UserDB] = htpasswd

#create a global BasicAuth based on the config
@@basic_auth WEBrick::HTTPAuth::BasicAuth.new config

#a class that contains an authenticate method
class AuthChecker WEBrick::HTTPServlet::AbstractServlet
  def authenticate(requestresponse)
    @@basic_auth.authenticate(requestresponse)
  end
end

#create a list class to handle calls to /list
class List AuthChecker
  def do_GET (requestresponse)
    authenticate(request,response)
    #return a list of the alarms pending for execution
    response['Content-Type'] = 'text/json'
    response.body JSON.dump(@@commands)
    #clear the list
    @@commands = []
  end
end

class Add AuthChecker
  def do_POST (requestresponse)
    response['Content-Type'] = 'text/plain'
    @@commands << request.query['command']
    response.body request.query['command']
  end
end

# Initialize our WEBrick server
if $0 == __FILE__
  #create a webrick server
  server WEBrick::HTTPServer.new({:Port=>8888})
  #what urls do we need to mount?
  server.mount('/add'Add)
  server.mount('/list'List)
  #check for user interrupts
  trap "INT" do
    server.shutdown
  end

  #start the server
  server.start
end

For easier copy/paste, the code as been put in the hoof at http://hoof.jezra.net/snip/om. So that I don't accidentally run commands multiple times, the list of commands is truncated whenever the list is requested.

So how do I add commands to the list? By simply opening the terminal on a user-centric device, and running a properly crafted curl command such as
curl -u writing_user:writing_password http://CMD_SERVER_NAME:8888/add -d "command=lights_fade_on.sh"
Oh, that seems like a lot of typing, I better make that a shell script named "proxy_lights_on.sh". Now I just run the script when I'm heading home, and the lights will be on when I get there. sweet sauce!

So the server is up and running, commands are being sent to it, but I still need to read the list of commands, check if the commands exist in a specific directory, and then run the commands. Wooooweeeee!

How about some more Ruby?

#!/usr/bin/env ruby

require 'net/http'
require 'json'

host "CMD_SERVER_NAME"
username "reading_user"
password "reading_password"
command_dir "./commands"
port 8888

http Net::HTTP.new(hostport)
http.start do |http|
  req Net::HTTP::Get.new("/list")
  req.basic_auth(usernamepassword)
  response http.request(req)
  resp response.body
  commands JSON::parse(resp)
  commands.each do |cmd|
    puts cmd
    cmd_path File.join(command_dir,cmd)
    if File.exists? cmd_path
      print`#{cmd_path}`
    else
      puts "Command #{cmd_pathdoes not exist"
    end
  end
end

Yup, this is in the hoof as well: http://hoof.jezra.net/snip/on. This reader script is run every 5 minutes via a cron job. If I needed a more immediate sending of commands, I could increase the cron frequency to run every minute; or better yet, I could have the server emit the commands as a Server Sent Event whenever a command is added. For my needs however, five minutes is fine.

Now quit reading, and go curl -u writing_user:writing_password http://CMD_SERVER_NAME:8888/add -d "command=queue_hella_iron_maiden.sh"

Comments
2014-06-03 Alison Chaiken:
So:
1. You're adding commands by POST
2. You're sending your password by plaintext?
3. And you're allowing remote execution?
2014-06-03 jezra:
in this instance:
1. yes, although I might as well have used GET
2. yes, that is how http basic_auth works, and I didn't feel like getting a cert for this example
3. sort of.
Name:
not required
Email:
not required (will not be displayed)
Website:
not required (will link your name to your site)
Comment:
required
Please do not post HTML code or bbcode unless you want it to show up as code in your post. (or if you are a blog spammer, in which case, you probably aren't reading this anyway).
Prove you are human by solving a math problem! I'm sorry, but due to an increase of blog spam, I've had to implement a CAPTCHA.
Problem:
2 plus 6
Answer:
required
subscribe
 
2019
2016
2015
2014
2013
2012
2011
2010
December
November
October
September
August
July
June
May
April
March
February
January
2009
December
November
October
September
August
July
June
May
April
March
February
January
2008