summaryrefslogtreecommitdiff
path: root/contrib/smsweb/smsws
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/smsweb/smsws')
-rwxr-xr-xcontrib/smsweb/smsws583
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(/&/, "&amp;")
+ data.gsub!(/</, "&lt;")
+ data.gsub!(/>/, "&gt;")
+
+ 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
+