diff options
Diffstat (limited to 'contrib/smsweb/smsws')
-rwxr-xr-x | contrib/smsweb/smsws | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/contrib/smsweb/smsws b/contrib/smsweb/smsws new file mode 100755 index 0000000..bdb593e --- /dev/null +++ b/contrib/smsweb/smsws @@ -0,0 +1,583 @@ +#!/usr/bin/env ruby +# +# vim: set sw=2 ts=2 expandtab: +# +# Copyright (C) 2010 by James Maki +# +# Author: James Maki <jamescmaki@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +#Thread.abort_on_exception = true + +require 'socket' +require 'inform' +require 'yaml' +require 'webrick' +require 'base64' +require 'rexml/document' + +module SMSUtils + SMS_ERR_LOG = "sms.err.log" + + module_function + + def init() + @sms_mutex = Mutex.new + File.unlink(SMS_ERR_LOG) rescue nil + + sms_init() + sms_load_phonebook() + end + + def sms_cmd(cmd) + cmd = "sms #{cmd} 2>>#{SMS_ERR_LOG}" + + Inform.debug "issuing command: #{cmd}" + + @sms_mutex.synchronize { + IO.popen(cmd, "w+") { |pipe| + if block_given? + yield pipe + else + data = pipe.read + Inform.debug "read: #{data.inspect}" + end + } + } + + unless $?.success? + raise "failure: #{cmd}" + else + Inform.debug "success: #{cmd}" + end + + true + end + + def sms_init() + sms_cmd("sms-init") + end + + def sms_load_phonebook + data = nil + sms_cmd("pb-list") { |pipe| + data = pipe.read + } + + @phonebook = YAML.load(data) + end + + def phonebook + @phonebook + end +end + +module WebUtils + private + + def pcdata(data) + data = data.gsub(/&/, "&") + data.gsub!(/</, "<") + data.gsub!(/>/, ">") + + return data + end + + def sql_escape(str) + return nil unless str + return str.gsub(/'/, "''") + end + + # Performs URI escaping so that you can construct proper + # query strings faster. Use this rather than the cgi.rb + # version since it's faster. (Stolen from Camping). + def url_escape(s) + s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) { + '%'+$1.unpack('H2'*$1.size).join('%').upcase + }.tr(' ', '+') + end + module_function :url_escape + + # Unescapes a URI escaped string. (Stolen from Camping). + def url_unescape(s) + s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){ + [$1.delete('%')].pack('H*') + } + end + module_function :url_unescape + + # Stolen from Mongrel: + # Parses a query string by breaking it up at the '&' + # and ';' characters. You can also use this to parse + # cookies by changing the characters used in the second + # parameter (which defaults to '&;'). + + def parse_query(qs, d = '&;') + params = {} + (qs||'').split(/[#{d}] */n).inject(params) { |h,p| + k, v=url_unescape(p).split('=',2) + if cur = params[k] + if cur.class == Array + params[k] << v + else + params[k] = [cur, v] + end + else + params[k] = v + end + } + + return params + end + module_function :parse_query + + def query_params + if @_qp + @_qp + else + @_qp = parse_query(request.query_string) + end + end + + def uri(path) + uri = URI.parse(request.request_uri.to_s) + uri.path = path + uri.query = nil + uri.to_s + end + + def xml_decl + rv = REXML::XMLDecl.new("1.1", "UTF-8") + rv.dowrite + return rv + end + + def xml_response(message, xml) + resp = <<-EOM +<?xml version="1.0" encoding="UTF-8"?> +<response> + <status-message>#{pcdata message}</status-message> +#{xml} +</response> + EOM + + return resp + end + + def verify_identity + request['Authorization'] =~ /^Basic (.+)/ + auth = $1 + + respond_unauthorized unless auth + + user, passwd = Base64.decode64(auth).split(":", 2) + respond_unauthorized unless user && passwd + + if user != $config["smsws"]["user"] || + passwd != $config["smsws"]["passwd"] + respond_unauthorized + end + + return true + end + + [ + {:method => 'ok', :code => 200, :message => 'OK'}, + {:method => 'created', :code => 201, :message => 'Created'}, + {:method => 'accepted', :code => 202, :message => 'Accepted'}, + {:method => 'bad_request', :code => 400, :message => 'Bad Request'}, + {:method => 'unauthorized', :code => 401, :message => 'Unauthorized'}, + {:method => 'forbidden', :code => 403, :message => 'Forbidden'}, + {:method => 'not_found', :code => 404, :message => 'Not Found'}, + {:method => 'method_not_allowed', :code => 405, :message => 'Method Not Allowed'}, + {:method => 'conflict', :code => 409, :message => 'Conflict'}, + {:method => 'gone', :code => 410, :message => 'Gone'}, + {:method => 'too_large', :code => 413, :message => 'Request Entity Too Large'}, + {:method => 'unsupported_media_type', :code => 415, :message => 'Unsupported Media Type'}, + {:method => 'internal_server_error', :code => 500, :message => 'Internal Server Error'}, + {:method => 'not_implemented', :code => 501, :message => 'Not Implemented'}, + {:method => 'service_unavailable', :code => 503, :message => 'Service Unavailable'}, + ].each do |resp| + module_eval <<-EOS + def respond_#{resp[:method]}(options = {}) + options = { + :code => #{resp[:code]}, + :message => '#{resp[:message]}' + }.merge(options) + + respond_with(options) + end + EOS + end + + def respond_with(options = {}) + options = { + :code => 200, + :message => 'OK', + }.merge(options) + + response['Content-Type'] = 'application/xml' + + response['Allow'] = options[:allow] if options[:allow] + + if options[:code] == 401 + response['WWW-Authenticate'] = %(Basic realm="FFWS Authentication required") + end + + xml = xml_response(options[:message], options[:xml]) + respond xml, options[:code] + end + + def response + @response + end + + def request + @request + end + + def handle(request, response) + @response = response + @request = request + + catch(:respond) { + verify_identity() + + begin + yield + rescue Exception => e + respond_internal_server_error(:message => e.to_s) + end + } + end + + def respond(body, status) + response.body = body + response.status = status + + throw :respond + end +end + +SMSWS_PATH = '/smsws/v1' + +class SMSPhonebook < WEBrick::HTTPServlet::AbstractServlet + include WebUtils + + PATH = "#{SMSWS_PATH}/phonebook" + + def do_GET(request, response) + handle(request, response) { + phonebook_get() + } + end + + def entry_to_xml(entry) + return <<EOM +<phonebook-entry> + <uri>#{uri("#{PATH}/#{entry["index"]}")}</uri> + <addr-type>#{entry["type"]}</addr-type> + <addr>#{entry["addr"]}</addr> + <name>#{pcdata entry["name"]}</name> +</phonebook-entry> +EOM + end + + def entry_index + len = PATH.split('/').length + parts = request.path.split('/') + parts.shift(len) + index = parts[0] + index = index.to_i if index + index + end + + def phonebook_get + index = entry_index() + + xml = "" + + list = SMSUtils.phonebook["list"] + list = [] unless list + + list.each do |entry| + unless index + xml << entry_to_xml(entry) + else + if index == entry["index"] + xml << entry_to_xml(entry) + break + end + end + end + + respond_ok(:message => "Success", :xml => xml) + end +end + +class SMSMessages < WEBrick::HTTPServlet::AbstractServlet + include WebUtils + + PATH = "#{SMSWS_PATH}/messages" + + def do_GET(request, response) + handle(request, response) { + messages_get() + } + end + + def do_DELETE(request, response) + handle(request, response) { + messages_delete() + } + end + + def message_to_xml(message) + return <<EOM +<sms-message> + <uri>#{uri("#{PATH}/#{message["index"]}")}</uri> + <message-status>#{message["message-status"]}</message-status> + <addr-type>#{message["pdu"]["addr-type"]}</addr-type> + <addr>#{message["pdu"]["addr"]}</addr> + <user-data> +#{Base64.encode64(message["pdu"]["user-data"])} + </user-data> +</sms-message> +EOM + end + + def message_index + len = PATH.split('/').length + parts = request.path.split('/') + parts.shift(len) + parts[0] + index = index.to_i if index + index + end + + def messages_get + index = message_index() + + data = nil + if index + SMSUtils.sms_cmd("read #{index}") { |pipe| + data = pipe.read + } + else + status = query_params["status"] + Inform.debug "status is #{status}" + + status = "all" unless status + + SMSUtils.sms_cmd("list #{status}") { |pipe| + data = pipe.read + } + end + + document = YAML.load(data) + + messages = document["messages"] + messages = [] unless messages + + xml = "" + messages.each do |message| + xml << message_to_xml(message) + end + + respond_ok(:message => "Success", :xml => xml) + end + + def messages_delete + index = message_index() + + data = nil + if index + SMSUtils.sms_cmd("delete index #{index}") { |pipe| + data = pipe.read + } + else + status = query_params["status"] + Inform.debug "status is #{status}" + + status = "all" unless status + + SMSUtils.sms_cmd("delete #{status}") { |pipe| + data = pipe.read + } + end + + respond_ok(:message => "Deleted") + end +end + +class SMSSend < WEBrick::HTTPServlet::AbstractServlet + include WebUtils + + PATH = "#{SMSWS_PATH}/send" + + def do_POST(request, response) + handle(request, response) { + send_post() + } + end + + def send_post + document = REXML::Document.new(request.body) + root = document.root + + addr = nil + alphabet = "seven-bit" + user_data = "" + + raise ArgumentError, "element is not sms-message" unless root && root.name == "sms-message" + root.each_element { |element| + case element.name + when "addr" + addr = element.text if element.has_text? + when "alphabet" + alphabet = element.text if element.has_text? + when "user-data" + user_data = element.text if element.has_text? + user_data = Base64.decode64(user_data) + end + } + + raise ArgumentError, "addr missing" unless addr + + if addr =~ /[^1234567890*#+-.]/ + list = SMSUtils.phonebook["list"] + list = [] unless list + + list.each do |entry| + if addr == entry["name"] + Inform.debug "#{entry["name"]} => #{entry["addr"]}" + addr = entry["addr"] + end + end + end + + SMSUtils.sms_cmd("send --alphabet #{alphabet} #{addr}") { |pipe| + pipe.write(user_data) + } + respond_created(:message => "Sent") + end +end + +class SMSHelp < WEBrick::HTTPServlet::AbstractServlet + include WebUtils + + def do_GET(request, response) + response['Content-Type'] = 'text/plain' + response.body = <<'EOM' +SMS Web Service Examples + + +Messages: + +opening connection to 192.168.2.1... +opened +<- "GET /smsws/v1/messages HTTP/1.1\r\nAccept: */*\r\nUser-Agent: smswsc/0.1\r\nContent-Type: application/xml\r\nAuthorization: Basic dXNlcjpwYXNzd2Q=\r\nHost: 192.168.2.1:5857\r\n\r\n" +-> "HTTP/1.1 200 OK \r\n" +-> "Connection: Keep-Alive\r\n" +-> "Content-Type: application/xml\r\n" +-> "Date: Mon, 10 May 2010 18:24:26 GMT\r\n" +-> "Server: WEBrick/1.3.1 (Ruby/1.8.7/2009-12-24)\r\n" +-> "Content-Length: 522\r\n" +-> "\r\n" +reading 522 bytes... +-> "" +-> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<response>\n <status-message>Success</status-message>\n<sms-message>\n <uri>http://192.168.2.1:5857/smsws/v1/messages/1</uri>\n <message-status>3</message-status>\n <addr-type>128</addr-type>\n <addr></addr>\n <user-data>\ndGVzdA==\n\n </user-data>\n</sms-message>\n<sms-message>\n <uri>http://192.168.2.1:5857/smsws/v1/messages/2</uri>\n <message-status>1</message-status>\n <addr-type>145</addr-type>\n <addr>13204931234</addr>\n <user-data>\n\n </user-data>\n</sms-message>\n\n</response>\n" +read 522 bytes +Conn keep-alive +[#<SMSWSC::SMSMessage:0x7f78169018a8 @message_status="3", @uri=#<URI::HTTP:0x7f7816900c78 URL:http://192.168.2.1:5857/smsws/v1/messages/1>, @addr="", @user_data="test", @addr_type="128">, #<SMSWSC::SMSMessage:0x7f7816900598 @message_status="1", @uri=#<URI::HTTP:0x7f78168ff968 URL:http://192.168.2.1:5857/smsws/v1/messages/2>, @addr="13204931234", @user_data="", @addr_type="145">] + + +Phonebook: + +opening connection to 192.168.2.1... +opened +<- "GET /smsws/v1/phonebook HTTP/1.1\r\nAccept: */*\r\nUser-Agent: smswsc/0.1\r\nContent-Type: application/xml\r\nAuthorization: Basic dXNlcjpwYXNzd2Q=\r\nHost: 192.168.2.1:5857\r\n\r\n" +-> "HTTP/1.1 200 OK \r\n" +-> "Connection: Keep-Alive\r\n" +-> "Content-Type: application/xml\r\n" +-> "Date: Mon, 10 May 2010 18:24:27 GMT\r\n" +-> "Server: WEBrick/1.3.1 (Ruby/1.8.7/2009-12-24)\r\n" +-> "Content-Length: 445\r\n" +-> "\r\n" +reading 445 bytes... +-> "" +-> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<response>\n <status-message>Success</status-message>\n<phonebook-entry>\n <uri>http://192.168.2.1:5857/smsws/v1/phonebook/1</uri>\n <addr-type>129</addr-type>\n <addr>3204931234</addr>\n <name>jcm</name>\n</phonebook-entry>\n<phonebook-entry>\n <uri>http://192.168.2.1:5857/smsws/v1/phonebook/2</uri>\n <addr-type>129</addr-type>\n <addr>3204931234</addr>\n <name>test</name>\n</phonebook-entry>\n\n</response>\n" +read 445 bytes +Conn keep-alive +[#<SMSWSC::PhonebookEntry:0x7f78168ef3b0 @uri=#<URI::HTTP:0x7f78168ee7d0 URL:http://192.168.2.1:5857/smsws/v1/phonebook/1>, @addr="3204931234", @name="jcm", @addr_type="129">, #<SMSWSC::PhonebookEntry:0x7f78168ee2a8 @uri=#<URI::HTTP:0x7f78168ed6c8 URL:http://192.168.2.1:5857/smsws/v1/phonebook/2>, @addr="3204931234", @name="test", @addr_type="129">] + + +Sending: + +opening connection to 192.168.2.1... +opened +<- "POST /smsws/v1/send HTTP/1.1\r\nAccept: */*\r\nUser-Agent: smswsc/0.1\r\nContent-Type: application/xml\r\nAuthorization: Basic dXNlcjpwYXNzd2Q=\r\nContent-Length: 184\r\nHost: 192.168.2.1:5857\r\n\r\n" +<- "<?xml version=\"1.0\" encoding=\"UTF-8\"?><sms-message><message-status></message-status><addr-type></addr-type><addr>3204931234</addr><user-data>dGVzdCBtZSB3ZWI=\n</user-data></sms-message>" +-> "HTTP/1.1 201 Created \r\n" +-> "Connection: Keep-Alive\r\n" +-> "Content-Type: application/xml\r\n" +-> "Date: Mon, 10 May 2010 18:24:29 GMT\r\n" +-> "Server: WEBrick/1.3.1 (Ruby/1.8.7/2009-12-24)\r\n" +-> "Content-Length: 103\r\n" +-> "\r\n" +reading 103 bytes... +-> "" +-> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<response>\n <status-message>Sent</status-message>\n\n</response>\n" +read 103 bytes +Conn keep-alive + + +EOM + response.status = 200 + end +end + +if $0 == __FILE__ then + SMSWS_ROOT = File.expand_path(File.dirname(__FILE__)) + + Dir.chdir(SMSWS_ROOT) + + SMSWS_CONFIG = "#{SMSWS_ROOT}/sms.config" + $config = YAML::load_file(SMSWS_CONFIG) + + ENV['SMS_CONFIG'] = "#{SMSWS_CONFIG}" + + Inform.syslog = false + if Inform.syslog + Syslog.open("smsws", Syslog::LOG_NDELAY, Syslog::LOG_LOCAL7) + else + Inform.level = Inform::LOG_ERR + #Inform.level = Inform::LOG_DEBUG + Inform.file = $stdout + end + + SMSUtils.init() + + server = WEBrick::HTTPServer.new(:Port => $config["smsws"]["port"]) + server.mount "/", SMSHelp + server.mount SMSPhonebook::PATH, SMSPhonebook + server.mount SMSMessages::PATH, SMSMessages + server.mount SMSSend::PATH, SMSSend + trap "INT" do server.shutdown end + server.start +end + |