diff options
author | James Maki <jmaki@multitech.com> | 2010-05-10 14:26:36 -0500 |
---|---|---|
committer | James Maki <jmaki@multitech.com> | 2010-05-10 14:26:36 -0500 |
commit | 9ba8adf164b316a3d6914b3b187f371f6e19066d (patch) | |
tree | cc4579c4141b42ed51cd25a4beb1ae733d8f03ac /contrib/smsweb | |
parent | 088fee0dd65a2d11dfac58afec8871b3a3cfc376 (diff) | |
download | sms-utils-9ba8adf164b316a3d6914b3b187f371f6e19066d.tar.gz sms-utils-9ba8adf164b316a3d6914b3b187f371f6e19066d.tar.bz2 sms-utils-9ba8adf164b316a3d6914b3b187f371f6e19066d.zip |
Add sms web service example0.0.3
Diffstat (limited to 'contrib/smsweb')
-rw-r--r-- | contrib/smsweb/inform.rb | 131 | ||||
-rw-r--r-- | contrib/smsweb/sms.config | 32 | ||||
-rwxr-xr-x | contrib/smsweb/smsws | 583 | ||||
-rwxr-xr-x | contrib/smsweb/smswsc.rb | 341 | ||||
-rw-r--r-- | contrib/smsweb/smswsc/builder.rb | 23 | ||||
-rw-r--r-- | contrib/smsweb/smswsc/phonebook_entry.rb | 56 | ||||
-rw-r--r-- | contrib/smsweb/smswsc/sms_message.rb | 62 | ||||
-rw-r--r-- | contrib/smsweb/smswsc/utils.rb | 63 |
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(/&/, "&") + 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 + 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 |