Skip to content

Commit

Permalink
Load GitHub token from netrc if it's not in AuthTokenFile
Browse files Browse the repository at this point in the history
Git, curl, and other tools already offer support for loading credentials
from ~/.netrc, namely when interacting with GitHub. This uses the netrc
gem to play along. This is implemented as a fallback, in case a token is
not found in the gist-specific file.

To avoid adding gem dependencies, the netrc library is imported from
https://github.com/heroku/netrc into the vendor folder. It is licensed
under the MIT license, the same as this project.

The standalone script build/gist was regenerated and pulls in missing
changes in addition to the netrc dependency.

Fixes defunkt#260.
  • Loading branch information
biochimia committed Apr 26, 2017
1 parent 21861ce commit 3d38e11
Show file tree
Hide file tree
Showing 4 changed files with 544 additions and 9 deletions.
4 changes: 3 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ task :standalone do
f.puts "#!/usr/bin/env ruby"
f.puts "# This is generated from https://github.com/defunkt/gist using 'rake standalone'"
f.puts "# any changes will be overwritten."
f.puts File.read("lib/gist.rb").split("require 'json'\n").join(File.read("vendor/json.rb"))
f.puts File.read("lib/gist.rb")
.split("require 'json'\n").join(File.read("vendor/json.rb"))
.split("require 'netrc'\n").join(File.read("vendor/netrc.rb"))

f.puts File.read("bin/gist").gsub(/^require.*gist.*\n/, '');
end
Expand Down
281 changes: 274 additions & 7 deletions build/gist
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,271 @@ rescue LoadError
require File.join File.dirname(File.dirname(__FILE__)), 'vendor', 'json.rb'
end

begin
# From https://raw.githubusercontent.com/heroku/netrc/262ef111/lib/netrc.rb
require 'rbconfig'
require 'io/console'

class Netrc
VERSION = "0.11.0"

# see http:https://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 = if ENV['GPG_AGENT_INFO']
`gpg --batch --quiet --decrypt #{path}`
else
print "Enter passphrase for #{path}: "
STDIN.noecho do
`gpg --batch --passphrase-fd 0 --quiet --decrypt #{path}`
end
end
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
rescue LoadError
require File.join File.dirname(File.dirname(__FILE__)), 'vendor', 'netrc.rb'
end

# It just gists.
module Gist
extend self
Expand All @@ -1322,14 +1587,14 @@ module Gist

# A list of clipboard commands with copy and paste support.
CLIPBOARD_COMMANDS = {
'pbcopy' => 'pbpaste',
'xclip' => 'xclip -o',
'xsel -i' => 'xsel -o',
'pbcopy' => 'pbpaste',
'putclip' => 'getclip'
'putclip' => 'getclip',
}

GITHUB_API_URL = URI("https://api.github.com/")
GIT_IO_URL = URI("http:https://git.io")
GIT_IO_URL = URI("https:https://git.io")

GITHUB_BASE_PATH = ""
GHE_BASE_PATH = "/api/v3"
Expand All @@ -1350,7 +1615,7 @@ module Gist
module AuthTokenFile
def self.filename
if ENV.key?(URL_ENV_NAME)
File.expand_path "~/.gist.#{ENV[URL_ENV_NAME].gsub(/[^a-z.]/, '')}"
File.expand_path "~/.gist.#{ENV[URL_ENV_NAME].gsub(/:/, '.').gsub(/[^a-z0-9.]/, '')}"
else
File.expand_path "~/.gist"
end
Expand All @@ -1371,7 +1636,7 @@ module Gist
#
# @return [String] string value of access token or `nil`, if not found
def auth_token
@token ||= AuthTokenFile.read rescue nil
@token ||= AuthTokenFile.read rescue nil || Netrc.read[self.api_url.host][1]
end

# Upload a gist to https://gist.github.com
Expand Down Expand Up @@ -1569,10 +1834,12 @@ module Gist
# @param [String] url
# @return [String] shortened url, or long url if shortening fails
def shorten(url)
request = Net::HTTP::Post.new("/")
request = Net::HTTP::Post.new("/create")
request.set_form_data(:url => url)
response = http(GIT_IO_URL, request)
case response.code
when "200"
URI.join(GIT_IO_URL, response.body).to_s
when "201"
response['Location']
else
Expand Down Expand Up @@ -1645,7 +1912,7 @@ module Gist

if Net::HTTPCreated === response
AuthTokenFile.write JSON.parse(response.body)['token']
puts "Success! #{ENV[URL_ENV_NAME] || "https://github.com/"}settings/applications"
puts "Success! #{ENV[URL_ENV_NAME] || "https://github.com/"}settings/tokens"
return
elsif Net::HTTPUnauthorized === response
puts "Error: #{JSON.parse(response.body)['message']}"
Expand Down
8 changes: 7 additions & 1 deletion lib/gist.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
require File.join File.dirname(File.dirname(__FILE__)), 'vendor', 'json.rb'
end

begin
require 'netrc'
rescue LoadError
require File.join File.dirname(File.dirname(__FILE__)), 'vendor', 'netrc.rb'
end

# It just gists.
module Gist
extend self
Expand Down Expand Up @@ -65,7 +71,7 @@ def self.write(token)
#
# @return [String] string value of access token or `nil`, if not found
def auth_token
@token ||= AuthTokenFile.read rescue nil
@token ||= AuthTokenFile.read rescue nil || Netrc.read[self.api_url.host][1]
end

# Upload a gist to https://gist.github.com
Expand Down
Loading

0 comments on commit 3d38e11

Please sign in to comment.