252 lines
5.6 KiB
Ruby
252 lines
5.6 KiB
Ruby
require 'rbconfig'
|
|
|
|
class Netrc
|
|
VERSION = "0.11.0"
|
|
|
|
# see http://stackoverflow.com/questions/4871309/what-is-the-correct-way-to-detect-if-ruby-is-running-on-windows
|
|
WINDOWS = RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
|
|
CYGWIN = RbConfig::CONFIG["host_os"] =~ /cygwin/
|
|
|
|
def self.default_path
|
|
File.join(ENV['NETRC'] || home_path, netrc_filename)
|
|
end
|
|
|
|
def self.home_path
|
|
home = Dir.respond_to?(:home) ? Dir.home : ENV['HOME']
|
|
|
|
if WINDOWS && !CYGWIN
|
|
home ||= File.join(ENV['HOMEDRIVE'], ENV['HOMEPATH']) if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
|
|
home ||= ENV['USERPROFILE']
|
|
# XXX: old stuff; most likely unnecessary
|
|
home = home.tr("\\", "/") unless home.nil?
|
|
end
|
|
|
|
(home && File.readable?(home)) ? home : Dir.pwd
|
|
rescue ArgumentError
|
|
return Dir.pwd
|
|
end
|
|
|
|
def self.netrc_filename
|
|
WINDOWS && !CYGWIN ? "_netrc" : ".netrc"
|
|
end
|
|
|
|
def self.config
|
|
@config ||= {}
|
|
end
|
|
|
|
def self.configure
|
|
yield(self.config) if block_given?
|
|
self.config
|
|
end
|
|
|
|
def self.check_permissions(path)
|
|
perm = File.stat(path).mode & 0777
|
|
if perm != 0600 && !(WINDOWS) && !(Netrc.config[:allow_permissive_netrc_file])
|
|
raise Error, "Permission bits for '#{path}' should be 0600, but are "+perm.to_s(8)
|
|
end
|
|
end
|
|
|
|
# Reads path and parses it as a .netrc file. If path doesn't
|
|
# exist, returns an empty object. Decrypt paths ending in .gpg.
|
|
def self.read(path=default_path)
|
|
check_permissions(path)
|
|
data = if path =~ /\.gpg$/
|
|
decrypted = `gpg --batch --quiet --decrypt #{path}`
|
|
if $?.success?
|
|
decrypted
|
|
else
|
|
raise Error.new("Decrypting #{path} failed.") unless $?.success?
|
|
end
|
|
else
|
|
File.read(path)
|
|
end
|
|
new(path, parse(lex(data.lines.to_a)))
|
|
rescue Errno::ENOENT
|
|
new(path, parse(lex([])))
|
|
end
|
|
|
|
class TokenArray < Array
|
|
def take
|
|
if length < 1
|
|
raise Error, "unexpected EOF"
|
|
end
|
|
shift
|
|
end
|
|
|
|
def readto
|
|
l = []
|
|
while length > 0 && ! yield(self[0])
|
|
l << shift
|
|
end
|
|
return l.join
|
|
end
|
|
end
|
|
|
|
def self.lex(lines)
|
|
tokens = TokenArray.new
|
|
for line in lines
|
|
content, comment = line.split(/(\s*#.*)/m)
|
|
content.each_char do |char|
|
|
case char
|
|
when /\s/
|
|
if tokens.last && tokens.last[-1..-1] =~ /\s/
|
|
tokens.last << char
|
|
else
|
|
tokens << char
|
|
end
|
|
else
|
|
if tokens.last && tokens.last[-1..-1] =~ /\S/
|
|
tokens.last << char
|
|
else
|
|
tokens << char
|
|
end
|
|
end
|
|
end
|
|
if comment
|
|
tokens << comment
|
|
end
|
|
end
|
|
tokens
|
|
end
|
|
|
|
def self.skip?(s)
|
|
s =~ /^\s/
|
|
end
|
|
|
|
|
|
|
|
# Returns two values, a header and a list of items.
|
|
# Each item is a tuple, containing some or all of:
|
|
# - machine keyword (including trailing whitespace+comments)
|
|
# - machine name
|
|
# - login keyword (including surrounding whitespace+comments)
|
|
# - login
|
|
# - password keyword (including surrounding whitespace+comments)
|
|
# - password
|
|
# - trailing chars
|
|
# This lets us change individual fields, then write out the file
|
|
# with all its original formatting.
|
|
def self.parse(ts)
|
|
cur, item = [], []
|
|
|
|
unless ts.is_a?(TokenArray)
|
|
ts = TokenArray.new(ts)
|
|
end
|
|
|
|
pre = ts.readto{|t| t == "machine" || t == "default"}
|
|
|
|
while ts.length > 0
|
|
if ts[0] == 'default'
|
|
cur << ts.take.to_sym
|
|
cur << ''
|
|
else
|
|
cur << ts.take + ts.readto{|t| ! skip?(t)}
|
|
cur << ts.take
|
|
end
|
|
|
|
if ts.include?('login')
|
|
cur << ts.readto{|t| t == "login"} + ts.take + ts.readto{|t| ! skip?(t)}
|
|
cur << ts.take
|
|
end
|
|
|
|
if ts.include?('password')
|
|
cur << ts.readto{|t| t == "password"} + ts.take + ts.readto{|t| ! skip?(t)}
|
|
cur << ts.take
|
|
end
|
|
|
|
cur << ts.readto{|t| t == "machine" || t == "default"}
|
|
|
|
item << cur
|
|
cur = []
|
|
end
|
|
|
|
[pre, item]
|
|
end
|
|
|
|
def initialize(path, data)
|
|
@new_item_prefix = ''
|
|
@path = path
|
|
@pre, @data = data
|
|
|
|
if @data && @data.last && :default == @data.last[0]
|
|
@default = @data.pop
|
|
else
|
|
@default = nil
|
|
end
|
|
end
|
|
|
|
attr_accessor :new_item_prefix
|
|
|
|
def [](k)
|
|
if item = @data.detect {|datum| datum[1] == k}
|
|
Entry.new(item[3], item[5])
|
|
elsif @default
|
|
Entry.new(@default[3], @default[5])
|
|
end
|
|
end
|
|
|
|
def []=(k, info)
|
|
if item = @data.detect {|datum| datum[1] == k}
|
|
item[3], item[5] = info
|
|
else
|
|
@data << new_item(k, info[0], info[1])
|
|
end
|
|
end
|
|
|
|
def length
|
|
@data.length
|
|
end
|
|
|
|
def delete(key)
|
|
datum = nil
|
|
for value in @data
|
|
if value[1] == key
|
|
datum = value
|
|
break
|
|
end
|
|
end
|
|
@data.delete(datum)
|
|
end
|
|
|
|
def each(&block)
|
|
@data.each(&block)
|
|
end
|
|
|
|
def new_item(m, l, p)
|
|
[new_item_prefix+"machine ", m, "\n login ", l, "\n password ", p, "\n"]
|
|
end
|
|
|
|
def save
|
|
if @path =~ /\.gpg$/
|
|
e = IO.popen("gpg -a --batch --default-recipient-self -e", "r+") do |gpg|
|
|
gpg.puts(unparse)
|
|
gpg.close_write
|
|
gpg.read
|
|
end
|
|
raise Error.new("Encrypting #{@path} failed.") unless $?.success?
|
|
File.open(@path, 'w', 0600) {|file| file.print(e)}
|
|
else
|
|
File.open(@path, 'w', 0600) {|file| file.print(unparse)}
|
|
end
|
|
end
|
|
|
|
def unparse
|
|
@pre + @data.map do |datum|
|
|
datum = datum.join
|
|
unless datum[-1..-1] == "\n"
|
|
datum << "\n"
|
|
else
|
|
datum
|
|
end
|
|
end.join
|
|
end
|
|
|
|
Entry = Struct.new(:login, :password) do
|
|
alias to_ary to_a
|
|
end
|
|
|
|
end
|
|
|
|
class Netrc::Error < ::StandardError
|
|
end
|