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.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +22 -0
- data/Rakefile +2 -0
- data/bin/dndwho +5 -0
- data/lib/net/dnd.rb +82 -0
- data/lib/net/dnd/connection.rb +89 -0
- data/lib/net/dnd/errors.rb +22 -0
- data/lib/net/dnd/expires.rb +25 -0
- data/lib/net/dnd/field.rb +58 -0
- data/lib/net/dnd/profile.rb +74 -0
- data/lib/net/dnd/response.rb +95 -0
- data/lib/net/dnd/session.rb +114 -0
- data/lib/net/dnd/user_spec.rb +56 -0
- data/lib/net/dnd/version.rb +11 -0
- data/net-dnd.gemspec +25 -0
- data/spec/dnd/connection_spec.rb +94 -0
- data/spec/dnd/field_spec.rb +72 -0
- data/spec/dnd/profile_spec.rb +66 -0
- data/spec/dnd/response_spec.rb +161 -0
- data/spec/dnd/session_spec.rb +254 -0
- data/spec/dnd/user_spec_spec.rb +70 -0
- data/spec/spec_helper.rb +14 -0
- metadata +128 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
data/bin/dndwho
ADDED
data/lib/net/dnd.rb
ADDED
@@ -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
|