]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/botuser.rb
6c84a93bca2461d80c56629e8eb8b4c26672aced
[user/henk/code/ruby/rbot.git] / lib / rbot / botuser.rb
1 #-- vim:sw=2:et\r
2 #++\r
3 # :title: User management\r
4 #\r
5 # rbot user management\r
6 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)\r
7 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta\r
8 # License:: GPLv2\r
9 \r
10 #--\r
11 #####\r
12 ####\r
13 ### Discussion on IRC on how to implement it\r
14 ##\r
15 #\r
16 # <tango_>      a. do we want user groups together with users?\r
17 # <markey>      hmm\r
18 # <markey>      let me think about it\r
19 # <markey>      generally I would say: as simple as possible while keeping it as flexible as need be\r
20 # <tango_>      I think we can put user groups in place afterwards if we build the structure right\r
21 # <markey>      prolly, yes\r
22 # <tango_>      so\r
23 # <tango_>      each plugin registers a name\r
24 # <tango_>      so rather than auth level we have +name -name\r
25 # <markey>      yes\r
26 # <markey>      much better\r
27 # <tango_>      the default is +name for every plugin, except when the plugin tells otherwise\r
28 # <markey>      although.. \r
29 # <markey>      if I only want to allow you access to one plugin\r
30 # <markey>      I have lots of typing to do\r
31 # <tango_>      nope\r
32 # <tango_>      we allow things like -*\r
33 # <markey>      ok\r
34 # <tango_>      and + has precedence\r
35 # <tango_>      hm no, not good either\r
36 # <tango_>      because we want bot -* +onething and +* -onething to work\r
37 # <markey>      but then: one plugin currently can have several levels, no?\r
38 # <tango_>      of course\r
39 # <markey>      commandedit, commanddel, commandfoo\r
40 # <tango_>      name.command ?\r
41 # <markey>      yep\r
42 # <tango_>      (then you can't have dots in commands\r
43 # <tango_>      maybe name:command\r
44 # <markey>      or name::comand\r
45 # <markey>      like a namespace\r
46 # <tango_>      ehehehe yeah I like it :)\r
47 # <tango_>      tel\r
48 # <tango_>      brb\r
49 # <markey>      usermod setcaps eean -*\r
50 # <markey>      usermod setcaps eean +quiz::edit\r
51 # <markey>      great\r
52 # <markey>      or even\r
53 # <markey>      auth eean -*, +quiz::edit\r
54 # <markey>      awesome\r
55 # <markey>      auth eean -*, +quiz::edit, +command, -command::del\r
56 # <tango_>      yes\r
57 # <markey>      you know, the default should be -*\r
58 # <markey>      because\r
59 # <markey>      in the time between adding the user and changing auth\r
60 # <markey>      it's insecure\r
61 # <markey>      user could do havoc\r
62 # <markey>      useradd eean, then eean does "~quit", before I change auth\r
63 # <tango_>      nope\r
64 # <markey>      perhaps we should allow combining useradd with auth\r
65 # <tango_>      the default should be +* -important stuff\r
66 # <markey>      ok\r
67 # <tango_>      how to specify channel stuff?\r
68 # <markey>      for one, when you issue the command on the channel itself\r
69 # <markey>      then it's channel relative\r
70 # <markey>      perhaps\r
71 # <markey>      or\r
72 # <tango_>      yes but I was thinking more about the syntax\r
73 # <markey>      auth eean #rbot -quiz\r
74 # <tango_>      hm\r
75 # <markey>      or maybe: treat channels like users: auth #rbot -quiz\r
76 # <markey>      would shut up quiz in #rbot\r
77 # <markey>      hm\r
78 # <markey>      heh\r
79 # <tango_>      auth * #rbot -quiz\r
80 # <markey>      not sure I'm making sense here ;)\r
81 # <tango_>      I think syntax should be auth [usermask] [channelmask] [modes]\r
82 # <markey>      yes\r
83 # <markey>      modes separated by comma?\r
84 # <tango_>      where channelmask is implied to be *\r
85 # <tango_>      no we can have it spacesplit\r
86 # <markey>      great\r
87 # <markey>      ok\r
88 # <tango_>      modes are detected by +-\r
89 # <tango_>      so you can do something like auth markey #rbot -quiz #amarok -chuck\r
90 # <markey>      also I like "auth" a lot more than "usermod foo"\r
91 # <markey>      yep\r
92 # <tango_>      I don't understand why the 'mod'\r
93 # <tango_>      we could have all auth commands start with use\r
94 # <tango_>      user\r
95 # <tango_>      user add\r
96 # <tango_>      user list\r
97 # <tango_>      user del\r
98 # <markey>      yes\r
99 # <tango_>      user auth\r
100 # <tango_>      hm\r
101 # <tango_>      and maybe auth as a synonym for user auth\r
102 # <markey>      this is also uncomfortable: usermod wants the full user mask\r
103 # <markey>      you have to copy/paste it\r
104 # <tango_>      no\r
105 # <tango_>      can't you use *?\r
106 # <markey>      sorry not sure\r
107 # <markey>      but this shows, it's not inuitive\r
108 # <markey>      I've read the docs\r
109 # <markey>      but didn't know how to use it really\r
110 # <tango_>      markey!*@*\r
111 # <markey>      that's not very intuitive\r
112 # <tango_>      we could use nick as a synonym for nick!*@* if it's too much for you :D\r
113 # <markey>      usermod markey foo should suffice\r
114 # <markey>      rememember: you're a hacker. when rbot gets many new users, they will often be noobs\r
115 # <markey>      gotta make things simple to use\r
116 # <tango_>      but the hostmask is only needed for the user creation\r
117 # <markey>      really? then forget what I said, sorry\r
118 # <tango_>      I think so\r
119 # <tango_>      ,help auth\r
120 # <testbot>     Auth module (User authentication) topics: setlevel, useradd, userdel, usermod, auth, levels, users, whoami, identify\r
121 # <tango_>      ,help usermod\r
122 # <testbot>     no help for topic usermod\r
123 # <tango_>      ,help auth usermod\r
124 # <testbot>     usermod <username> <item> <value> => Modify <username>s settings. Valid <item>s are: hostmask, (+|-)hostmask, password, level (private addressing only)\r
125 # <tango_>      see? it's username, not nick :D\r
126 # <markey>      btw, help usermod should also work\r
127 # <tango_>      ,help auth useradd\r
128 # <testbot>     useradd <username> => Add user <mask>, you still need to set him up correctly (private addressing only)\r
129 # <markey>      instead of help auth usermode\r
130 # <markey>      when it's not ambiguous\r
131 # <tango_>      and the help for useradd is wrong\r
132 # <markey>      for the website, we could make a logo contest :) the current logo looks like giblet made it in 5 minutes ;)\r
133 # <markey>      ah well, for 1.0 maybe\r
134 # <tango_>      so a user on rbot is given by\r
135 # <tango_>      username, password, hostmasks, permissions\r
136 # <markey>      yup\r
137 # <tango_>      the default permission is +* -importantstuff\r
138 # <markey>      how defines importantstuff?\r
139 # <markey>      you mean like core and auth?\r
140 # <tango_>      yes\r
141 # <markey>      ok\r
142 # <tango_>      but we can decide about this :)\r
143 # <markey>      some plugins are dangerous by default\r
144 # <markey>      like command plugin\r
145 # <markey>      you can do all sorts of nasty shit with it\r
146 # <tango_>      then command plugin will do something like: command.defaultperm("-command")\r
147 # <markey>      yes, good point\r
148 # <tango_>      this is then added to the default permissions (user * channel *)\r
149 # <tango_>      when checking for auth, we go like this:\r
150 # <tango_>      hm\r
151 # <tango_>      check user * channel *\r
152 # <tango_>      then user name channel *\r
153 # <tango_>      then user * channel name\r
154 # <tango_>      then user name channel name\r
155 # <tango_>      for each of these combinations we match against * first, then against command, and then against command::subcommand\r
156 # <markey>      yup\r
157 # <tango_>      setting or resetting it depending on wether it's + or -\r
158 # <tango_>      the final result gives us the permission\r
159 # <tango_>      implementation detail\r
160 # <tango_>      username and passwords are strings\r
161 # <markey>      (I might rename the command plugin, the name is somewhat confusing)\r
162 # <tango_>      yeah\r
163 # <tango_>      hostmasks are hostmasks\r
164 # <markey>      also I'm pondering to restrict it more: disallow access to @bot\r
165 # <tango_>      permissions are in the form [ [channel, {command => bool, ...}] ...]\r
166 #++\r
167 \r
168 require 'singleton'\r
169 \r
170 module Irc\r
171 \r
172   # This method raises a TypeError if _user_ is not of class User\r
173   #\r
174   def Irc.error_if_not_user(user)\r
175     raise TypeError, "#{user.inspect} must be of type Irc::User and not #{user.class}" unless user.class <= User\r
176   end\r
177 \r
178   # This method raises a TypeError if _chan_ is not of class Chan\r
179   #\r
180   def Irc.error_if_not_channel(chan)\r
181     raise TypeError, "#{chan.inspect} must be of type Irc::User and not #{chan.class}" unless chan.class <= Channel\r
182   end\r
183 \r
184 \r
185   # This module contains the actual Authentication stuff\r
186   #\r
187   module Auth\r
188 \r
189     # Generate a random password of length _l_\r
190     #\r
191     def random_password(l=8)\r
192       pwd = ""\r
193       8.times do\r
194         pwd += (rand(26) + (rand(2) == 0 ? 65 : 97) ).chr\r
195       end\r
196       return pwd\r
197     end\r
198 \r
199 \r
200     # An Irc::Auth::Command defines a command by its "path":\r
201     #\r
202     #   base::command::subcommand::subsubcommand::subsubsubcommand\r
203     #\r
204     class Command\r
205 \r
206       attr_reader :command, :path\r
207 \r
208       # A method that checks if a given _cmd_ is in a form that can be\r
209       # reduced into a canonical command path, and if so, returns it\r
210       #\r
211       def sanitize_command_path(cmd)\r
212         pre = cmd.to_s.downcase.gsub(/^\*?(?:::)?/,"").gsub(/::$/,"")\r
213         return pre if pre.empty?\r
214         return pre if pre =~ /^\S+(::\S+)*$/\r
215         raise TypeError, "#{cmd.inspect} is not a valid command"\r
216       end\r
217 \r
218       # Creates a new Command from a given string; you can then access\r
219       # the command as a symbol with the :command method and the whole\r
220       # path as :path\r
221       #\r
222       #   Command.new("core::auth::save").path => [:"*", :"core", :"core::auth", :"core::auth::save"]\r
223       #\r
224       #   Command.new("core::auth::save").command => :"core::auth::save"\r
225       #\r
226       def initialize(cmd)\r
227         cmdpath = sanitize_command_path(cmd).split('::')\r
228         seq = cmdpath.inject(["*"]) { |list, cmd|\r
229           list << (list.length > 1 ? list.last + "::" : "") + cmd\r
230         }\r
231         @path = seq.map { |k|\r
232           k.to_sym\r
233         }\r
234         @command = path.last\r
235         debug "Created command #{@command.inspect} with path #{@path.join(', ')}"\r
236       end\r
237     end\r
238 \r
239     # This method raises a TypeError if _user_ is not of class User\r
240     #\r
241     def Irc.error_if_not_command(cmd)\r
242       raise TypeError, "#{cmd.inspect} must be of type Irc::Auth::Command and not #{cmd.class}" unless cmd.class <= Command\r
243     end\r
244 \r
245 \r
246     # This class describes a permission set\r
247     class PermissionSet\r
248 \r
249       # Create a new (empty) PermissionSet\r
250       #\r
251       def initialize\r
252         @perm = {}\r
253       end\r
254 \r
255       # Inspection simply inspects the internal hash\r
256       def inspect\r
257         @perm.inspect\r
258       end\r
259 \r
260       # Sets the permission for command _cmd_ to _val_,\r
261       #\r
262       def set_permission(cmd, val)\r
263         Irc::error_if_not_command(cmd)\r
264         case val\r
265         when true, false\r
266           @perm[cmd.command] = val\r
267         when nil\r
268           @perm.delete(cmd.command)\r
269         else\r
270           raise TypeError, "#{val.inspect} must be true or false" unless [true,false].include?(val)\r
271         end\r
272       end\r
273 \r
274       # Resets the permission for command _cmd_\r
275       #\r
276       def reset_permission(cmd)\r
277         set_permission(cmd, nil)\r
278       end\r
279 \r
280       # Tells if command _cmd_ is permitted. We do this by returning\r
281       # the value of the deepest Command#path that matches.\r
282       #\r
283       def permit?(cmd)\r
284         Irc::error_if_not_command(cmd)\r
285         allow = nil\r
286         cmd.path.reverse.each { |k|\r
287           if @perm.has_key?(k)\r
288             allow = @perm[k]\r
289             break\r
290           end\r
291         }\r
292         return allow\r
293       end\r
294     end\r
295 \r
296 \r
297     # This is the basic class for bot users: they have a username, a password, a\r
298     # list of netmasks to match against, and a list of permissions.\r
299     #\r
300     class BotUser\r
301 \r
302       attr_reader :username\r
303       attr_reader :password\r
304       attr_reader :netmasks\r
305 \r
306       # Create a new BotUser with given username\r
307       def initialize(username)\r
308         @username = BotUser.sanitize_username(username)\r
309         @password = nil\r
310         @netmasks = NetmaskList.new\r
311         @perm = {}\r
312       end\r
313 \r
314       # Resets the password by creating a new onw\r
315       def reset_password\r
316         @password = random_password\r
317       end\r
318 \r
319       # Sets the permission for command _cmd_ to _val_ on channel _chan_\r
320       #\r
321       def set_permission(cmd, val, chan="*")\r
322         k = chan.to_s.to_sym\r
323         @perm[k] = PermissionSet.new unless @perm.has_key?(k)\r
324         case cmd\r
325         when String\r
326           @perm[k].set_permission(Command.new(cmd), val)\r
327         else\r
328           @perm[k].set_permission(cmd, val)\r
329         end\r
330       end\r
331 \r
332       # Resets the permission for command _cmd_ on channel _chan_\r
333       #\r
334       def reset_permission(cmd, chan ="*")\r
335         set_permission(cmd, nil, chan)\r
336       end\r
337 \r
338       # Checks if BotUser is allowed to do something on channel _chan_,\r
339       # or on all channels if _chan_ is nil\r
340       #\r
341       def permit?(cmd, chan=nil)\r
342         if chan\r
343           k = chan.to_s.to_sym\r
344         else\r
345           k = :*\r
346         end\r
347         allow = nil\r
348         if @perm.has_key?(k)\r
349           allow = @perm[k].permit?(cmd)\r
350         end\r
351         return allow\r
352       end\r
353 \r
354       # Adds a Netmask\r
355       #\r
356       def add_netmask(mask)\r
357         case mask\r
358         when Netmask\r
359           @netmasks << mask\r
360         else\r
361           @netmasks << Netmask(mask)\r
362         end\r
363       end\r
364 \r
365       # Removes a Netmask\r
366       #\r
367       def delete_netmask(mask)\r
368         case mask\r
369         when Netmask\r
370           m = mask\r
371         else\r
372           m << Netmask(mask)\r
373         end\r
374         @netmasks.delete(m)\r
375       end\r
376 \r
377       # Removes all <code>Netmask</code>s\r
378       def reset_netmask_list\r
379         @netmasks = NetmaskList.new\r
380       end\r
381 \r
382       # This method checks if BotUser has a Netmask that matches _user_\r
383       def knows?(user)\r
384         Irc::error_if_not_user(user)\r
385         known = false\r
386         @netmasks.each { |n|\r
387           if user.matches?(n)\r
388             known = true\r
389             break\r
390           end\r
391         }\r
392         return known\r
393       end\r
394 \r
395       # This method gets called when User _user_ wants to log in.\r
396       # It returns true or false depending on whether the password\r
397       # is right. If it is, the Netmask of the user is added to the\r
398       # list of acceptable Netmask unless it's already matched.\r
399       def login(user, password)\r
400         if password == @password\r
401           add_netmask(user) unless knows?(user)\r
402           return true\r
403         else\r
404           return false\r
405         end\r
406       end\r
407 \r
408       # # This method gets called when User _user_ has logged out as this BotUser\r
409       # def logout(user)\r
410       #   delete_netmask(user) if knows?(user)\r
411       # end\r
412 \r
413       # This method sanitizes a username by chomping, downcasing\r
414       # and replacing any nonalphanumeric character with _\r
415       #\r
416       def BotUser.sanitize_username(name)\r
417         return name.to_s.chomp.downcase.gsub(/[^a-z0-9]/,"_")\r
418       end\r
419 \r
420       # This method sets the password if the proposed new password\r
421       # is valid\r
422       def password=(pwd=nil)\r
423         if pwd\r
424           begin\r
425             raise InvalidPassword, "#{pwd} contains invalid characters" if pwd !~ /^[A-Za-z0-9]+$/\r
426             raise InvalidPassword, "#{pwd} too short" if pwd.length < 4\r
427             @password = pwd\r
428           rescue InvalidPassword => e\r
429             raise e\r
430           rescue => e\r
431             raise InvalidPassword, "Exception #{e.inspect} while checking #{pwd}"\r
432           end\r
433         else\r
434           reset_password\r
435         end\r
436       end\r
437     end\r
438 \r
439 \r
440     # This is the default BotUser: it's used for all users which haven't\r
441     # identified with the bot\r
442     #\r
443     class DefaultBotUserClass < BotUser\r
444       include Singleton\r
445       def initialize\r
446         super("everyone")\r
447         @default_perm = PermissionSet.new\r
448       end\r
449       private :login, :add_netmask, :delete_netmask\r
450 \r
451       # Sets the default permission for the default user (i.e. the ones\r
452       # set by the BotModule writers) on all channels\r
453       #\r
454       def set_default_permission(cmd, val)\r
455         @default_perm.set_permission(Command.new(cmd), val)\r
456         debug "Default permissions now:\n#{@default_perm.inspect}"\r
457       end\r
458 \r
459       # default knows everybody\r
460       #\r
461       def knows?(user)\r
462         Irc::error_if_not_user(user)\r
463         return true\r
464       end\r
465 \r
466       # Resets the NetmaskList\r
467       def reset_netmask_list\r
468         super\r
469         add_netmask("*!*@*")\r
470       end\r
471 \r
472       # DefaultBotUser will check the default_perm after checking\r
473       # the global ones\r
474       # or on all channels if _chan_ is nil\r
475       #\r
476       def permit?(cmd, chan=nil)\r
477         allow = super(cmd, chan)\r
478         if allow.nil? && chan.nil?\r
479           allow = @default_perm.permit?(cmd)\r
480         end\r
481         return allow\r
482       end\r
483     end\r
484 \r
485     # Returns the only instance of DefaultBotUserClass\r
486     #\r
487     def Auth.defaultbotuser\r
488       return DefaultBotUserClass.instance\r
489     end\r
490 \r
491     # This is the BotOwner: he can do everything\r
492     #\r
493     class BotOwnerClass < BotUser\r
494       include Singleton\r
495       def initialize\r
496         super("owner")\r
497       end\r
498 \r
499       def permit?(cmd, chan=nil)\r
500         return true\r
501       end\r
502     end\r
503 \r
504     # Returns the only instance of BotOwnerClass\r
505     #\r
506     def Auth.botowner\r
507       return BotOwnerClass.instance\r
508     end\r
509 \r
510 \r
511     # This is the AuthManagerClass singleton, used to manage User/BotUser connections and\r
512     # everything\r
513     #\r
514     class AuthManagerClass\r
515       include Singleton\r
516 \r
517       attr_reader :everyone\r
518       attr_reader :botowner\r
519 \r
520       # The instance manages two <code>Hash</code>es: one that maps\r
521       # <code>Irc::User</code>s onto <code>BotUser</code>s, and the other that maps\r
522       # usernames onto <code>BotUser</code>\r
523       def initialize\r
524         @everyone = Auth::defaultbotuser\r
525         @botowner = Auth::botowner\r
526         bot_associate(nil)\r
527       end\r
528 \r
529       def bot_associate(bot)\r
530         raise "Cannot associate with a new bot! Save first" if defined?(@has_changes) && @has_changes\r
531 \r
532         reset_hashes\r
533 \r
534         # Associated bot\r
535         @bot = bot\r
536 \r
537         # This variable is set to true when there have been changes\r
538         # to the botusers list, so that we know when to save\r
539         @has_changes = false\r
540       end\r
541 \r
542       # resets the hashes\r
543       def reset_hashes\r
544         @botusers = Hash.new\r
545         @allbotusers = Hash.new\r
546         [everyone, botowner].each { |x|\r
547           @allbotusers[x.username.to_sym] = x\r
548         }\r
549       end\r
550 \r
551       # load botlist from userfile\r
552       def load_merge(filename=nil)\r
553         # TODO\r
554         raise NotImplementedError\r
555         @has_changes = true\r
556       end\r
557 \r
558       def load(filename=nil)\r
559         reset_hashes\r
560         load_merge(filename)\r
561       end\r
562 \r
563       # save botlist to userfile\r
564       def save(filename=nil)\r
565         return unless @has_changes\r
566         # TODO\r
567         raise NotImplementedError\r
568       end\r
569 \r
570       # checks if we know about a certain BotUser username\r
571       def include?(botusername)\r
572         @allbotusers.has_key?(botusername.to_sym)\r
573       end\r
574 \r
575       # Maps <code>Irc::User</code> to BotUser\r
576       def irc_to_botuser(ircuser)\r
577         Irc::error_if_not_user(ircuser)\r
578         # TODO check netmasks\r
579         return @botusers[ircuser] || everyone\r
580       end\r
581 \r
582       # creates a new BotUser\r
583       def create_botuser(name, password=nil)\r
584         n = BotUser.sanitize_username(name)\r
585         k = n.to_sym\r
586         raise "BotUser #{n} exists" if include?(k)\r
587         bu = BotUser.new(n)\r
588         bu.password = password\r
589         @allbotusers[k] = bu\r
590       end\r
591 \r
592       # Logs Irc::User _ircuser_ in to BotUser _botusername_ with password _pwd_\r
593       #\r
594       # raises an error if _botusername_ is not a known BotUser username\r
595       #\r
596       # It is possible to autologin by Netmask, on request\r
597       #\r
598       def login(ircuser, botusername, pwd, bymask = false)\r
599         Irc::error_if_not_user(ircuser)\r
600         n = BotUser.sanitize_username(name)\r
601         k = n.to_sym\r
602         raise "No such BotUser #{n}" unless include?(k)\r
603         if @botusers.has_key?(ircuser)\r
604           # TODO\r
605           # @botusers[ircuser].logout(ircuser)\r
606         end\r
607         bu = @allbotusers[k]\r
608         if bymask && bu.knows?(user)\r
609           @botusers[ircuser] = bu\r
610           return true\r
611         elsif bu.login(ircuser, pwd)\r
612           @botusers[ircuser] = bu\r
613           return true\r
614         end\r
615         return false\r
616       end\r
617 \r
618       # Checks if User _user_ can do _cmd_ on _chan_.\r
619       #\r
620       # Permission are checked in this order, until a true or false\r
621       # is returned:\r
622       # * associated BotUser on _chan_\r
623       # * associated BotUser on all channels\r
624       # * everyone on _chan_\r
625       # * everyone on all channels\r
626       #\r
627       def permit?(user, cmdtxt, chan=nil)\r
628         botuser = irc_to_botuser(user)\r
629         cmd = Command.new(cmdtxt)\r
630 \r
631         case chan\r
632         when User\r
633           chan = "?"\r
634         when Channel\r
635           chan = chan.name\r
636         end\r
637 \r
638         allow = nil\r
639 \r
640         allow = botuser.permit?(cmd, chan) if chan\r
641         return allow unless allow.nil?\r
642         allow = botuser.permit?(cmd)\r
643         return allow unless allow.nil?\r
644 \r
645         unless botuser == everyone\r
646           allow = everyone.permit?(cmd, chan) if chan\r
647           return allow unless allow.nil?\r
648           allow = everyone.permit?(cmd)\r
649           return allow unless allow.nil?\r
650         end\r
651 \r
652         raise "Could not check permission for user #{user.inspect} to run #{cmdtxt.inspect} on #{chan.inspect}"\r
653       end\r
654 \r
655       # Checks if command _cmd_ is allowed to User _user_ on _chan_\r
656       def allow?(cmdtxt, user, chan=nil)\r
657         permit?(user, cmdtxt, chan)\r
658       end\r
659     end\r
660 \r
661     # Returns the only instance of AuthManagerClass\r
662     #\r
663     def Auth.authmanager\r
664       return AuthManagerClass.instance\r
665     end\r
666   end\r
667 end\r