noms-command 0.5.0 → 2.1.1
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.
- checksums.yaml +4 -4
- data/README.rst +104 -37
- data/TODO.rst +3 -3
- data/fixture/dnc.rb +110 -1
- data/fixture/public/dnc.json +6 -5
- data/fixture/public/lib/dnc.js +171 -24
- data/fixture/public/lib/nomsargs.js +72 -0
- data/lib/noms/command.rb +37 -8
- data/lib/noms/command/application.rb +32 -26
- data/lib/noms/command/auth.rb +44 -62
- data/lib/noms/command/auth/identity.rb +205 -5
- data/lib/noms/command/base.rb +11 -1
- data/lib/noms/command/formatter.rb +5 -4
- data/lib/noms/command/home.rb +21 -0
- data/lib/noms/command/useragent.rb +117 -40
- data/lib/noms/command/useragent/cache.rb +124 -0
- data/lib/noms/command/useragent/requester.rb +48 -0
- data/lib/noms/command/useragent/requester/httpclient.rb +61 -0
- data/lib/noms/command/useragent/requester/typhoeus.rb +73 -0
- data/lib/noms/command/useragent/response.rb +202 -0
- data/lib/noms/command/useragent/response/httpclient.rb +59 -0
- data/lib/noms/command/useragent/response/typhoeus.rb +74 -0
- data/lib/noms/command/version.rb +1 -1
- data/lib/noms/command/window.rb +21 -3
- data/lib/noms/command/xmlhttprequest.rb +8 -8
- data/noms-command.gemspec +3 -1
- data/spec/07js_spec.rb +1 -1
- data/spec/10auth_spec.rb +132 -0
- data/spec/11useragent_cache_spec.rb +160 -0
- data/spec/12useragent_auth_cookie_spec.rb +53 -0
- data/spec/13useragent_auth_spec.rb +90 -0
- data/spec/spec_helper.rb +5 -0
- metadata +46 -4
- data/fixture/public/lib/noms-args.js +0 -13
data/lib/noms/command/base.rb
CHANGED
@@ -12,8 +12,18 @@ end
|
|
12
12
|
|
13
13
|
class NOMS::Command::Base
|
14
14
|
|
15
|
+
attr_accessor :logger
|
16
|
+
|
17
|
+
def logger
|
18
|
+
@log
|
19
|
+
end
|
20
|
+
|
21
|
+
def logger=(new_logger)
|
22
|
+
@log = new_logger
|
23
|
+
end
|
24
|
+
|
15
25
|
def default_logger
|
16
|
-
log = Logger.new $
|
26
|
+
log = Logger.new $stderr
|
17
27
|
log.level = Logger::WARN
|
18
28
|
log.level = Logger::DEBUG if ENV['NOMS_DEBUG']
|
19
29
|
log
|
@@ -14,10 +14,12 @@ class NOMS::Command
|
|
14
14
|
|
15
15
|
end
|
16
16
|
|
17
|
-
class NOMS::Command::Formatter
|
17
|
+
class NOMS::Command::Formatter < NOMS::Command::Base
|
18
18
|
|
19
19
|
def initialize(data=nil, opt={})
|
20
|
+
@log = opt[:logger] || default_logger
|
20
21
|
@data = data
|
22
|
+
@log.debug("Created formatter for: #{@data.inspect}");
|
21
23
|
@format_raw_object = opt[:format_raw_object] || lambda { |o| o.to_yaml }
|
22
24
|
end
|
23
25
|
|
@@ -60,7 +62,6 @@ class NOMS::Command::Formatter
|
|
60
62
|
end
|
61
63
|
|
62
64
|
def render_object_list(objlist)
|
63
|
-
objlist['$labels'] ||= true
|
64
65
|
objlist['$format'] ||= 'lines'
|
65
66
|
raise NOMS::Command::Error.new("objectlist ('lines' format) must contain '$columns' list") unless
|
66
67
|
objlist['$columns'] and objlist['$columns'].respond_to? :map
|
@@ -160,7 +161,7 @@ class NOMS::Command::Formatter
|
|
160
161
|
|
161
162
|
def render_object_record(object)
|
162
163
|
labels = object.has_key?('$labels') ? object['$labels'] : true
|
163
|
-
fields = (object['$fields']
|
164
|
+
fields = (object['$fields'].nil? or object['$fields'].empty?) ? object['$data'].keys.sort : object['$fields']
|
164
165
|
data = object['$data']
|
165
166
|
fields.map do |field|
|
166
167
|
(labels ? (field + ': ') : '' ) + _string(data[field])
|
@@ -168,7 +169,7 @@ class NOMS::Command::Formatter
|
|
168
169
|
end
|
169
170
|
|
170
171
|
def filter_object(object)
|
171
|
-
if object['$fields']
|
172
|
+
if (object['$fields'] and ! object['$fields'].empty?)
|
172
173
|
Hash[object['$fields'].map { |f| [f, object['$data'][f]] }]
|
173
174
|
else
|
174
175
|
object['$data']
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'noms/command/version'
|
4
|
+
|
5
|
+
class NOMS
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
class NOMS::Command
|
10
|
+
|
11
|
+
@@home = (ENV.has_key?('NOMS_HOME') and ! ENV['NOMS_HOME'].empty?) ? ENV['NOMS_HOME'] : File.join(ENV['HOME'], '.noms')
|
12
|
+
|
13
|
+
def self.home=(value)
|
14
|
+
@@home = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.home
|
18
|
+
@@home
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -7,8 +7,10 @@ require 'httpclient'
|
|
7
7
|
require 'uri'
|
8
8
|
require 'highline/import'
|
9
9
|
|
10
|
-
require 'noms/command
|
11
|
-
require 'noms/command/
|
10
|
+
require 'noms/command'
|
11
|
+
require 'noms/command/useragent/cache'
|
12
|
+
require 'noms/command/useragent/requester'
|
13
|
+
require 'noms/command/useragent/response'
|
12
14
|
|
13
15
|
class NOMS
|
14
16
|
|
@@ -20,24 +22,34 @@ end
|
|
20
22
|
|
21
23
|
class NOMS::Command::UserAgent < NOMS::Command::Base
|
22
24
|
|
25
|
+
attr_accessor :cache, :cacher, :auth
|
26
|
+
|
23
27
|
def initialize(origin, attrs={})
|
24
|
-
@origin = origin
|
25
|
-
|
28
|
+
@origin = origin.respond_to?(:scheme) ? origin : URI.parse(origin)
|
29
|
+
# httpclient
|
26
30
|
# TODO Replace with TOFU implementation
|
27
|
-
@client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
28
|
-
@redirect_checks = [ ]
|
29
31
|
@log = attrs[:logger] || default_logger
|
32
|
+
cookies = attrs.has_key?(:cookies) ? attrs[:cookies] : true
|
33
|
+
|
34
|
+
@client = NOMS::Command::UserAgent::Requester.new :logger => @log, :cookies => cookies
|
35
|
+
|
36
|
+
@redirect_checks = [ ]
|
37
|
+
@plaintext_identity = attrs[:plaintext_identity] || nil
|
38
|
+
|
39
|
+
@cache = attrs.has_key?(:cache) ? attrs[:cache] : true
|
40
|
+
@max_age = attrs[:max_age] || 3600
|
30
41
|
|
31
|
-
@
|
32
|
-
|
33
|
-
:specified_identities => (attrs[:specified_identities] || []))
|
34
|
-
# TODO: Set cookie jar to something origin-specific
|
35
|
-
# TODO: Set user-agent to something nomsy
|
36
|
-
# caching
|
37
|
-
@client.redirect_uri_callback = lambda do |uri, res|
|
38
|
-
raise NOMS::Command::Error.new "Bad redirect URL #{url}" unless check_redirect(uri)
|
39
|
-
@client.default_redirect_uri_callback(uri, res)
|
42
|
+
if @cache
|
43
|
+
@cacher = NOMS::Command::UserAgent::Cache.new
|
40
44
|
end
|
45
|
+
|
46
|
+
@auth = attrs[:auth] || NOMS::Command::Auth.new(:logger => @log,
|
47
|
+
:specified_identities =>
|
48
|
+
(attrs[:specified_identities] || []))
|
49
|
+
end
|
50
|
+
|
51
|
+
def clear_cache!
|
52
|
+
@cacher.clear! unless @cacher.nil?
|
41
53
|
end
|
42
54
|
|
43
55
|
def auth
|
@@ -45,17 +57,14 @@ class NOMS::Command::UserAgent < NOMS::Command::Base
|
|
45
57
|
end
|
46
58
|
|
47
59
|
def check_redirect(url)
|
48
|
-
@log.debug "Running #{@redirect_checks.size} redirect checks on #{url}" unless @redirect_checks.empty?
|
49
60
|
@redirect_checks.all? { |check| check.call(url) }
|
50
61
|
end
|
51
62
|
|
52
63
|
def origin=(new_origin)
|
53
|
-
@log.debug "Setting my origin to #{new_origin}"
|
54
64
|
@origin = new_origin
|
55
65
|
end
|
56
66
|
|
57
67
|
def absolute_url(url)
|
58
|
-
@log.debug "Calculating absolute url of #{url} in context of #{@origin}"
|
59
68
|
begin
|
60
69
|
url = URI.parse url unless url.respond_to? :scheme
|
61
70
|
url = URI.join(@origin, url) unless url.absolute?
|
@@ -65,43 +74,114 @@ class NOMS::Command::UserAgent < NOMS::Command::Base
|
|
65
74
|
end
|
66
75
|
end
|
67
76
|
|
68
|
-
|
77
|
+
# Calculate a key for caching based on the method and URL
|
78
|
+
def request_key(method, url, opt={})
|
79
|
+
OpenSSL::Digest::SHA1.new([method, url.to_s].join(' ')).hexdigest
|
80
|
+
end
|
81
|
+
|
82
|
+
def request(method, url, data=nil, headers={}, tries=10, identity=nil, cached=nil)
|
69
83
|
req_url = absolute_url(url)
|
70
84
|
@log.debug "#{method} #{req_url}" + (headers.empty? ? '' : headers.inspect)
|
71
|
-
|
72
|
-
|
73
|
-
@
|
85
|
+
|
86
|
+
# TODO: check Vary
|
87
|
+
if @cache and cached.nil? and method.to_s.upcase == 'GET'
|
88
|
+
key = request_key('GET', req_url)
|
89
|
+
cached_response = NOMS::Command::UserAgent::Response.from_cache(@cacher.get(key), :logger => @log)
|
90
|
+
if cached_response and cached_response.is_a? NOMS::Command::UserAgent::Response
|
91
|
+
cached_response.logger = @log
|
92
|
+
|
93
|
+
if cached_response.age < @max_age
|
94
|
+
if (cached_response.auth_hash.nil? or (identity and identity.auth_verify? cached_response.auth_hash))
|
95
|
+
if cached_response.current?
|
96
|
+
return [cached_response, req_url]
|
97
|
+
else
|
98
|
+
# Maybe we can revalidate it
|
99
|
+
if cached_response.etag
|
100
|
+
headers = { 'If-None-Match' => cached_response.etag }.merge headers
|
101
|
+
return self.request(method, url, data, headers, tries, identity, cached_response)
|
102
|
+
elsif cached_response.last_modified
|
103
|
+
headers = { 'If-Modified-Since' => cached_response.last_modified.httpdate }.merge headers
|
104
|
+
return self.request(method, url, data, headers, tries, identity, cached_response)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
begin
|
113
|
+
response = @client.request :method => method,
|
114
|
+
:url => req_url,
|
115
|
+
:body => data,
|
116
|
+
:headers => headers
|
117
|
+
rescue StandardError => e
|
118
|
+
@log.debug e.backtrace.join("\n")
|
119
|
+
raise NOMS::Command::Error.new "Couldn't retrieve #{req_url} (#{e.class}): #{e.message}"
|
120
|
+
end
|
121
|
+
@log.debug "-> #{response.statusText} (#{response.body.size} bytes of #{response.content_type})"
|
122
|
+
@log.debug { JSON.pretty_generate(response.header) }
|
123
|
+
|
74
124
|
case response.status
|
75
125
|
when 401
|
76
|
-
@log.debug " handling unauthorized"
|
77
126
|
if identity
|
78
|
-
|
127
|
+
# The identity we got was no good, try again
|
128
|
+
identity.clear
|
79
129
|
if tries > 0
|
80
|
-
@
|
81
|
-
|
130
|
+
identity = @auth.load(req_url, response)
|
131
|
+
# httpclient
|
82
132
|
@client.set_auth(identity['domain'], identity['username'], identity['password'])
|
83
|
-
|
133
|
+
return self.request(method, url, data, headers, tries - 1, identity)
|
84
134
|
end
|
85
135
|
else
|
86
|
-
identity = @auth.load(
|
87
|
-
|
88
|
-
|
136
|
+
identity = @auth.load(req_url, response)
|
137
|
+
# httpclient
|
138
|
+
if identity
|
139
|
+
@client.set_auth(identity['domain'], identity['username'], identity['password'])
|
140
|
+
return self.request(method, url, data, headers, 2, identity)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
identity = nil
|
144
|
+
when 304
|
145
|
+
# The cached response has been revalidated
|
146
|
+
if cached
|
147
|
+
# TODO: Update Date: and Expires:/Cache-Control: headers
|
148
|
+
# in cached response and re-cache, while maintaining
|
149
|
+
# original_date
|
150
|
+
key = request_key(method, req_url)
|
151
|
+
@cacher.freshen key
|
152
|
+
response, req_url = [cached, req_url]
|
153
|
+
else
|
154
|
+
raise NOMS::Command::Error.new "Server returned 304 Not Modified for #{new_url}, " +
|
155
|
+
"but we were not revalidating a cached copy"
|
89
156
|
end
|
90
157
|
when 302, 301
|
91
|
-
new_url = response.header
|
158
|
+
new_url = response.header('Location')
|
92
159
|
if check_redirect new_url
|
93
|
-
@log.debug "redirect to #{new_url}"
|
94
160
|
raise NOMS::Command::Error.new "Can't follow redirect to #{new_url}: too many redirects" if tries <= 0
|
95
|
-
|
161
|
+
return self.request(method, new_url, data, headers, tries - 1, identity)
|
96
162
|
end
|
97
163
|
end
|
98
164
|
|
99
|
-
if identity and response.
|
100
|
-
@log.debug "Login succeeded, saving #{identity}"
|
101
|
-
|
165
|
+
if identity and response.success?
|
166
|
+
@log.debug "Login succeeded, saving #{identity['username']} @ #{identity}"
|
167
|
+
if @plaintext_identity
|
168
|
+
identity.save :file => @plaintext_identity, :encrypt => false
|
169
|
+
else
|
170
|
+
identity.save :encrypt => true
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
if @cache and method.to_s.upcase == 'GET' and response.cacheable?
|
175
|
+
cache_object = response.cacheable_copy
|
176
|
+
cache_object.cached!
|
177
|
+
if identity
|
178
|
+
cache_object.auth_hash = identity.verification_hash
|
179
|
+
end
|
180
|
+
key = request_key(method, req_url)
|
181
|
+
@cacher.set(key, cache_object.to_cache)
|
102
182
|
end
|
103
183
|
|
104
|
-
@log.debug "<- #{response.
|
184
|
+
@log.debug "<- #{response.statusText} <- #{req_url}"
|
105
185
|
[response, req_url]
|
106
186
|
end
|
107
187
|
|
@@ -116,18 +196,15 @@ class NOMS::Command::UserAgent < NOMS::Command::Base
|
|
116
196
|
end
|
117
197
|
|
118
198
|
def add_redirect_check(&block)
|
119
|
-
@log.debug "Adding #{block} to redirect checks"
|
120
199
|
@redirect_checks << block
|
121
200
|
end
|
122
201
|
|
123
202
|
def clear_redirect_checks
|
124
|
-
@log.debug "Clearing redirect checks"
|
125
203
|
@redirect_checks = [ ]
|
126
204
|
end
|
127
205
|
|
128
206
|
def pop_redirect_check
|
129
207
|
unless @redirect_checks.empty?
|
130
|
-
@log.debug "Popping redirect check: #{@redirect_checks[-1]}"
|
131
208
|
@redirect_checks.pop
|
132
209
|
end
|
133
210
|
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'noms/command/version'
|
4
|
+
require 'noms/command/home'
|
5
|
+
|
6
|
+
require 'pstore'
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
require 'noms/command'
|
10
|
+
|
11
|
+
class NOMS
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class NOMS::Command
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class NOMS::Command::UserAgent < NOMS::Command::Base
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class NOMS::Command::UserAgent::Cache
|
24
|
+
|
25
|
+
@@format_version = '0'
|
26
|
+
@@max_cache_size = 10 * 1024 * 1024
|
27
|
+
@@trim_cache_size = 8 * 1024 * 1024
|
28
|
+
@@location = File.join(NOMS::Command.home, 'cache', @@format_version)
|
29
|
+
|
30
|
+
def self.clear!
|
31
|
+
FileUtils.rm_r @@location if File.directory? @@location
|
32
|
+
ensure_dir @@location
|
33
|
+
meta = PStore.new(File.join(@@location, 'metadata.db'))
|
34
|
+
meta.transaction do
|
35
|
+
meta[:cache_size] = 0
|
36
|
+
meta[:file_count] = 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ensure_dir(dir)
|
41
|
+
FileUtils.mkdir_p dir unless File.directory? dir
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
ensure_dir @@location
|
46
|
+
@meta = PStore.new File.join(@@location, 'metadata.db')
|
47
|
+
@meta.transaction do
|
48
|
+
@meta[:cache_size] ||= 0
|
49
|
+
@meta[:file_count] ||= 0
|
50
|
+
|
51
|
+
if @meta[:cache_size] > @@max_cache_size
|
52
|
+
trim!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def clear!
|
58
|
+
FileUtils.rm_r @@location if File.directory? @@location
|
59
|
+
ensure_dir @@location
|
60
|
+
@meta.transaction do
|
61
|
+
@meta[:cache_size] = 0
|
62
|
+
@meta[:file_count] = 0
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def cache_dir(key=nil)
|
67
|
+
if key
|
68
|
+
File.join(@@location, 'data', key[0 .. 1])
|
69
|
+
else
|
70
|
+
File.join(@@location, 'data')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def set(key, item_data)
|
75
|
+
size = 0
|
76
|
+
ensure_dir cache_dir(key)
|
77
|
+
File.open(File.join(cache_dir(key), key), 'w') do |fh|
|
78
|
+
fh.write item_data
|
79
|
+
size = fh.stat.size
|
80
|
+
end
|
81
|
+
@meta.transaction do
|
82
|
+
@meta[:cache_size] += size
|
83
|
+
@meta[:file_count] += 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def freshen(key)
|
88
|
+
cache_file = File.join(cache_dir(key), key)
|
89
|
+
File.utime(Time.now, Time.now, cache_file)
|
90
|
+
end
|
91
|
+
|
92
|
+
def get(key)
|
93
|
+
cache_file = File.join(cache_dir(key), key)
|
94
|
+
obj = nil
|
95
|
+
if File.exist? cache_file
|
96
|
+
obj = File.open(cache_file, 'r') do |fh|
|
97
|
+
fh.read
|
98
|
+
# Marshal.load(fh)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
obj
|
102
|
+
end
|
103
|
+
|
104
|
+
def trim!
|
105
|
+
# Trim cache, all within PStore transaction
|
106
|
+
cache_size = 0
|
107
|
+
files = Dir["#{@@location}/*/*"].map do |file|
|
108
|
+
stat = File.stat(file)
|
109
|
+
size = stat.size
|
110
|
+
mtime = stat.mtime
|
111
|
+
total += size
|
112
|
+
[file, size, mtime]
|
113
|
+
end.sort { |a, b| a[2] <=> b[2] }
|
114
|
+
file_count = files.length
|
115
|
+
while cache_size > @@trim_cache_size
|
116
|
+
file, size, = files.shift
|
117
|
+
File.unlink(file)
|
118
|
+
cache_size -= size
|
119
|
+
end
|
120
|
+
@meta[:cache_size] = cache_size
|
121
|
+
@meta[:file_count] = file_count
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require 'noms/command/version'
|
4
|
+
|
5
|
+
require 'noms/command/base'
|
6
|
+
|
7
|
+
class NOMS
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
class NOMS::Command
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class NOMS::Command::UserAgent < NOMS::Command::Base
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class NOMS::Command::UserAgent::Requester < NOMS::Command::Base
|
20
|
+
|
21
|
+
@@requester_class = 'httpclient'
|
22
|
+
|
23
|
+
def self.new(opts={})
|
24
|
+
case @@requester_class
|
25
|
+
when 'httpclient'
|
26
|
+
require 'noms/command/useragent/requester/httpclient'
|
27
|
+
NOMS::Command::UserAgent::Requester::HTTPClient.new(opts)
|
28
|
+
when 'typhoeus'
|
29
|
+
require 'noms/command/useragent/requester/typhoeus'
|
30
|
+
NOMS::Command::UserAgent::Requester::Typhoeus.new(opts)
|
31
|
+
else
|
32
|
+
raise NOMS::Command::Error.new "Internal error - no requester class #{@@requester_class}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(opt={})
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
def request(req_attr={})
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_auth(domain, user, password)
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|