camper_van 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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