Net::Netrc request for comments

B

Bob Showalter

Hi,

I'm a Perl-er learning ruby. I'm creating a ruby port of Perl's Net::Netrc
module for accessing ftp(1)'s .netrc file. I couldn't find an existing ruby
version anywhere.

My current code is below. I've essentially ported the Perl module's usage
semantics, although my internal implementation is quite different. I'd sure
appreciate any feedback, especially related to doing things the "ruby way",
with a view toward submitting this code to the appropriate archive site(s)
at some point.

Example usage:

require 'net/netrc'

n = Net::Netrc.locate('example.com')
if (n.nil?)
puts "No entry found"
else
puts "login = #{n.login}, password = #{n.password}"
end

Questions:

1. Should I use locate() instead of new()? locate() is how the Perl
module works.

2. Should I return nil if no entry is found, or should I return an
object with the accessors all nil?

3. Should I be returning a Net::Netrc object at all, or just a simple
Hash? Or should Net::Netrc sublcass Hash?

4. Is rasing a SecurityError appropriate? Should I create a specific
exception class instead?

------- BEGIN CODE --------

module Net

class Netrc

attr_accessor :machine, :login, :password, :account

# returns name of .netrc file
def Netrc.rcname
# TODO: cross platform? getpwuid() for home dir?
home = ENV['HOME']
home ||= ENV['HOMEDRIVE'] + (ENV['HOMEPATH'] || '') if
ENV['HOMEDRIVE']
File.join(home, '.netrc')
end

# opens .netrc file, returning File object if successful.
# returns nil if .netrc not found.
# raises SecurityError if .netrc is not owned by the current.
# user or if it is readable or writable by other than the
# current user.
def Netrc.open
name = rcname
return nil unless File.exist?(name)
# TODO: this stat code not applicable to Win32 (and others?)
s = File.stat(name)
raise SecurityError, "Not owner: #{name}" unless s.owned?
raise SecurityError, "Bad permissions: #{name}" if s.mode & 077 != 0
File.open(name, 'r')
end

# given a machine name, returns a Net::Netrc object containing
# the matching entry for that name, or the default entry. If
# no match is found an no default entry exists, nil is returned.
def Netrc.locate(mach)
f = open or return nil
entry = nil
key = nil
inmacdef = false
while line = f.gets
if inmacdef
inmacdef = false if line.strip.empty?
next
end
toks = line.scan(/"((?:\\.|[^"])*)"|((?:\\.|\S)+)/).flatten.compact
toks.each { |t| t.gsub!(/\\(.)/, '\1') }
while toks.length > 0
tok = toks.shift
if key
entry = new if key == 'machine' && tok == mach
entry.send "#{key}=", tok if entry
key = nil
end
case tok
when 'default'
return entry if entry
entry = new
when 'machine'
return entry if entry
key = 'machine'
when 'login', 'password', 'account'
key = tok
end
end
end
entry
end

end

end

------- END CODE --------

TIA for any feedback,

Bob
 
R

Robert Klemme

Bob Showalter said:
Hi,

I'm a Perl-er learning ruby. I'm creating a ruby port of Perl's
Net::Netrc module for accessing ftp(1)'s .netrc file. I couldn't find
an existing ruby version anywhere.

My current code is below. I've essentially ported the Perl module's
usage semantics, although my internal implementation is quite
different. I'd sure appreciate any feedback, especially related to
doing things the "ruby way", with a view toward submitting this code
to the appropriate archive site(s) at some point.

Example usage:

require 'net/netrc'

n = Net::Netrc.locate('example.com')
if (n.nil?)
puts "No entry found"
else
puts "login = #{n.login}, password = #{n.password}"
end

Questions:

1. Should I use locate() instead of new()? locate() is how the Perl
module works.

I'd use locate only and probably make new private. Reason is that locate
will not always return a valid object.
2. Should I return nil if no entry is found, or should I return an
object with the accessors all nil?

Return nil
3. Should I be returning a Net::Netrc object at all, or just a simple
Hash? Or should Net::Netrc sublcass Hash?

Definitely not the latter. I'd probably use an instance of Net::Netrc and
add some methods (who says the Ruby version must not be better than the Perl
version)? For example, I'd add a method that opens a connection with the
info you have:

class Netrc
def open(do_init = true)
conn = Ftp.open machine
begin
conn.login login, password
# add code that executes all default actions if defined, maybe
argument flag controlled
rescue Net::FTPPermError
conn.close
raise
end

if block_given?
begin
yield conn
ensure
conn.close
end

nil
else
conn
end
end
end

Then you can do

n = Net::Netrc.locate('example.com')

if n
n.open do |ftp|
files = ftp.chdir('pub/lang/ruby/contrib')
files = ftp.list('n*')
ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
end
else
puts "not found"
end

You could even make this more convenient by adding a class method open that
does this

class Netrc
def self.open(machine, do_init = true, &b)
n = locate machine
raise "error" unless n
n.open(do_init, &b)
end
end

Then you can do

Net::Netrc.open('ftp.foo.bar') do |ftp|
...
end

Uh, just detected a name clash. I'd rename your open as this seems to be a
rather internal method.
4. Is rasing a SecurityError appropriate? Should I create a specific
exception class instead?

I think it's appropriate. However, it seems to me that if you want to mimic
FTP's behavior then you should raise the exception only if you find a
password:
http://www.die.net/doc/linux/man/man5/netrc.5.html
------- BEGIN CODE --------

module Net

class Netrc

attr_accessor :machine, :login, :password, :account

# returns name of .netrc file
def Netrc.rcname
# TODO: cross platform? getpwuid() for home dir?
home = ENV['HOME']
home ||= ENV['HOMEDRIVE'] + (ENV['HOMEPATH'] || '') if
ENV['HOMEDRIVE']
File.join(home, '.netrc')
end

# opens .netrc file, returning File object if successful.
# returns nil if .netrc not found.
# raises SecurityError if .netrc is not owned by the current.
# user or if it is readable or writable by other than the
# current user.
def Netrc.open
name = rcname
return nil unless File.exist?(name)
# TODO: this stat code not applicable to Win32 (and others?)
s = File.stat(name)
raise SecurityError, "Not owner: #{name}" unless s.owned?
raise SecurityError, "Bad permissions: #{name}" if s.mode & 077
!= 0 File.open(name, 'r')
end

# given a machine name, returns a Net::Netrc object containing
# the matching entry for that name, or the default entry. If
# no match is found an no default entry exists, nil is returned.
def Netrc.locate(mach)
f = open or return nil
entry = nil
key = nil
inmacdef = false
while line = f.gets
if inmacdef
inmacdef = false if line.strip.empty?
next
end
toks =
line.scan(/"((?:\\.|[^"])*)"|((?:\\.|\S)+)/).flatten.compact
toks.each { |t| t.gsub!(/\\(.)/, '\1') } while toks.length > 0

# what do you need that "while toks.length > 0" for? Seems completely
superfluous to me. Or is this an indentation problem and the "while" should
have been on the next line? Hmm, probably...

I'd probably do the parsing a bit different: I would have to think about
this a bit more but I'd keep a set of settings for default and a set for the
machine and directly return if I found the machine. Just a rough idea...
tok = toks.shift
if key
entry = new if key == 'machine' && tok == mach
entry.send "#{key}=", tok if entry
key = nil
end
case tok
when 'default'
return entry if entry
entry = new
when 'machine'
return entry if entry
key = 'machine'
when 'login', 'password', 'account'
key = tok
end
end
end
entry
end

end

end

------- END CODE --------

TIA for any feedback,

Bob

You're welcome!

Kind regards

robert
 
B

Bob Showalter

Hi,

At Fri, 23 Sep 2005 01:46:40 +0900,
Robert Klemme wrote in [ruby-talk:157135]:
"Internal Server Error" - Hm...

Hmmm, I can't stop it from seeing .rb as CGI. Try
http://www.rubyist.net/~nobu/ruby/netrc_rb.txt

Thanks, I can see it now. Looks like you subclassed Hash. I will study your
code more. Your parsing logic seems much more complex than mine; perhaps I'm
missing something.

I note that your technique is similar to the Perl module in that you load
the entire file into a hash. But this conflicts with the documentation in
that once a matching "machine" entry is found or the "default" entry is
found, parsing stops when the next "machine" or "default" entry (or eof) is
found.

So if I have:

machine foo ...
default ...
machine bar ...

ftp(1) will not see the "bar" entry (but instead would return the default
login), while your code (and the Perl code) will return the "bar" entry.
 
N

nobuyoshi nakada

Hi,

At Fri, 23 Sep 2005 09:52:06 +0900,
Bob Showalter wrote in [ruby-talk:157201]:
I note that your technique is similar to the Perl module in that you load
the entire file into a hash. But this conflicts with the documentation in
that once a matching "machine" entry is found or the "default" entry is
found, parsing stops when the next "machine" or "default" entry (or eof) is
found.

Thank you, I haven't noticed it. But is that behaviors better?
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,055
Latest member
SlimSparkKetoACVReview

Latest Threads

Top