summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--contrib/smsweb/inform.rb131
-rw-r--r--contrib/smsweb/sms.config32
-rwxr-xr-xcontrib/smsweb/smsws583
-rwxr-xr-xcontrib/smsweb/smswsc.rb341
-rw-r--r--contrib/smsweb/smswsc/builder.rb23
-rw-r--r--contrib/smsweb/smswsc/phonebook_entry.rb56
-rw-r--r--contrib/smsweb/smswsc/sms_message.rb62
-rw-r--r--contrib/smsweb/smswsc/utils.rb63
8 files changed, 1291 insertions, 0 deletions
diff --git a/contrib/smsweb/inform.rb b/contrib/smsweb/inform.rb
new file mode 100644
index 0000000..e710cc8
--- /dev/null
+++ b/contrib/smsweb/inform.rb
@@ -0,0 +1,131 @@
+# 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
+#
+
+require 'syslog'
+
+# Inform.syslog = false
+# if Inform.syslog
+# Syslog.open("smsws", Syslog::LOG_NDELAY, Syslog::LOG_LOCAL7)
+# else
+# Inform.level = Inform::LOG_ERR
+# Inform.file = $stdout
+# end
+
+module Inform
+ include Syslog
+
+ LEVELS = {
+ LOG_EMERG => :emerg,
+ LOG_ALERT => :alert,
+ LOG_CRIT => :crit,
+ LOG_ERR => :err,
+ LOG_WARNING => :warning,
+ LOG_NOTICE => :notice,
+ LOG_INFO => :info,
+ LOG_DEBUG => :debug,
+ }
+
+ LEVELS.each_pair do |key, value|
+ module_eval <<-EOS
+ def #{value}(msg)
+ inform(#{key}, msg, caller[0])
+ end
+ EOS
+
+ module_function value
+ end
+
+ module_function
+
+ def level=(level)
+ raise ArgumentError, "bad level" unless level >= LOG_EMERG && level <= LOG_DEBUG
+
+ @level = level
+ end
+
+ def level
+ @level
+ end
+
+ def file=(file)
+ @file = file
+ end
+
+ def file
+ @file
+ end
+
+ def syslog=(syslog)
+ @syslog = syslog
+ end
+
+ def syslog
+ @syslog
+ end
+
+ def inform(level, msg, from = caller[0])
+ time = Time.now.strftime('%Y-%m-%d %H:%M:%S')
+
+ from =~ /(.+)\:(\d+)(?:\:in \`(\S+)\')?/
+ file = File.basename($1)
+ line = $2
+ func = $3
+ func ||= 'main'
+
+ str = "#{time} [#{LEVELS[level].to_s.upcase}] #{file}:#{func}:#{line}: "
+
+ if msg.is_a?(Exception)
+ str << "#{msg.class} #{msg}"
+ else
+ str << "#{msg}"
+ end
+
+ log(level, str)
+
+ if msg.is_a?(Exception)
+ msg.backtrace[0..9].each do |line|
+ log(level, "> #{line}")
+ end
+ end
+ end
+
+ def escape_spec(str)
+ new = ""
+ str.each_byte { |c|
+ #if c == "%".ord
+ if c == ?%
+ new << "%%"
+ else
+ new << c
+ end
+ }
+
+ return new
+ end
+
+ def log(level, msg)
+ if @syslog
+ Syslog.log(level, escape_spec(msg))
+ elsif @level && @file
+ @file.puts(msg) if level <= @level
+ end
+ end
+end
diff --git a/contrib/smsweb/sms.config b/contrib/smsweb/sms.config
new file mode 100644
index 0000000..2ad2ba1
--- /dev/null
+++ b/contrib/smsweb/sms.config
@@ -0,0 +1,32 @@
+user:
+ name: Real Name
+ email: user@gmail.com
+
+core:
+ verbose: false
+ interactive: false
+ sms-init: false
+ baud-rate: 115200
+ read-timeout: 5000
+ device: /dev/ttyS1
+ #device: /dev/rfcomm0
+ msg-store-read: MT
+ msg-store-send: MT
+ msg-store-new: MT
+ pb-store: ME
+ editor: vi
+ edit-file: "${HOME}/.smsmsg"
+
+smtp:
+ server: smtp.gmail.com
+ port: 587
+ user: user@gmail.com
+ encryption: tls
+
+send-email:
+ domain: txt.att.net
+
+smsws:
+ port: 5857
+ user: user
+ passwd: passwd
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
+
diff --git a/contrib/smsweb/smswsc.rb b/contrib/smsweb/smswsc.rb
new file mode 100755
index 0000000..1800e13
--- /dev/null
+++ b/contrib/smsweb/smswsc.rb
@@ -0,0 +1,341 @@
+#!/usr/bin/env ruby1.8
+#
+# vim: set sw=2 ts=2 expandtab:
+#
+# SMS Web Service client example library
+#
+# 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
+#
+
+require 'rubygems'
+require 'net/http'
+require 'cgi'
+require 'inform'
+require 'smswsc/utils'
+require 'smswsc/phonebook_entry'
+require 'smswsc/sms_message'
+
+module SMSWSC
+ module Error
+ class General < RuntimeError; end
+ class HTTP < General; end
+ class XML < General; end
+ class BadRequest < General; end
+ class Unauthorized < General; end
+ class Forbidden < General; end
+ class NotFound < General; end
+ class ServiceUnavailable < General; end
+ end
+
+ SMSWS_PATH = "/smsws/v1"
+ PHONEBOOK_PATH = Utils.ref(SMSWS_PATH, "phonebook")
+ MESSAGES_PATH = Utils.ref(SMSWS_PATH, "messages")
+ SEND_PATH = Utils.ref(SMSWS_PATH, "send")
+
+ class Client
+ include Utils
+
+ DEFAULT_PORT = 5857
+ VERSION = '0.1'
+
+ attr_accessor :use_ssl
+ attr_accessor :http_debug
+ attr_accessor :http_read_timeout
+
+ def initialize(username, passwd, host, port = DEFAULT_PORT)
+ @host = host
+ @port = port
+ @username = username
+ @passwd = passwd
+ @error = ""
+ @headers = {
+ 'Content-Type' => 'application/xml',
+ 'User-Agent' => "smswsc/#{VERSION}",
+ }
+ @use_ssl = false
+ @http_debug = false
+ @http_read_timeout = 300
+ end
+
+ def user_agent(ua)
+ @headers['User-Agent'] = ua
+ end
+
+ def phonebook_query(path = PHONEBOOK_PATH)
+ case path
+ when PhonebookEntry
+ path = path.uri.to_s
+ when String
+ when URI
+ path = path.to_s
+ else
+ raise ArgumentError, "Invalid param type: #{path.class}"
+ end
+
+ phonebook = []
+
+ response = get_resource(path)
+
+ root = process_response(response, Net::HTTPOK)
+
+ root.elements.each("phonebook-entry") do |entry|
+ phonebook << PhonebookEntry.load_xml(entry)
+ end
+
+ return phonebook
+ end
+
+ def sms_messages_query(path = MESSAGES_PATH)
+ case path
+ when SMSMessage
+ path = path.uri.to_s
+ when String
+ when URI
+ path = path.to_s
+ else
+ raise ArgumentError, "Invalid param type: #{path.class}"
+ end
+
+ msgs = []
+
+ response = get_resource(path)
+
+ root = process_response(response, Net::HTTPOK)
+
+ root.elements.each("sms-message") do |entry|
+ msgs << SMSMessage.load_xml(entry)
+ end
+
+ return msgs
+ end
+
+ def sms_messages_delete(path = MESSAGES_PATH)
+ case path
+ when SMSMessage
+ path = path.uri.to_s
+ when String
+ when URI
+ path = path.to_s
+ else
+ raise ArgumentError, "Invalid param type: #{path.class}"
+ end
+
+ response = delete_resource(path)
+
+ root = process_response(response, Net::HTTPOK)
+
+ return true
+ end
+
+ def sms_send(msg)
+ response = post_resource(SEND_PATH, msg.to_xml)
+
+ root = process_response(response, Net::HTTPCreated)
+
+ return true
+ end
+
+ private
+
+ def process_response(response, klass)
+ root = response_xml(response)
+ if root
+ message = root.elements['status-message'].text
+ else
+ message = "response message not given"
+ end
+
+ unless response.instance_of?(klass)
+ if response.instance_of?(Net::HTTPBadRequest)
+ raise Error::BadRequest, "Bad Request: " + message
+ elsif response.instance_of?(Net::HTTPUnauthorized)
+ raise Error::Unauthorized, "Unauthorized: " + message
+ elsif response.instance_of?(Net::HTTPForbidden)
+ raise Error::Forbidden, "Forbidden: " + message
+ elsif response.instance_of?(Net::HTTPNotFound)
+ raise Error::NotFound, "Not Found: " + message
+ elsif response.instance_of?(Net::HTTPServiceUnavailable)
+ raise Error::ServiceUnavailable, "Service Unavailable: " + message
+ else
+ raise Error::General, "Failed to fulfill request: " + message
+ end
+ end
+
+ return root
+ end
+
+ def response_xml(response)
+ return nil unless response.content_type.to_s.downcase == 'application/xml'
+
+ begin
+ document = REXML::Document.new(response.body)
+ root = document.root
+ rescue REXML::ParseException => e
+ Inform.err(e.to_s)
+ raise Error::XML, "failed to parse response XML"
+ end
+
+ return root
+ end
+
+ def authorize(request)
+ if @username && @passwd
+ request.basic_auth(@username, @passwd)
+ end
+ end
+
+ def parse_qpath(url)
+ url = URI.parse(url)
+ qpath = url.path
+ if url.query
+ qpath += "?" + url.query
+ end
+
+ return qpath
+ end
+
+ def net_http
+ http = Net::HTTP.new(@host, @port)
+
+ http.read_timeout = @http_read_timeout
+ http.set_debug_output($stderr) if @http_debug
+
+ if @use_ssl
+ http.use_ssl = @use_ssl
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ end
+
+ return http
+ end
+
+ def post_resource(ref, body, headers = {})
+ headers = @headers.merge(headers)
+ qpath = parse_qpath(ref)
+
+ begin
+ request = Net::HTTP::Post.new(qpath, headers)
+ authorize(request)
+
+ response = net_http.start do |http|
+ http.request(request, body)
+ end
+ rescue => e
+ Inform.err("POST #{ref} failed: #{e.class}: #{e}")
+ raise Error::HTTP, "POST #{ref} failed: #{e}"
+ end
+
+ return response
+ end
+
+ def delete_resource(ref, headers = {})
+ headers = @headers.merge(headers)
+ qpath = parse_qpath(ref)
+
+ begin
+ request = Net::HTTP::Delete.new(qpath, headers)
+ authorize(request)
+
+ response = net_http.start do |http|
+ http.request(request)
+ end
+ rescue => e
+ Inform.err("DELETE #{ref} failed: #{e.class}: #{e}")
+ raise Error::HTTP, "DELETE #{ref} failed: #{e}"
+ end
+
+ return response
+ end
+
+ def get_resource(ref, headers = {})
+ headers = @headers.merge(headers)
+ qpath = parse_qpath(ref)
+
+ begin
+ request = Net::HTTP::Get.new(qpath, headers)
+ authorize(request)
+
+ response = net_http.start do |http|
+ http.request(request)
+ end
+ rescue => e
+ Inform.err("GET #{ref} failed: #{e.class}: #{e}")
+ raise Error::HTTP, "GET #{ref} failed: #{e}"
+ end
+
+ return response
+ end
+
+ def put_resource(ref, body, headers = {})
+ headers = @headers.merge(headers)
+ qpath = parse_qpath(ref)
+
+ begin
+ request = Net::HTTP::Put.new(qpath, headers)
+ authorize(request)
+
+ response = net_http.start do |http|
+ http.request(request, body)
+ end
+ rescue => e
+ Inform.err("PUT #{ref} failed: #{e.class}: #{e}")
+ raise Error::HTTP, "PUT #{ref} failed: #{e}"
+ end
+
+ return response
+ end
+ 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("smswsc", Syslog::LOG_NDELAY, Syslog::LOG_LOCAL7)
+ else
+ Inform.level = Inform::LOG_ERR
+ #Inform.level = Inform::LOG_DEBUG
+ Inform.file = $stdout
+ end
+
+ client = SMSWSC::Client.new(
+ $config["smsws"]["user"],
+ $config["smsws"]["passwd"],
+ "192.168.2.1",
+ $config["smsws"]["port"]
+ )
+
+ msgs = client.sms_messages_query
+ p msgs
+
+ pb = client.phonebook_query
+ p pb
+
+ msg = SMSWSC::SMSMessage.new
+ msg.addr = "jcm"
+ msg.user_data = "test me web by name"
+ client.sms_send(msg)
+end
diff --git a/contrib/smsweb/smswsc/builder.rb b/contrib/smsweb/smswsc/builder.rb
new file mode 100644
index 0000000..a6e25a9
--- /dev/null
+++ b/contrib/smsweb/smswsc/builder.rb
@@ -0,0 +1,23 @@
+require 'builder'
+
+module SMSWSC
+ XmlOptions = {
+ :version => "1.0",
+ :encoding => "UTF-8",
+ :indent => 0,
+ }
+
+ def self.new_builder(xml)
+ builder = Builder::XmlMarkup.new(
+ :target => xml,
+ :indent => SMSWSC::XmlOptions[:indent]
+ )
+ builder.instruct!(
+ :xml,
+ :version => SMSWSC::XmlOptions[:version],
+ :encoding => SMSWSC::XmlOptions[:encoding]
+ )
+
+ return builder
+ end
+end
diff --git a/contrib/smsweb/smswsc/phonebook_entry.rb b/contrib/smsweb/smswsc/phonebook_entry.rb
new file mode 100644
index 0000000..ef13bc4
--- /dev/null
+++ b/contrib/smsweb/smswsc/phonebook_entry.rb
@@ -0,0 +1,56 @@
+require 'smswsc/builder'
+require 'rexml/document'
+
+module SMSWSC
+
+ class PhonebookEntry
+ attr_accessor :name
+ attr_accessor :addr_type
+ attr_accessor :addr
+ attr_accessor :uri
+
+ def initialize(options = {})
+ @addr_type = options["addr-type"]
+ @addr = options["addr"]
+ @name = options["name"]
+ end
+
+ def uri=(value)
+ @uri = value
+ end
+
+ def self.load_xml(parent)
+ entry = PhonebookEntry.new
+
+ parent.each_element do |element|
+ case element.name
+ when "addr-type"
+ entry.addr_type = element.text.to_s
+ when "addr"
+ entry.addr = element.text.to_s
+ when "name"
+ entry.name = element.text.to_s
+ when "uri"
+ entry.uri = URI.parse(element.text.to_s)
+ end
+ end
+
+ return entry
+ end
+
+ def to_xml(builder = nil, element = "phonebook-entry")
+ xml = ""
+ unless builder
+ builder = SMSWSC.new_builder(xml)
+ end
+
+ builder.tag!(element.to_s) {
+ builder.tag! "addr-type", @addr_type
+ builder.tag! "addr", @addr
+ builder.tag! "name", @name
+ }
+
+ return xml
+ end
+ end
+end
diff --git a/contrib/smsweb/smswsc/sms_message.rb b/contrib/smsweb/smswsc/sms_message.rb
new file mode 100644
index 0000000..787b58b
--- /dev/null
+++ b/contrib/smsweb/smswsc/sms_message.rb
@@ -0,0 +1,62 @@
+require 'smswsc/builder'
+require 'rexml/document'
+require 'base64'
+
+module SMSWSC
+
+ class SMSMessage
+ attr_accessor :message_status
+ attr_accessor :addr_type
+ attr_accessor :addr
+ attr_accessor :user_data
+ attr_accessor :uri
+
+ def initialize(options = {})
+ @message_status = options["message_status"]
+ @addr_type = options["addr-type"]
+ @addr = options["addr"]
+ @user_data = options["user-data"]
+ end
+
+ def uri=(value)
+ @uri = value
+ end
+
+ def self.load_xml(parent)
+ message = SMSMessage.new
+
+ parent.each_element do |element|
+ case element.name
+ when "message-status"
+ message.message_status = element.text.to_s
+ when "addr-type"
+ message.addr_type = element.text.to_s
+ when "addr"
+ message.addr = element.text.to_s
+ when "user-data"
+ message.user_data = Base64.decode64(element.text.to_s)
+ when "uri"
+ message.uri = URI.parse(element.text.to_s)
+ end
+ end
+
+ return message
+ end
+
+ def to_xml(builder = nil, element = "sms-message")
+ xml = ""
+ unless builder
+ builder = SMSWSC.new_builder(xml)
+ end
+
+ builder.tag!(element.to_s) {
+ builder.tag! "message-status", @message_status
+ builder.tag! "addr-type", @addr_type
+ builder.tag! "addr", @addr
+ builder.tag! "user-data", Base64.encode64(@user_data)
+ }
+
+ return xml
+ end
+ end
+end
diff --git a/contrib/smsweb/smswsc/utils.rb b/contrib/smsweb/smswsc/utils.rb
new file mode 100644
index 0000000..fa07efd
--- /dev/null
+++ b/contrib/smsweb/smswsc/utils.rb
@@ -0,0 +1,63 @@
+require 'cgi'
+
+class Time
+ # try rfc3339
+ # try sql
+ # try rfc822
+ def self.rfcdate(str)
+ if str =~ /\A(\d{4})-(\d{2})-(\d{2})[T ](\d{2})\:(\d{2})\:(\d{2})/
+ return Time.utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i).localtime
+ elsif str =~ /\A\w{3}, (\d{1,2}) (\w{3}) (\d{4}) (\d{2})\:(\d{2})\:(\d{2})/
+ return Time.utc($3.to_i, $2, $1.to_i, $4.to_i, $5.to_i, $6.to_i).localtime
+ else
+ return nil
+ end
+ end
+end
+
+class Array
+ def urlpath
+ args, atoms = self.flatten.partition { |a| a.is_a?(Hash) }
+ args = args.flatten.inject { |s, v| s.merge!(v) }
+
+ front = atoms.join('/').squeeze('/')
+
+ if args
+ rear = args.inject('?') { |s, (k, v)|
+ s << CGI::escape(k.to_s) + "=" + CGI::escape(v.to_s) + ";"
+ } [0 .. -2]
+
+ front + rear
+ else
+ front
+ end
+ end
+end
+
+module SMSWSC
+ module Utils
+
+ # Assemble a reference from arguments.
+ #
+ # ref("/path", "to", "it", {:q => "test"}) => "/path/to/it?q=test"
+
+ def ref(*atoms)
+ args, atoms = atoms.flatten.partition{|a| a.is_a?(Hash) }
+ args = args.flatten.inject { |s, v| s.merge!(v) }
+
+ front = atoms.join('/').squeeze('/')
+
+ if args
+ rear = args.inject('?') { |s, (k, v)|
+ s << CGI::escape(k.to_s) + "=" + CGI::escape(v.to_s) + ";"
+ } [0 .. -2]
+
+ front + rear
+ else
+ front
+ end
+ end
+ module_function :ref
+
+ end
+end