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