camper_van 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,340 @@
1
+ require "socket" # for gethostname
2
+
3
+ module CamperVan
4
+
5
+ # the IRCD is the server that IRC clients connect to. It handles:
6
+ #
7
+ # * irc client registration and validation against campfire
8
+ # * mapping irc commands to internal Commands
9
+ # * proxying irc commands to campfire channels
10
+ class IRCD
11
+
12
+ # The IRC client
13
+ attr_reader :client
14
+
15
+ # Registration information for campfire authentication,
16
+ # comes from the PASS command from the irc client
17
+ attr_reader :subdomain, :api_key
18
+
19
+ # Information for the connected user
20
+ attr_reader :nick, :user, :host
21
+
22
+ # A Hash of connected CampfireChannels
23
+ attr_reader :channels
24
+
25
+ # Whether or not this server is actively sending/receiving data.
26
+ # Set to false when shutting down so extra commands are ignored.
27
+ attr_reader :active
28
+
29
+ MOTD = <<-motd
30
+ Welcome to CamperVan.
31
+ To see what campfire rooms are available to the
32
+ configured subdomain and api key, use the LIST command.
33
+ motd
34
+
35
+ include CommandDefinition # handle :command { ... }
36
+ include CommandParser # parses IRC commands
37
+ include ServerReply # IRC reply helpers
38
+ include Utils # irc translation helpers
39
+ include Logger # logging helper
40
+
41
+ # Public: initialize an IRC server connection
42
+ #
43
+ # client - the EM connection representing the IRC client
44
+ def initialize(client)
45
+ @client = client
46
+ @active = true
47
+ @channels = {}
48
+ end
49
+
50
+ # The campfire client
51
+ #
52
+ # Returns the existing or initializes a new instance of a campfire
53
+ # client using the configured subdomain and API key.
54
+ def campfire
55
+ @campfire ||= Firering::Connection.new(
56
+ "http://#{subdomain}.campfirenow.com"
57
+ ) do |c|
58
+ c.token = api_key
59
+ c.logger = CamperVan.logger
60
+ end
61
+ end
62
+
63
+ # Handler for when a client sends an IRC command
64
+ def receive_line(line)
65
+ if @active
66
+ cmd = parse(line)
67
+ handle cmd
68
+ end
69
+ rescue HandlerMissing
70
+ logger.info "ignoring irc command #{cmd.inspect}: no handler"
71
+ end
72
+
73
+ # Send a line back to the irc client
74
+ def send_line(line)
75
+ client.send_line line if @active
76
+ end
77
+
78
+ # Shuts down this connection to the server
79
+ def shutdown
80
+ @active = false
81
+ client.close_connection
82
+ end
83
+
84
+ # IRC registration sequence:
85
+ #
86
+ # PASS <password> (may not be sent!)
87
+ # NICK <nickname>
88
+ # USER <user info>
89
+ #
90
+
91
+ # PASS command handler
92
+ handle :pass do |args|
93
+ if args.empty?
94
+ numeric_reply :err_needmoreparams, ":must specify a password: subdomain:api_key"
95
+ shutdown
96
+ else
97
+ @subdomain, @api_key = *args.first.split(":")
98
+ end
99
+ end
100
+
101
+ # NICK command handler
102
+ #
103
+ # As a part of the registration sequence, sets the nickname.
104
+ # If sent after the client is registered, responds with an IRC
105
+ # error, as nick changes with campfire are disallowed (TODO)
106
+ handle :nick do |args|
107
+ if args.empty?
108
+ numeric_reply :err_nonicknamegiven, ":no nickname given"
109
+ else
110
+ if @nick
111
+ # TODO error
112
+ else
113
+ @nick = args.first
114
+ end
115
+ end
116
+ end
117
+
118
+ # USER command handler
119
+ #
120
+ # Final part of the registration sequence.
121
+ # If registration is successful, sends a welcome reply sequence.
122
+ handle :user do |args|
123
+ if args.size < 4
124
+ numeric_reply :err_needmoreparams, "Need more params"
125
+ else
126
+ @user = args.first
127
+ # grab the remote IP address for the client
128
+ @host = client.remote_ip
129
+
130
+ unless @api_key
131
+ command_reply :notice, "AUTH", "*** must specify campfire API key as password ***"
132
+ shutdown
133
+ return
134
+ end
135
+
136
+ successful_registration
137
+ end
138
+ end
139
+
140
+ # PING command handler.
141
+ #
142
+ # Responds with a PONG
143
+ handle :ping do |args|
144
+ command_reply :pong, *args
145
+ end
146
+
147
+ # LIST command handler
148
+ #
149
+ # Sends the list of available campfire channels to the client.
150
+ handle :list do |args|
151
+ # hooray async code: have to do gymnastics to make this appear
152
+ # sequential
153
+ campfire.rooms do |rooms|
154
+ sent = 0
155
+ rooms.each do |room|
156
+ name = "#" + irc_name(room.name)
157
+ topic = room.topic
158
+ room.users do |users|
159
+ numeric_reply :rpl_list, name, users.size, topic
160
+ sent += 1
161
+ if sent == rooms.size
162
+ numeric_reply :rpl_listend, "End of list"
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ handle :who do |args|
170
+ if channel = channels[args.first]
171
+ channel.list_users
172
+ else
173
+ if args.empty?
174
+ numeric_reply :rpl_endofwho, "End of WHO list"
175
+ else
176
+ numeric_reply :rpl_endofwho, args.first, "End of WHO list"
177
+ end
178
+ end
179
+ end
180
+
181
+ handle :join do |args|
182
+ args.each do |channel|
183
+ join_channel channel
184
+ end
185
+ end
186
+
187
+ handle :part do |args|
188
+ name = args.first
189
+ if channel = channels[name]
190
+ channel.part
191
+ else
192
+ numeric_reply :err_notonchannel, "You're not on that channel"
193
+ end
194
+ end
195
+
196
+ handle :topic do |args|
197
+ name, new_topic = *args
198
+ if channel = channels[name]
199
+ if new_topic
200
+ channel.set_topic new_topic
201
+ else
202
+ channel.current_topic
203
+ end
204
+ else
205
+ # TODO topic error
206
+ end
207
+ end
208
+
209
+ handle :privmsg do |args|
210
+ name, msg = *args
211
+ if channel = channels[name]
212
+ channel.privmsg msg
213
+ else
214
+ numeric_reply :err_nonicknamegiven, name, "No such nick/channel"
215
+ end
216
+ end
217
+
218
+ handle :mode do |args|
219
+ if channel = channels[args.shift]
220
+
221
+ if mode = args.first
222
+ if mode =~ /^[+-][si]$/
223
+ channel.set_mode mode
224
+ else
225
+ mode = mode.gsub(/\W/,'')
226
+ numeric_reply :err_unknownmode, mode, "Unknown mode #{mode}"
227
+ end
228
+ else
229
+ channel.current_mode
230
+ end
231
+
232
+ else
233
+ # no error message for this situation, so ignore it silently
234
+ end
235
+ end
236
+
237
+ handle :away do |args|
238
+ # ignore silently, there's no campfire API for this
239
+ end
240
+
241
+ handle :quit do |args|
242
+ channels.values.each do |channel|
243
+ channel.part
244
+ end
245
+ shutdown
246
+ end
247
+
248
+ # Completes a successful registration with the appropriate responses
249
+ def successful_registration
250
+ check_campfire_authentication do
251
+ check_nick_matches_authenticated_user
252
+ send_welcome
253
+ send_luser_info
254
+ send_motd
255
+ end
256
+ end
257
+
258
+ # Checks that the campfire authentication is successful.
259
+ #
260
+ # callback - a block to call if successful.
261
+ #
262
+ # Yields to the callback on success (async)
263
+ #
264
+ # If it fails, it replies with an error to the client and
265
+ # disconnects.
266
+ def check_campfire_authentication(&callback)
267
+ # invalid user only returns a nil result!
268
+ campfire.user("me") do |user|
269
+ if user.name
270
+ yield
271
+ else
272
+ command_reply :notice, "AUTH", "could not connect to campfire: invalid API key"
273
+ shutdown
274
+ end
275
+ end
276
+ rescue Firering::Connection::HTTPError => e
277
+ command_reply :notice, "AUTH", "could not connect to campfire: #{e.message}"
278
+ shutdown
279
+ end
280
+
281
+ # Check to see that the nick as provided during the registration
282
+ # process matches the authenticated campfire user. If the nicks don't
283
+ # match, send a nick change to the connected client.
284
+ def check_nick_matches_authenticated_user
285
+ campfire.user("me") do |user|
286
+ name = irc_name user.name
287
+ if name != nick
288
+ user_reply :nick, name
289
+ @nick = name
290
+ end
291
+ end
292
+ end
293
+
294
+ def send_welcome
295
+ hostname = Socket.gethostname
296
+ numeric_reply :rpl_welcome, "Welcome to CamperVan, #{nick}!#{user}@#{host}"
297
+ numeric_reply :rpl_yourhost, "Your host is #{hostname}, " +
298
+ "running CamperVan version #{CamperVan::VERSION}"
299
+ # using Time.now instead of a global start time since, well, this
300
+ # particular instance really did just start right now. Give or
301
+ # take a few seconds.
302
+ numeric_reply :rpl_created, "This server was created #{Time.now}"
303
+ numeric_reply :rpl_myinfo, hostname, CamperVan::VERSION,
304
+ # channel modes: invite-only, secret
305
+ "is",
306
+ # user modes: away
307
+ "a"
308
+ end
309
+
310
+ def send_luser_info
311
+ numeric_reply :rpl_luserclient, "There is 1 user on 1 channel"
312
+ numeric_reply :rpl_luserop, 0, "IRC Operators online"
313
+ numeric_reply :rpl_luserchannels, 0, "channels formed"
314
+ numeric_reply :rpl_myinfo, "I have 1 client and 0 servers"
315
+ end
316
+
317
+ def send_motd
318
+ numeric_reply :rpl_motdstart, ":- MOTD for camper_van -"
319
+ MOTD.split("\n").each do |line|
320
+ numeric_reply :rpl_motd, ":- #{line.strip}"
321
+ end
322
+ numeric_reply :rpl_endofmotd, "END of MOTD"
323
+ end
324
+
325
+ def join_channel(name)
326
+ campfire.rooms do |rooms|
327
+ if room = rooms.detect { |r| "#" + irc_name(r.name) == name }
328
+ channel = Channel.new(name, self, room)
329
+ if channel.join
330
+ channels[name] = channel
331
+ end
332
+ else
333
+ numeric_reply :err_nosuchchannel, name, "No such campfire room!"
334
+ end
335
+ end
336
+ end
337
+
338
+ end
339
+ end
340
+
@@ -0,0 +1,10 @@
1
+ module CamperVan
2
+ module Logger
3
+ # Public: the logger for this class
4
+ #
5
+ # Returns a Logging::Logger instance
6
+ def logger
7
+ CamperVan.logger
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,105 @@
1
+ module CamperVan
2
+
3
+ # The core EventMachine server instance that listens for IRC
4
+ # connections and maps them to IRCD instances.
5
+ module Server
6
+ # Public: start the server
7
+ #
8
+ # bind_address - what address to bind to
9
+ # port - what port to listen on
10
+ # log_options - an optional hash of additional configuration
11
+ # options for the logger (see .initialize_logging)
12
+ def self.run(bind_address="localhost", port=6667, log_options={})
13
+
14
+ initialize_logging log_options
15
+
16
+ EM.run do
17
+ logger = Logging.logger[self.name]
18
+ logger.info "starting server on #{bind_address}:#{port}"
19
+ EM.start_server bind_address, port, self
20
+ trap("INT") do
21
+ logger.info "SIGINT, shutting down"
22
+ EM.stop
23
+ end
24
+ end
25
+ end
26
+
27
+ # Initialize the logging system
28
+ #
29
+ # opts - Hash of logging options
30
+ # - :log_level (default :info)
31
+ # - :log_to - where to log to (default STDOUT), can be IO or
32
+ # String for log filename
33
+ def self.initialize_logging(opts={})
34
+ Logging.consolidate("CamperVan")
35
+
36
+ Logging.logger.root.level = opts[:log_level] || :info
37
+
38
+ appender = case opts[:log_to]
39
+ when String
40
+ Logging.appenders.file(opts[:log_to])
41
+ when IO
42
+ Logging.appenders.io(opts[:log_to])
43
+ when nil
44
+ Logging.appenders.stdout
45
+ end
46
+
47
+ # YYYY-MM-DDTHH:MM:SS 12345 LEVEL LoggerName : The Log message
48
+ appender.layout = Logging::Layouts::Pattern.new(:pattern => "%d %5p %5l %c : %m\n")
49
+
50
+ Logging.logger.root.add_appenders appender
51
+ end
52
+
53
+ # Using a line-based protocol
54
+ include EM::Protocols::LineText2
55
+
56
+ include Logger
57
+
58
+ # Public: returns the instance of the ircd for this connection
59
+ attr_reader :ircd
60
+
61
+ # Public callback once a server connection is established.
62
+ #
63
+ # Initializes an IRCD instance for this connection.
64
+ def post_init(*args)
65
+ logger.info "got connection from #{remote_ip}"
66
+
67
+ # initialize the line-based protocol: IRC is \r\n
68
+ @lt2_delimiter = "\r\n"
69
+
70
+ # start up the IRCD for this connection
71
+ @ircd = IRCD.new(self)
72
+ end
73
+
74
+ # Public: callback for when a line of the protocol has been
75
+ # received. Delegates the received line to the ircd instance.
76
+ #
77
+ # line - the line received
78
+ def receive_line(line)
79
+ logger.debug "irc -> #{line.strip}"
80
+ ircd.receive_line(line)
81
+ end
82
+
83
+ # Public: send a line to the connected client.
84
+ #
85
+ # line - the line to send, sans \r\n delimiter.
86
+ def send_line(line)
87
+ logger.debug "irc <- #{line}"
88
+ send_data line + "\r\n"
89
+ end
90
+
91
+ # Public: callback when a client disconnects
92
+ def unbind
93
+ logger.info "closed connection from #{remote_ip}"
94
+ end
95
+
96
+ # Public: return the remote ip address of the connected client
97
+ #
98
+ # Returns an IP address string
99
+ def remote_ip
100
+ @remote_ip ||= get_peername[4,4].unpack("C4").map { |q| q.to_s }.join(".")
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,94 @@
1
+ module CamperVan
2
+ module ServerReply
3
+
4
+ # not an exhaustive list, just what i'm using
5
+ NUMERIC_REPLIES = {
6
+
7
+ # successful registration / welcome to the network
8
+ :rpl_welcome => "001",
9
+ :rpl_yourhost => "002",
10
+ :rpl_created => "003",
11
+ :rpl_myinfo => "004",
12
+
13
+ # more welcome messages
14
+ :rpl_luserclient => "251",
15
+ :rpl_luserop => "252",
16
+ :rpl_luserchannels => "254",
17
+ :rpl_luserme => "255",
18
+
19
+ # MOTD
20
+ :rpl_motdstart => "375",
21
+ :rpl_motd => "372",
22
+ :rpl_endofmotd => "376",
23
+
24
+ # MODE
25
+ :rpl_channelmodeis => "324",
26
+
27
+ # room listing
28
+ :rpl_list => "322",
29
+ :rpl_listend => "323",
30
+ :rpl_whoreply => "352",
31
+ :rpl_endofwho => "315",
32
+
33
+ # channel joins
34
+ :rpl_notopic => "331",
35
+ :rpl_topic => "332",
36
+ :rpl_namereply => "353",
37
+ :rpl_endofnames => "366",
38
+
39
+ # errors
40
+ :err_nosuchnick => "401", # no privmsgs to nicks allowed
41
+ :err_nosuchchannel => "403", # no such channel yo
42
+
43
+ :err_nonicknamegiven => "413",
44
+
45
+ :err_notonchannel => "442",
46
+
47
+ :err_needmoreparams => "461",
48
+ :err_passwdmismatch => "464",
49
+
50
+ :err_channelisfull => "471", # room is full
51
+ :err_unknownmode => "472",
52
+ :err_inviteonlychan => "473", # couldn't join the room, it's locked
53
+ :err_unavailresource => "437" # no such room!
54
+
55
+ }
56
+
57
+ def numeric_reply(code, *args)
58
+ number = NUMERIC_REPLIES[code]
59
+ raise ArgumentError, "unknown code #{code}" unless number
60
+ send_line ":camper_van #{number} #{nick}" << reply_args(args)
61
+ end
62
+
63
+ def command_reply(command, *args)
64
+ send_line ":camper_van #{command.to_s.upcase}" << reply_args(args)
65
+ end
66
+
67
+ def user_reply(command, *args)
68
+ send_line ":#{nick}!#{user}@#{host} #{command.to_s.upcase}" << reply_args(args)
69
+ end
70
+
71
+ def campfire_reply(command, username, *args)
72
+ # TODO instead of @campfire, use user's email address
73
+ send_line ":#{username}!#{username}@campfire #{command.to_s.upcase}" << reply_args(args)
74
+ end
75
+
76
+ def error_reply(reason)
77
+ send_line "ERROR :#{reason}"
78
+ end
79
+
80
+ private
81
+
82
+ def reply_args(args)
83
+ reply = ""
84
+ if args.size > 0
85
+ if args.last =~ /\s/ && !args.last.start_with?(':')
86
+ args[-1] = ':' + args.last
87
+ end
88
+ reply << " " << args.join(" ")
89
+ end
90
+ reply
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,59 @@
1
+ module CamperVan
2
+ class User
3
+
4
+ # IRC normalization from names
5
+ include Utils
6
+
7
+ # Public: the user's campfire id
8
+ attr_reader :id
9
+
10
+ # Public: the user's campfire name
11
+ attr_reader :name
12
+
13
+ # Public: the user's irc nick
14
+ attr_reader :nick
15
+
16
+ # Public: the user's unix account name for user@host pairs in irc,
17
+ # mapped from the user's email address
18
+ attr_reader :account
19
+
20
+ # Public: the user's unix server name for user@host pairs in irc,
21
+ # mapped from the user's email address
22
+ attr_reader :server
23
+
24
+ # Public: whether the user is idle or not. Updated by campfire
25
+ # Idle/Unidle messages
26
+ def idle?
27
+ @idle
28
+ end
29
+
30
+ # Public: set the user's idle state.
31
+ #
32
+ # is_idle - true/false
33
+ attr_writer :idle
34
+
35
+ # Public: whether or not the user is an admin
36
+ def admin?
37
+ @admin
38
+ end
39
+
40
+ # Public: set the user's admin state
41
+ #
42
+ # admin - true/false
43
+ attr_writer :admin
44
+
45
+ # Public: create a new user from a campfire user definition.
46
+ #
47
+ # Initializes the user's fields based on the campfire user info.
48
+ def initialize(user)
49
+ @id = user.id
50
+ @name = user.name
51
+ @account, @server = user.email_address.split("@")
52
+ @nick = irc_name user.name
53
+ @idle = false
54
+ @admin = user.admin
55
+ end
56
+
57
+ end
58
+ end
59
+
@@ -0,0 +1,22 @@
1
+ module CamperVan
2
+ module Utils
3
+ # TODO make irc-safe substitutions, etc.
4
+ def irc_name(name)
5
+ name.gsub('/', '-').
6
+ gsub(/\W/, ' ').
7
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
8
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
9
+ gsub(/\s+/, "_").
10
+ tr("-", "_").
11
+ downcase
12
+ end
13
+
14
+ def stringify_keys(hash)
15
+ hash.keys.each do |key|
16
+ hash[key.to_s] = hash.delete(key)
17
+ end
18
+ hash
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module CamperVan
2
+ VERSION = "0.0.1"
3
+ end
data/lib/camper_van.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "camper_van/version"
2
+
3
+ require "eventmachine"
4
+ require "firering"
5
+ require "logging"
6
+
7
+ module CamperVan
8
+ require "camper_van/debug_proxy" # debug proxy
9
+
10
+ require "camper_van/utils" # utility methods
11
+ require "camper_van/logger" # logging helper
12
+ require "camper_van/command_parser" # irc command parser
13
+ require "camper_van/command_definition" # command definition and processing
14
+ require "camper_van/server_reply" # ircd responses and helpers
15
+ require "camper_van/user" # channel/campfire user
16
+
17
+ require "camper_van/ircd" # ircd server
18
+ require "camper_van/channel" # campfire room <-> channel bridge
19
+
20
+ require "camper_van/server" # the core campfire EM server
21
+
22
+ # Public: return the logger for the module
23
+ #
24
+ # Returns a Logging::Logger instance.
25
+ def self.logger
26
+ @logger = Logging::Logger[self.name]
27
+ end
28
+ end