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:
- setup an HTTP basic_auth server that will let me add a command to a list by using the POST method
run a script on a machine inside my home network that will:
- pull the list of commands from the server
- check if the commands match a script in a specific directory
- 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
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(request, response)
@@basic_auth.authenticate(request, response)
end
end
#create a list class to handle calls to /list
class List < AuthChecker
def do_GET (request, response)
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 (request, response)
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?
require 'net/http'
require 'json'
host = "CMD_SERVER_NAME"
username = "reading_user"
password = "reading_password"
command_dir = "./commands"
port = 8888
http = Net::HTTP.new(host, port)
http.start do |http|
req = Net::HTTP::Get.new("/list")
req.basic_auth(username, password)
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_path} does 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"
1. You're adding commands by POST
2. You're sending your password by plaintext?
3. And you're allowing remote execution?
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.