net-dnd 1.1.2

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,5 @@
1
+ .bundle
2
+ .DS_Store
3
+ pkg/*
4
+ *.gem
5
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in net-dnd.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011 by the Trustees of Dartmouth College. All rights reserved.
2
+
3
+ The net-dnd Ruby library, and accompanying documentation, are provided
4
+ subject to the following license agreement. By obtaining and/or using the
5
+ software, you agree that you have read and understood this agreement, and
6
+ will comply with its terms and conditions.
7
+
8
+ 1. Permission to use, copy, modify and distribute this software and
9
+ documentation without fee is hereby granted, provided that the copyright
10
+ notice and this agreement appear on all copies of the software and
11
+ documentation.
12
+
13
+ 2. Any documentation and advertising material relating to this software must
14
+ acknowledge that the software was developed by Dartmouth College.
15
+
16
+ 3. The name of Dartmouth College may not be used to endorse or promote
17
+ products derived from this software without specific prior written
18
+ permission.
19
+
20
+ 4. Neither the Trustees of Dartmouth College nor the authors make any
21
+ representations about the suitability of this software for any purpose. It is
22
+ provided "as is", without any express or implied warranty.
@@ -0,0 +1,22 @@
1
+ # net-dnd
2
+
3
+ Net::DND is a Ruby library for performing user finding (aka. lookup) operations on a Dartmouth Name Directory (DND) server. Inspired by the net-ssh library, net-dnd uses a familiar block construct for starting and interacting with a DND session/connection.
4
+
5
+ Within the block you can submit various find commands and get back zero, one or more 'hits', in the form of Net::DND::Profile instances. Each Profile instance will contain accessors for the fields that were used to seed the DND server connection.
6
+
7
+ ## Installation
8
+
9
+ $ sudo gem intall net-dnd
10
+
11
+ ## Basic Usage
12
+
13
+ Opening a connection to the main Dartmouth College DND server, requesting the return of name and email address fields, for Profiles matching the name `Smith`:
14
+
15
+ ```ruby
16
+ profiles = nil
17
+ Net::DND.start('dnd.dartmouth.edu', %w(name email)) do |dnd|
18
+ profiles = dnd.find('Smith')
19
+ end
20
+ puts profiles[0].name, profiles[0].email
21
+ ```
22
+
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # TODO
4
+ # require 'net/dnd/cli'
5
+ # Net::DND::CLI.start
@@ -0,0 +1,82 @@
1
+ $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../../lib"
2
+ require "net/dnd/session"
3
+
4
+ module Net
5
+
6
+ # Inspired by the outstanding work done by Jamis Buck on the Net::SSH family of gems/libraries,
7
+ # this new version of Net::DND aims to be a much better interface for working with the
8
+ # Dartmouth Name Directory (DND) protocol.
9
+ #
10
+ # In the first (1.0.0) release, the focus is still on providing primarily user finding
11
+ # facilites, however, the architecture has been re-worked to allow for fairly easy support
12
+ # of the rest of the DND protocol, should the need ever arise.
13
+ #
14
+ # Contained here, in the module/librtary wrapper file, is the primary 'start' method, which
15
+ # allows for straightforward session/connection management. The intent is to call this method
16
+ # with a block construct, like this:
17
+ #
18
+ # Net::DND.start("your.dnd.server", fields) do |dnd|
19
+ # profile = dnd.find("some user")
20
+ # end
21
+ #
22
+ # But you don't need to use the block version. You can simply call the start method and it
23
+ # will return a DND session object. You can then send find commands to that object. Used in
24
+ # this way, you have to remember to explicitly close the DND session, as below:
25
+ #
26
+ # dnd = Net::DND.start('dnd.dartmouth.edu', %w(name nickname uid))
27
+ # profile = dnd.find('Throckmorton Scribblemonger', :one)
28
+ # dnd.close
29
+ # puts profile.nickname
30
+ # > throckie
31
+ #
32
+ # All calls to the find command, whether it's restricted to a single match or not, will return
33
+ # Net::DND::Profile objects. The profile object(s) will contain getter methods for the
34
+ # field names specified in the start call. If no fields are supplied, the DND session, and
35
+ # therefore all returned profile objects will contain the full set of publicly viewable fields
36
+ # from the partiuclar DND server to which you've connected.
37
+
38
+ module DND
39
+
40
+ def self.start(host, fields=[], &block)
41
+ session = Session.start(host, fields)
42
+ if block_given?
43
+ yield session
44
+ session.close
45
+ else
46
+ return session
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ # Convenience modules and start methods for the three most commonly accessed DND hosts.
53
+ # Basically, so you don't need to remeber the exact host name, as long as you know which
54
+ # version of the start method to call for the DND server to which you want to send finds.
55
+
56
+ module DartmouthDND
57
+
58
+ HOST = 'dnd.dartmouth.edu'
59
+ def self.start(fields=[], &block)
60
+ DND.start(HOST, fields, &block)
61
+ end
62
+
63
+ end
64
+
65
+ module AlumniDND
66
+
67
+ HOST = 'dnd.dartmouth.org'
68
+ def self.start(fields=[], &block)
69
+ DND.start(HOST, fields, &block)
70
+ end
71
+
72
+ end
73
+
74
+ module HitchcockDND
75
+
76
+ HOST = 'dnd.hitchcock.org'
77
+ def self.start(fields=[], &block)
78
+ DND.start(HOST, fields, &block)
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,89 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+
4
+ folder_path = File.dirname(__FILE__)
5
+ require "#{folder_path}/user_spec"
6
+ require "#{folder_path}/response"
7
+
8
+ module Net
9
+ module DND
10
+
11
+ # An internal class, used by the Session object, to manage the TCP Socket connection
12
+ # to the requested DND server. The Connection object contains the low-level protocol
13
+ # commands that are actually composed and sent down the socket. Once a command is sent
14
+ # the Connection object instantiates a new Response object to parse the returned data.
15
+ # The Response object is sent back to the Session as the result of calling the fields
16
+ # and lookup methods. The quit method is used to close down the socket connection. It
17
+ # doesn't actually return anything back to the Session.
18
+ class Connection
19
+
20
+ attr_reader :host, :socket, :error, :response
21
+
22
+ # Initialize the TCP connection to be used by the Session. Will raise errors if
23
+ # there's no response from port 902 on the supplied host, or if the connection
24
+ # attempt times out. This constructor also verifies that the connected DND server
25
+ # is ready to respond to protocol commands.
26
+
27
+ def initialize(hostname)
28
+ @host = hostname
29
+ @open = false
30
+ begin
31
+ @socket = Timeout::timeout(5) { TCPSocket.open(host, 902) }
32
+ rescue Timeout::Error
33
+ @error = "Connection attempt to DND server on host #{host} has timed out."
34
+ return
35
+ rescue Errno::ECONNREFUSED
36
+ @error = "Could not connect to DND server on host #{host}."
37
+ return
38
+ end
39
+ @response = Response.process(socket)
40
+ @open = @response.ok?
41
+ end
42
+
43
+ # Is the TCP socket still open/active?
44
+
45
+ def open?
46
+ @open
47
+ end
48
+
49
+ # Low-level protocol command for verifying a list of supplied fields. If no fields are
50
+ # supplied, the fields command will return verification data for all known fields.
51
+
52
+ def fields(field_list=[])
53
+ cmd = "fields #{field_list.join(' ')}".rstrip
54
+ read_response(cmd)
55
+ end
56
+
57
+ # Low-level protocol command for performing a 'find' operation. Takes a user specifier
58
+ # and a list of fields.
59
+
60
+ def lookup(user, field_list)
61
+ user_spec = UserSpec.new(user)
62
+ cmd = "lookup #{user_spec.to_s},#{field_list.join(' ')}"
63
+ read_response(cmd)
64
+ end
65
+
66
+ # Low-level protocol command for telling the DND server that you are closing the connection.
67
+ # Calling this method on the socket also closes the session's TCP connection.
68
+
69
+ def quit(noargs = nil)
70
+ cmd = "quit"
71
+ read_response(cmd)
72
+ @socket.close
73
+ response
74
+ end
75
+
76
+ private
77
+
78
+ # Private method for sending the protocol commands across the socket. Also handles the
79
+ # dispatching of any returned data to the Net::DND::Response class for processing.
80
+
81
+ def read_response(cmd="noop")
82
+ socket.puts(cmd)
83
+ @open = !socket.closed?
84
+ @response = Response.process(socket)
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,22 @@
1
+ module Net
2
+ module DND
3
+
4
+ # These classes are used to provide good class names to the various errors that might
5
+ # be raised during a Net::DND session. All are based off of of Ruby's standard
6
+ # RuntimeError class.
7
+ class Error < RuntimeError; end
8
+
9
+ class FieldNotFound < Net::DND::Error; end
10
+
11
+ class FieldLineInvalid < Net::DND::Error; end
12
+
13
+ class FieldAccessDenied < Net::DND::Error; end
14
+
15
+ class ConnectionError < Net::DND::Error; end
16
+
17
+ class ConnectionClosed < Net::DND::Error; end
18
+
19
+ class InvalidResponse < Net::DND::Error; end
20
+
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require 'date'
2
+
3
+ module Net
4
+ module DND
5
+
6
+ # Set of methods that are conditionally added to the Profile class, when the "expires" field
7
+ # has been specified.
8
+ module Expires
9
+
10
+ def expires_on
11
+ @expires_date ||= Date.parse(expires) rescue nil
12
+ end
13
+
14
+ def expire_days
15
+ (expires_on - Date.today).to_i rescue nil
16
+ end
17
+
18
+ def expired?
19
+ expire_days.nil? and return false
20
+ expire_days < 1 ? true : false
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,58 @@
1
+ folder_path = File.dirname(__FILE__)
2
+ require "#{folder_path}/errors"
3
+
4
+ module Net
5
+ module DND
6
+
7
+ # The Field class is another wrapper class used by the Session object. Unless you are simply
8
+ # performing a lookup to verify that there's at least one name match, you will be passing
9
+ # one or more fields to the command, which will determine the data that's returned back,
10
+ # once a successful match has been found.
11
+
12
+ # This class comes into play, because when the DND.start method is used, you pass in a list
13
+ # of fields that you want to have returned. That list is first verified against the host
14
+ # DND server's known list of fields, through use of the protocol's FIELDS command. When sent
15
+ # with a list of fields, the server responds back with each fields read and write permissions.
16
+ # This data, along with the fields name is captured and stored in instances of this class.
17
+
18
+ class Field
19
+
20
+ attr_reader :name, :writeable, :readable
21
+
22
+ def initialize(name, writeable, readable)
23
+ @name = name.to_s
24
+ store_access_value(:writeable, writeable)
25
+ store_access_value(:readable, readable)
26
+ end
27
+
28
+ def self.from_field_line(line)
29
+ args = line.split
30
+ raise FieldLineInvalid unless args.length == 3
31
+ new(args[0], args[1], args[2])
32
+ end
33
+
34
+ def inspect
35
+ "#<#{self.class} name=\"#{@name}\" writeable=\"#{@writeable}\" readable=\"#{@readable}\">"
36
+ end
37
+
38
+ def to_s
39
+ @name
40
+ end
41
+
42
+ def to_sym
43
+ @name.to_sym
44
+ end
45
+
46
+ def read_all?
47
+ @readable == 'A'
48
+ end
49
+
50
+ private
51
+
52
+ def store_access_value(read_write, value)
53
+ instance_variable_set("@#{read_write}".to_sym, value)
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,74 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__))
2
+ require 'errors'
3
+
4
+ module Net
5
+ module DND
6
+
7
+ autoload :Expires, 'expires'
8
+
9
+ # Container class for a single DND Profile. Takes the current fields set in the Session, along with
10
+ # the returned items from a lookup command and builds a 'profile' Hash. The class then provides
11
+ # dynamic accessor methods for each of the fields, as well as a [] accessor method for accessing
12
+ # fields whose name is a Ruby reserved word, i.e. class.
13
+ class Profile
14
+
15
+ # The profile constructor method. Takes 2 array arguments: fields and items. From these
16
+ # it creates a Hash object that is the internal represenation of the profile.
17
+
18
+ def initialize(fields, items)
19
+ @profile = Hash[*fields.zip(items).flatten]
20
+
21
+ if @profile.has_key?(:expires)
22
+ self.extend Net::DND::Expires
23
+ end
24
+ end
25
+
26
+ # Inspection string for instances of the class
27
+
28
+ def inspect
29
+ attrib_inspect = @profile.inject("") { |s, pair| k,v = pair; s << "#{k}=\"#{v}\", " }
30
+ "<#{self.class} length=#{length}, #{attrib_inspect.rstrip.chomp(',')}>"
31
+ end
32
+
33
+ # Length of the class instance, basically the number of fields/items. Only really used
34
+ # by the inspect method.
35
+
36
+ def length
37
+ @profile.length
38
+ end
39
+
40
+ # Generic Hash-style accessor method. Provides access to fields in the profile hash when
41
+ # the name of the field is a reserved word in Ruby. Allows for field names supplied as
42
+ # either Strings or Symbols.
43
+
44
+ def [](field)
45
+ return_field(field)
46
+ end
47
+
48
+ private
49
+
50
+ # Handles all dynamic accessor methods for the Profile instance. This is based on the
51
+ # field accessor methods from Rails ActiveRecord. Fields can be directly accessed on
52
+ # the Profile object, either for purposes of returning their value or, if the field name
53
+ # is requested with a '?' on the end, a true/false is returned based on the existence of
54
+ # the named field in Profile instance.
55
+
56
+ def method_missing(method_id)
57
+ attrib_name = method_id.to_s
58
+ return @profile.has_key?(attrib_name.chop.to_sym) if attrib_name[-1, 1] == '?'
59
+ return_field(method_id)
60
+ end
61
+
62
+ # Private method, used by the [] method and the dynamic accessors. It will return the value
63
+ # of the named field, or it will raise a FieldNotFound error if the field isn't part of the
64
+ # current Profile.
65
+
66
+ def return_field(field)
67
+ field = field.to_sym
68
+ return @profile[field] if @profile.has_key?(field)
69
+ raise FieldNotFound, "Field #{field} not found."
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,95 @@
1
+ module Net
2
+ module DND
3
+
4
+ # Container class for fetching and parsing the response lines returned down the socket
5
+ # after a command has been sent to the connected DND server. Checks for good responses
6
+ # as well as error responses.
7
+
8
+ # For good responses that contain multiple lines-- fields and lookup commands --it parses
9
+ # those lines into an items array that it makes avaialable back to calling method.
10
+
11
+ class Response
12
+
13
+ attr_reader :code, :error, :items, :count, :sub_count
14
+
15
+ # Constructor method for a Response object. This method is only called directly by
16
+ # our unit tests (specs). Normal interaction with the Response class is via the
17
+ # 'process' class method.
18
+
19
+ def initialize(socket)
20
+ @items = []
21
+ @count, @sub_count = 0, 0
22
+ @socket = socket
23
+ end
24
+
25
+ # Convenience method for creating a new Response object and automatically parse
26
+ # data items, if they exist.
27
+
28
+ def self.process(socket)
29
+ resp = Response.new(socket)
30
+ resp.status_line
31
+ resp.parse_items if [101, 102].include?(resp.code)
32
+ resp
33
+ end
34
+
35
+ # Was the result of the last command a 'good' respose?
36
+
37
+ def ok?
38
+ (200..299) === code
39
+ end
40
+
41
+ # The first line returned from all command sent to a DND server is the Status Line. The
42
+ # makup of this line not only tells us the success/failuer of the command, but whether
43
+ # there is more data to be read.
44
+ #
45
+ # When there is more data, it will be contained on one or more additional data lines,
46
+ # called 'items' internally. The status line tells us if we have 1 level of n items, or
47
+ # 2 levels of n items, each of which has m sub-items. In the class, n and m are the count
48
+ # and sub_count attributes, respectively.
49
+
50
+ def status_line
51
+ line = @socket.gets.chomp
52
+ @code = line.match(/^(\d\d\d) /).captures[0].to_i
53
+ case code
54
+ when 200..299 # Command successful, ready for next
55
+ true
56
+ when 500..599 # Command error, set the error value to the line text
57
+ @error = line.match(/^\d\d\d (.*)/).captures[0]
58
+ when 101, 102 # Data command status, set the count and sub_count values
59
+ counts = line.match(/^\d\d\d (\d+) *(\d*)/).captures
60
+ @count = counts[0].to_i
61
+ @sub_count = counts[1].to_i
62
+ end
63
+ end
64
+
65
+ # The result of our command has told us there are data items that need to be read and
66
+ # parsed. This method sets up the loops used to read the correct number of data lines.
67
+ # If we have a postive sub_count value, we actually build a nested array of arrays,
68
+ # otherwise, we build a single level array of data lines.
69
+
70
+ def parse_items
71
+ count.times do # loop at least count times
72
+ if sub_count > 0 # do we have an inner loop
73
+ sub_ary = []
74
+ sub_count.times { sub_ary << data_line }
75
+ @items << sub_ary
76
+ else
77
+ @items << data_line
78
+ end
79
+ end
80
+ status_line
81
+ end
82
+
83
+ private
84
+
85
+ # This private method handles the actually reading of the data item lines from the
86
+ # TCP socket. It reads the line and then does a quick parse on it, to return just
87
+ # the data item.
88
+
89
+ def data_line
90
+ @socket.gets.chomp.match(/^\d\d\d (.*)/).captures[0]
91
+ end
92
+
93
+ end
94
+ end
95
+ end