[webservice] expose webservice to plugins
[rbot] / lib / rbot / core / webservice.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Web service for bot
5 #
6 # Author:: Matthias Hecker (apoc@geekosphere.org)
7 #
8 # HTTP(S)/json based web service for remote controlling the bot,
9 # similar to remote but much more portable.
10 #
11 # For more info/documentation:
12 # https://github.com/4poc/rbot/wiki/Web-Service
13 #
14
15 require 'webrick'
16 require 'webrick/https'
17 require 'openssl'
18 require 'cgi'
19 require 'json'
20
21 class ::WebServiceUser < Irc::User
22   def initialize(str, botuser, opts={})
23     super(str, opts)
24     @botuser = botuser
25     @response = []
26   end
27   attr_reader :botuser
28   attr_accessor :response
29 end
30
31 class PingServlet < WEBrick::HTTPServlet::AbstractServlet
32   def initialize(server, bot)
33     super server
34     @bot = bot
35   end
36
37   def do_GET(req, res)
38     res['Content-Type'] = 'text/plain'
39     res.body = "pong\r\n"
40   end
41 end
42
43 class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet
44   def initialize(server, bot)
45     super server
46     @bot = bot
47   end
48
49   def dispatch_command(command, botuser, ip)
50     netmask = '%s!%s@%s' % [botuser.username, botuser.username, ip]
51
52     user = WebServiceUser.new(netmask, botuser)
53     message = Irc::PrivMessage.new(@bot, nil, user, @bot.myself, command)
54
55     @bot.plugins.irc_delegate('privmsg', message)
56
57     { :reply => user.response }
58   end
59
60   # Handle a dispatch request.
61   def do_POST(req, res)
62     post = CGI::parse(req.body)
63     ip = req.peeraddr[3]
64
65     username = post['username'].first
66     password = post['password'].first
67     command = post['command'].first
68
69     botuser = @bot.auth.get_botuser(username)
70     raise 'Permission Denied' if not botuser or botuser.password != password
71
72     begin
73       ret = dispatch_command(command, botuser, ip)
74     rescue
75       debug '[webservice] error: ' + $!.to_s
76       debug $@.join("\n")
77     end
78
79     res.status = 200
80     if req['Accept'] == 'application/json'
81       res['Content-Type'] = 'application/json'
82       res.body = JSON.dump ret
83     else
84       res['Content-Type'] = 'text/plain'
85       res.body = ret[:reply].join("\n") + "\n"
86     end
87   end
88 end
89
90 class WebServiceModule < CoreBotModule
91
92   Config.register Config::BooleanValue.new('webservice.autostart',
93     :default => false,
94     :requires_rescan => true,
95     :desc => 'Whether the web service should be started automatically')
96
97   Config.register Config::IntegerValue.new('webservice.port',
98     :default => 7260, # that's 'rbot'
99     :requires_rescan => true,
100     :desc => 'Port on which the web service will listen')
101
102   Config.register Config::StringValue.new('webservice.host',
103     :default => '127.0.0.1',
104     :requires_rescan => true,
105     :desc => 'Host the web service will bind on')
106
107   Config.register Config::BooleanValue.new('webservice.ssl',
108     :default => false,
109     :requires_rescan => true,
110     :desc => 'Whether the web server should use SSL (recommended!)')
111
112   Config.register Config::StringValue.new('webservice.ssl_key',
113     :default => '~/.rbot/wskey.pem',
114     :requires_rescan => true,
115     :desc => 'Private key file to use for SSL')
116
117   Config.register Config::StringValue.new('webservice.ssl_cert',
118     :default => '~/.rbot/wscert.pem',
119     :requires_rescan => true,
120     :desc => 'Certificate file to use for SSL')
121
122   def initialize
123     super
124     @port = @bot.config['webservice.port']
125     @host = @bot.config['webservice.host']
126     @server = nil
127     @bot.webservice = self
128     begin
129       start_service if @bot.config['webservice.autostart']
130     rescue => e
131       error "couldn't start web service provider: #{e.inspect}"
132     end
133   end
134
135   def start_service
136     raise "Remote service provider already running" if @server
137     opts = {:BindAddress => @host, :Port => @port}
138     if @bot.config['webservice.ssl']
139       opts.merge! :SSLEnable => true
140       cert = File.expand_path @bot.config['webservice.ssl_cert']
141       key = File.expand_path @bot.config['webservice.ssl_key']
142       if File.exists? cert and File.exists? key
143         debug 'using ssl certificate files'
144         opts.merge!({
145           :SSLCertificate => OpenSSL::X509::Certificate.new(File.read(cert)),
146           :SSLPrivateKey => OpenSSL::PKey::RSA.new(File.read(key))
147         })
148       else
149         debug 'using on-the-fly generated ssl certs'
150         opts.merge! :SSLCertName => [ %w[CN localhost] ]
151         # the problem with this is that it will always use the same
152         # serial number which makes this feature pretty much useless.
153       end
154     end
155     # Logging to file in ~/.rbot
156     logfile = File.open(@bot.path('webservice.log'), 'a+')
157     opts.merge!({
158       :Logger => WEBrick::Log.new(logfile),
159       :AccessLog => [[logfile, WEBrick::AccessLog::COMBINED_LOG_FORMAT]]
160     })
161     @server = WEBrick::HTTPServer.new(opts)
162     debug 'webservice started: ' + opts.inspect
163     @server.mount('/dispatch', DispatchServlet, @bot)
164     @server.mount('/ping', PingServlet, @bot)
165     Thread.new { @server.start }
166   end
167
168   def stop_service
169     @server.shutdown if @server
170     @server = nil
171   end
172
173   def cleanup
174     stop_service
175     super
176   end
177
178   def handle_start(m, params)
179     s = ''
180     if @server
181       s << 'web service already running'
182     else
183       begin
184         start_service
185         s << 'web service started'
186       rescue
187         s << 'unable to start web service, error: ' + $!.to_s
188       end
189     end
190     m.reply s
191   end
192
193   def register_servlet(plugin, servlet)
194     @server.mount('/plugin/%s' % plugin.name, servlet, plugin, @bot)
195   end
196
197 end
198
199 webservice = WebServiceModule.new
200
201 webservice.map 'webservice start',
202   :action => 'handle_start',
203   :auth_path => ':manage:'
204
205 webservice.map 'webservice stop',
206   :action => 'handle_stop',
207   :auth_path => ':manage:'
208
209 webservice.default_auth('*', false)
210