Annotation of embedaddon/strongswan/src/libcharon/plugins/vici/ruby/lib/vici.rb, revision 1.1

1.1     ! misho       1: ##
        !             2: # The Vici module implements a native ruby client side library for the
        !             3: # strongSwan VICI protocol. The Connection class provides a high-level
        !             4: # interface to issue requests or listen for events.
        !             5: #
        !             6: #  Copyright (C) 2019 Tobias Brunner
        !             7: #  HSR Hochschule fuer Technik Rapperswil
        !             8: #
        !             9: #  Copyright (C) 2014 Martin Willi
        !            10: #  Copyright (C) 2014 revosec AG
        !            11: #
        !            12: #  Permission is hereby granted, free of charge, to any person obtaining a copy
        !            13: #  of this software and associated documentation files (the "Software"), to deal
        !            14: #  in the Software without restriction, including without limitation the rights
        !            15: #  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        !            16: #  copies of the Software, and to permit persons to whom the Software is
        !            17: #  furnished to do so, subject to the following conditions:
        !            18: #
        !            19: #  The above copyright notice and this permission notice shall be included in
        !            20: #  all copies or substantial portions of the Software.
        !            21: #
        !            22: #  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        !            23: #  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        !            24: #  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        !            25: #  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        !            26: #  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        !            27: #  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
        !            28: #  THE SOFTWARE.
        !            29: 
        !            30: module Vici
        !            31:   ##
        !            32:   # Vici specific exception all others inherit from
        !            33:   class Error < StandardError
        !            34:   end
        !            35: 
        !            36:   ##
        !            37:   # Error while parsing a vici message from the daemon
        !            38:   class ParseError < Error
        !            39:   end
        !            40: 
        !            41:   ##
        !            42:   # Error while encoding a vici message from ruby data structures
        !            43:   class EncodeError < Error
        !            44:   end
        !            45: 
        !            46:   ##
        !            47:   # Error while exchanging messages over the vici Transport layer
        !            48:   class TransportError < Error
        !            49:   end
        !            50: 
        !            51:   ##
        !            52:   # Generic vici command execution error
        !            53:   class CommandError < Error
        !            54:   end
        !            55: 
        !            56:   ##
        !            57:   # Error if an issued vici command is unknown by the daemon
        !            58:   class CommandUnknownError < CommandError
        !            59:   end
        !            60: 
        !            61:   ##
        !            62:   # Error if a command failed to execute in the daemon
        !            63:   class CommandExecError < CommandError
        !            64:   end
        !            65: 
        !            66:   ##
        !            67:   # Generic vici event handling error
        !            68:   class EventError < Error
        !            69:   end
        !            70: 
        !            71:   ##
        !            72:   # Tried to register to / unregister from an unknown vici event
        !            73:   class EventUnknownError < EventError
        !            74:   end
        !            75: 
        !            76:   ##
        !            77:   # Exception to raise from an event listening closure to stop listening
        !            78:   class StopEventListening < Exception
        !            79:   end
        !            80: 
        !            81:   ##
        !            82:   # The Message class provides the low level encoding and decoding of vici
        !            83:   # protocol messages. Directly using this class is usually not required.
        !            84:   class Message
        !            85:     SECTION_START = 1
        !            86:     SECTION_END = 2
        !            87:     KEY_VALUE = 3
        !            88:     LIST_START = 4
        !            89:     LIST_ITEM = 5
        !            90:     LIST_END = 6
        !            91: 
        !            92:     def initialize(data = "")
        !            93:       if data.nil?
        !            94:         @root = {}
        !            95:       elsif data.is_a?(Hash)
        !            96:         @root = data
        !            97:       else
        !            98:         @encoded = data
        !            99:       end
        !           100:     end
        !           101: 
        !           102:     ##
        !           103:     # Get the raw byte encoding of an on-the-wire message
        !           104:     def encoding
        !           105:       @encoded = encode(@root) if @encoded.nil?
        !           106:       @encoded
        !           107:     end
        !           108: 
        !           109:     ##
        !           110:     # Get the root element of the parsed ruby data structures
        !           111:     def root
        !           112:       @root = parse(@encoded) if @root.nil?
        !           113:       @root
        !           114:     end
        !           115: 
        !           116:     private
        !           117: 
        !           118:     def encode_name(name)
        !           119:       [name.length].pack("c") << name
        !           120:     end
        !           121: 
        !           122:     def encode_value(value)
        !           123:       value = value.to_s if value.class != String
        !           124:       [value.length].pack("n") << value
        !           125:     end
        !           126: 
        !           127:     def encode_kv(encoding, key, value)
        !           128:       encoding << KEY_VALUE << encode_name(key) << encode_value(value)
        !           129:     end
        !           130: 
        !           131:     def encode_section(encoding, key, value)
        !           132:       encoding << SECTION_START << encode_name(key)
        !           133:       encoding << encode(value) << SECTION_END
        !           134:     end
        !           135: 
        !           136:     def encode_list(encoding, key, value)
        !           137:       encoding << LIST_START << encode_name(key)
        !           138:       value.each do |item|
        !           139:         encoding << LIST_ITEM << encode_value(item)
        !           140:       end
        !           141:       encoding << LIST_END
        !           142:     end
        !           143: 
        !           144:     def encode(node)
        !           145:       encoding = ""
        !           146:       node.each do |key, value|
        !           147:         encoding = if value.is_a?(Hash)
        !           148:                      encode_section(encoding, key, value)
        !           149:                    elsif value.is_a?(Array)
        !           150:                      encode_list(encoding, key, value)
        !           151:                    else
        !           152:                      encode_kv(encoding, key, value)
        !           153:                    end
        !           154:       end
        !           155:       encoding
        !           156:     end
        !           157: 
        !           158:     def parse_name(encoding)
        !           159:       len = encoding.unpack("c")[0]
        !           160:       name = encoding[1, len]
        !           161:       [encoding[(1 + len)..-1], name]
        !           162:     end
        !           163: 
        !           164:     def parse_value(encoding)
        !           165:       len = encoding.unpack("n")[0]
        !           166:       value = encoding[2, len]
        !           167:       [encoding[(2 + len)..-1], value]
        !           168:     end
        !           169: 
        !           170:     def parse(encoding)
        !           171:       stack = [{}]
        !           172:       list = nil
        !           173:       until encoding.empty?
        !           174:         type = encoding.unpack("c")[0]
        !           175:         encoding = encoding[1..-1]
        !           176:         case type
        !           177:         when SECTION_START
        !           178:           encoding, name = parse_name(encoding)
        !           179:           stack.push(stack[-1][name] = {})
        !           180:         when SECTION_END
        !           181:           raise ParseError, "unexpected section end" if stack.length == 1
        !           182:           stack.pop
        !           183:         when KEY_VALUE
        !           184:           encoding, name = parse_name(encoding)
        !           185:           encoding, value = parse_value(encoding)
        !           186:           stack[-1][name] = value
        !           187:         when LIST_START
        !           188:           encoding, name = parse_name(encoding)
        !           189:           stack[-1][name] = []
        !           190:           list = name
        !           191:         when LIST_ITEM
        !           192:           raise ParseError, "unexpected list item" if list.nil?
        !           193:           encoding, value = parse_value(encoding)
        !           194:           stack[-1][list].push(value)
        !           195:         when LIST_END
        !           196:           raise ParseError, "unexpected list end" if list.nil?
        !           197:           list = nil
        !           198:         else
        !           199:           raise ParseError, "invalid type: #{type}"
        !           200:         end
        !           201:       end
        !           202:       raise ParseError, "unexpected message end" if stack.length > 1
        !           203:       stack[0]
        !           204:     end
        !           205:   end
        !           206: 
        !           207:   ##
        !           208:   # The Transport class implements to low level segmentation of packets
        !           209:   # to the underlying transport stream.  Directly using this class is usually
        !           210:   # not required.
        !           211:   class Transport
        !           212:     CMD_REQUEST = 0
        !           213:     CMD_RESPONSE = 1
        !           214:     CMD_UNKNOWN = 2
        !           215:     EVENT_REGISTER = 3
        !           216:     EVENT_UNREGISTER = 4
        !           217:     EVENT_CONFIRM = 5
        !           218:     EVENT_UNKNOWN = 6
        !           219:     EVENT = 7
        !           220: 
        !           221:     ##
        !           222:     # Create a transport layer using a provided socket for communication.
        !           223:     def initialize(socket)
        !           224:       @socket = socket
        !           225:       @events = {}
        !           226:     end
        !           227: 
        !           228:     ##
        !           229:     # Receive data from socket, until len bytes read
        !           230:     def recv_all(len)
        !           231:       encoding = ""
        !           232:       while encoding.length < len
        !           233:         data = @socket.recv(len - encoding.length)
        !           234:         raise TransportError, "connection closed" if data.empty?
        !           235:         encoding << data
        !           236:       end
        !           237:       encoding
        !           238:     end
        !           239: 
        !           240:     ##
        !           241:     # Send data to socket, until all bytes sent
        !           242:     def send_all(encoding)
        !           243:       len = 0
        !           244:       len += @socket.send(encoding[len..-1], 0) while len < encoding.length
        !           245:     end
        !           246: 
        !           247:     ##
        !           248:     # Write a packet prefixed by its length over the transport socket. Type
        !           249:     # specifies the message, the optional label and message get appended.
        !           250:     def write(type, label, message)
        !           251:       encoding = ""
        !           252:       encoding << label.length << label if label
        !           253:       encoding << message.encoding if message
        !           254:       send_all([encoding.length + 1, type].pack("Nc") + encoding)
        !           255:     end
        !           256: 
        !           257:     ##
        !           258:     # Read a packet from the transport socket. Returns the packet type, and
        !           259:     # if available in the packet a label and the contained message.
        !           260:     def read
        !           261:       len = recv_all(4).unpack("N")[0]
        !           262:       encoding = recv_all(len)
        !           263:       type = encoding.unpack("c")[0]
        !           264:       len = 1
        !           265:       case type
        !           266:       when CMD_REQUEST, EVENT_REGISTER, EVENT_UNREGISTER, EVENT
        !           267:         label = encoding[2, encoding[1].unpack("c")[0]]
        !           268:         len += label.length + 1
        !           269:       when CMD_RESPONSE, CMD_UNKNOWN, EVENT_CONFIRM, EVENT_UNKNOWN
        !           270:         label = nil
        !           271:       else
        !           272:         raise TransportError, "invalid message: #{type}"
        !           273:       end
        !           274:       message = if encoding.length == len
        !           275:                   Message.new
        !           276:                 else
        !           277:                   Message.new(encoding[len..-1])
        !           278:                 end
        !           279:       [type, label, message]
        !           280:     end
        !           281: 
        !           282:     def dispatch_event(name, message)
        !           283:       @events[name].each do |handler|
        !           284:         handler.call(name, message)
        !           285:       end
        !           286:     end
        !           287: 
        !           288:     def read_and_dispatch_event
        !           289:       type, label, message = read
        !           290:       raise TransportError, "unexpected message: #{type}" if type != EVENT
        !           291: 
        !           292:       dispatch_event(label, message)
        !           293:     end
        !           294: 
        !           295:     def read_and_dispatch_events
        !           296:       loop do
        !           297:         type, label, message = read
        !           298:         return type, label, message if type != EVENT
        !           299: 
        !           300:         dispatch_event(label, message)
        !           301:       end
        !           302:     end
        !           303: 
        !           304:     ##
        !           305:     # Send a command with a given name, and optionally a message. Returns
        !           306:     # the reply message on success.
        !           307:     def request(name, message = nil)
        !           308:       write(CMD_REQUEST, name, message)
        !           309:       type, _label, message = read_and_dispatch_events
        !           310:       case type
        !           311:       when CMD_RESPONSE
        !           312:         return message
        !           313:       when CMD_UNKNOWN
        !           314:         raise CommandUnknownError, name
        !           315:       else
        !           316:         raise CommandError, "invalid response for #{name}"
        !           317:       end
        !           318:     end
        !           319: 
        !           320:     ##
        !           321:     # Register a handler method for the given event name
        !           322:     def register(name, handler)
        !           323:       write(EVENT_REGISTER, name, nil)
        !           324:       type, _label, _message = read_and_dispatch_events
        !           325:       case type
        !           326:       when EVENT_CONFIRM
        !           327:         if @events.key?(name)
        !           328:           @events[name] += [handler]
        !           329:         else
        !           330:           @events[name] = [handler]
        !           331:         end
        !           332:       when EVENT_UNKNOWN
        !           333:         raise EventUnknownError, name
        !           334:       else
        !           335:         raise EventError, "invalid response for #{name} register"
        !           336:       end
        !           337:     end
        !           338: 
        !           339:     ##
        !           340:     # Unregister a handler method for the given event name
        !           341:     def unregister(name, handler)
        !           342:       write(EVENT_UNREGISTER, name, nil)
        !           343:       type, _label, _message = read_and_dispatch_events
        !           344:       case type
        !           345:       when EVENT_CONFIRM
        !           346:         @events[name] -= [handler]
        !           347:       when EVENT_UNKNOWN
        !           348:         raise EventUnknownError, name
        !           349:       else
        !           350:         raise EventError, "invalid response for #{name} unregister"
        !           351:       end
        !           352:     end
        !           353:   end
        !           354: 
        !           355:   ##
        !           356:   # The Connection class provides the high-level interface to monitor, configure
        !           357:   # and control the IKE daemon. It takes a connected stream-oriented Socket for
        !           358:   # the communication with the IKE daemon.
        !           359:   #
        !           360:   # This class takes and returns ruby objects for the exchanged message data.
        !           361:   # * Sections get encoded as Hash, containing other sections as Hash, or
        !           362:   # * Key/Values, where the values are Strings as Hash values
        !           363:   # * Lists get encoded as Arrays with String values
        !           364:   # Non-String values that are not a Hash nor an Array get converted with .to_s
        !           365:   # during encoding.
        !           366:   class Connection
        !           367:     ##
        !           368:     # Create a connection, optionally using the given socket
        !           369:     def initialize(socket = nil)
        !           370:       socket = UNIXSocket.new("/var/run/charon.vici") if socket.nil?
        !           371:       @transp = Transport.new(socket)
        !           372:     end
        !           373: 
        !           374:     ##
        !           375:     # Get daemon version information
        !           376:     def version
        !           377:       call("version")
        !           378:     end
        !           379: 
        !           380:     ##
        !           381:     # Get daemon statistics and information.
        !           382:     def stats
        !           383:       call("stats")
        !           384:     end
        !           385: 
        !           386:     ##
        !           387:     # Reload strongswan.conf settings.
        !           388:     def reload_settings
        !           389:       call("reload-settings")
        !           390:     end
        !           391: 
        !           392:     ##
        !           393:     # Initiate a connection. The provided closure is invoked for each log line.
        !           394:     def initiate(options, &block)
        !           395:       call_with_event("initiate", Message.new(options), "control-log", &block)
        !           396:     end
        !           397: 
        !           398:     ##
        !           399:     # Terminate a connection. The provided closure is invoked for each log line.
        !           400:     def terminate(options, &block)
        !           401:       call_with_event("terminate", Message.new(options), "control-log", &block)
        !           402:     end
        !           403: 
        !           404:     ##
        !           405:     # Initiate the rekeying of an SA.
        !           406:     def rekey(options)
        !           407:       call("rekey", Message.new(options))
        !           408:     end
        !           409: 
        !           410:     ##
        !           411:     # Redirect an IKE_SA.
        !           412:     def redirect(options)
        !           413:       call("redirect", Message.new(options))
        !           414:     end
        !           415: 
        !           416:     ##
        !           417:     # Install a shunt/route policy.
        !           418:     def install(policy)
        !           419:       call("install", Message.new(policy))
        !           420:     end
        !           421: 
        !           422:     ##
        !           423:     # Uninstall a shunt/route policy.
        !           424:     def uninstall(policy)
        !           425:       call("uninstall", Message.new(policy))
        !           426:     end
        !           427: 
        !           428:     ##
        !           429:     # List matching active SAs. The provided closure is invoked for each
        !           430:     # matching SA.
        !           431:     def list_sas(match = nil, &block)
        !           432:       call_with_event("list-sas", Message.new(match), "list-sa", &block)
        !           433:     end
        !           434: 
        !           435:     ##
        !           436:     # List matching installed policies. The provided closure is invoked
        !           437:     # for each matching policy.
        !           438:     def list_policies(match, &block)
        !           439:       call_with_event("list-policies", Message.new(match), "list-policy",
        !           440:                       &block)
        !           441:     end
        !           442: 
        !           443:     ##
        !           444:     # List matching loaded connections. The provided closure is invoked
        !           445:     # for each matching connection.
        !           446:     def list_conns(match = nil, &block)
        !           447:       call_with_event("list-conns", Message.new(match), "list-conn", &block)
        !           448:     end
        !           449: 
        !           450:     ##
        !           451:     # Get the names of connections managed by vici.
        !           452:     def get_conns
        !           453:       call("get-conns")
        !           454:     end
        !           455: 
        !           456:     ##
        !           457:     # List matching loaded certificates. The provided closure is invoked
        !           458:     # for each matching certificate definition.
        !           459:     def list_certs(match = nil, &block)
        !           460:       call_with_event("list-certs", Message.new(match), "list-cert", &block)
        !           461:     end
        !           462: 
        !           463:     ##
        !           464:     # List matching loaded certification authorities. The provided closure is
        !           465:     # invoked for each matching certification authority definition.
        !           466:     def list_authorities(match = nil, &block)
        !           467:       call_with_event("list-authorities", Message.new(match), "list-authority",
        !           468:                       &block)
        !           469:     end
        !           470: 
        !           471:     ##
        !           472:     # Get the names of certification authorities managed by vici.
        !           473:     def get_authorities
        !           474:       call("get-authorities")
        !           475:     end
        !           476: 
        !           477:     ##
        !           478:     # Load a connection into the daemon.
        !           479:     def load_conn(conn)
        !           480:       call("load-conn", Message.new(conn))
        !           481:     end
        !           482: 
        !           483:     ##
        !           484:     # Unload a connection from the daemon.
        !           485:     def unload_conn(conn)
        !           486:       call("unload-conn", Message.new(conn))
        !           487:     end
        !           488: 
        !           489:     ##
        !           490:     # Load a certificate into the daemon.
        !           491:     def load_cert(cert)
        !           492:       call("load-cert", Message.new(cert))
        !           493:     end
        !           494: 
        !           495:     ##
        !           496:     # Load a private key into the daemon.
        !           497:     def load_key(key)
        !           498:       call("load-key", Message.new(key))
        !           499:     end
        !           500: 
        !           501:     ##
        !           502:     # Unload a private key from the daemon.
        !           503:     def unload_key(key)
        !           504:       call("unload-key", Message.new(key))
        !           505:     end
        !           506: 
        !           507:     ##
        !           508:     # Get the identifiers of private keys loaded via vici.
        !           509:     def get_keys
        !           510:       call("get-keys")
        !           511:     end
        !           512: 
        !           513:     ##
        !           514:     # Load a private key located on a token into the daemon.
        !           515:     def load_token(token)
        !           516:       call("load-token", Message.new(token))
        !           517:     end
        !           518: 
        !           519:     ##
        !           520:     # Load a shared key into the daemon.
        !           521:     def load_shared(shared)
        !           522:       call("load-shared", Message.new(shared))
        !           523:     end
        !           524: 
        !           525:     ##
        !           526:     # Unload a shared key from the daemon.
        !           527:     def unload_shared(shared)
        !           528:       call("unload-shared", Message.new(shared))
        !           529:     end
        !           530: 
        !           531:     ##
        !           532:     # Get the unique identifiers of shared keys loaded via vici.
        !           533:     def get_shared
        !           534:       call("get-shared")
        !           535:     end
        !           536: 
        !           537:     ##
        !           538:     # Flush credential cache.
        !           539:     def flush_certs(match = nil)
        !           540:       call("flush-certs", Message.new(match))
        !           541:     end
        !           542: 
        !           543:     ##
        !           544:     # Clear all loaded credentials.
        !           545:     def clear_creds
        !           546:       call("clear-creds")
        !           547:     end
        !           548: 
        !           549:     ##
        !           550:     # Load a certification authority into the daemon.
        !           551:     def load_authority(authority)
        !           552:       call("load-authority", Message.new(authority))
        !           553:     end
        !           554: 
        !           555:     ##
        !           556:     # Unload a certification authority from the daemon.
        !           557:     def unload_authority(authority)
        !           558:       call("unload-authority", Message.new(authority))
        !           559:     end
        !           560: 
        !           561:     ##
        !           562:     # Load a virtual IP / attribute pool into the daemon.
        !           563:     def load_pool(pool)
        !           564:       call("load-pool", Message.new(pool))
        !           565:     end
        !           566: 
        !           567:     ##
        !           568:     # Unload a virtual IP / attribute pool from the daemon.
        !           569:     def unload_pool(pool)
        !           570:       call("unload-pool", Message.new(pool))
        !           571:     end
        !           572: 
        !           573:     ##
        !           574:     # Get the currently loaded pools.
        !           575:     def get_pools(options)
        !           576:       call("get-pools", Message.new(options))
        !           577:     end
        !           578: 
        !           579:     ##
        !           580:     # Get currently loaded algorithms and their implementation.
        !           581:     def get_algorithms
        !           582:       call("get-algorithms")
        !           583:     end
        !           584: 
        !           585:     ##
        !           586:     # Get global or connection-specific counters for IKE events.
        !           587:     def get_counters(options = nil)
        !           588:       call("get-counters", Message.new(options))
        !           589:     end
        !           590: 
        !           591:     ##
        !           592:     # Reset global or connection-specific IKE event counters.
        !           593:     def reset_counters(options = nil)
        !           594:       call("reset-counters", Message.new(options))
        !           595:     end
        !           596: 
        !           597:     ##
        !           598:     # Listen for a set of event messages. This call is blocking, and invokes
        !           599:     # the passed closure for each event received. The closure receives the
        !           600:     # event name and the event message as argument. To stop listening, the
        !           601:     # closure may raise a StopEventListening exception, the only caught
        !           602:     # exception.
        !           603:     def listen_events(events, &block)
        !           604:       self.class.instance_eval do
        !           605:         define_method(:listen_event) do |label, message|
        !           606:           block.call(label, message.root)
        !           607:         end
        !           608:       end
        !           609:       events.each do |event|
        !           610:         @transp.register(event, method(:listen_event))
        !           611:       end
        !           612:       begin
        !           613:         loop do
        !           614:           @transp.read_and_dispatch_event
        !           615:         end
        !           616:       rescue StopEventListening
        !           617:       ensure
        !           618:         events.each do |event|
        !           619:           @transp.unregister(event, method(:listen_event))
        !           620:         end
        !           621:       end
        !           622:     end
        !           623: 
        !           624:     ##
        !           625:     # Issue a command request. Checks if the reply of a command indicates
        !           626:     # "success", otherwise raises a CommandExecError exception.
        !           627:     def call(command, request = nil)
        !           628:       check_success(@transp.request(command, request))
        !           629:     end
        !           630: 
        !           631:     ##
        !           632:     # Issue a command request, but register for a specific event while the
        !           633:     # command is active. VICI uses this mechanism to stream potentially large
        !           634:     # data objects continuously. The provided closure is invoked for all
        !           635:     # event messages.
        !           636:     def call_with_event(command, request, event, &block)
        !           637:       self.class.instance_eval do
        !           638:         define_method(:call_event) do |_label, message|
        !           639:           block.call(message.root)
        !           640:         end
        !           641:       end
        !           642:       @transp.register(event, method(:call_event))
        !           643:       begin
        !           644:         reply = @transp.request(command, request)
        !           645:       ensure
        !           646:         @transp.unregister(event, method(:call_event))
        !           647:       end
        !           648:       check_success(reply)
        !           649:     end
        !           650: 
        !           651:     ##
        !           652:     # Check if the reply of a command indicates "success", otherwise raise a
        !           653:     # CommandExecError exception
        !           654:     def check_success(reply)
        !           655:       root = reply.root
        !           656:       if root.key?("success") && root["success"] != "yes"
        !           657:         raise CommandExecError, root["errmsg"]
        !           658:       end
        !           659: 
        !           660:       root
        !           661:     end
        !           662:   end
        !           663: end

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>