net-dnd 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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