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.
@@ -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 $stdin
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'] || object['$data'].keys).sort
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/auth'
11
- require 'noms/command/base'
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
- @client = HTTPClient.new :agent_name => "noms/#{NOMS::Command::VERSION}"
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
- @log.debug "(UserAgent) specified identities = #{attrs[:specified_identities]}"
32
- @auth = NOMS::Command::Auth.new(:logger => @log,
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
- def request(method, url, data=nil, headers={}, tries=10, identity=nil)
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
- response = @client.request(method.to_s.upcase, req_url, '', data, headers)
72
- @log.debug "-> #{response.status} #{response.reason} (#{response.content.size} bytes of #{response.contenttype})"
73
- @log.debug JSON.pretty_generate(response.headers)
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
- @log.debug " we have an identity #{identity} but are trying again"
127
+ # The identity we got was no good, try again
128
+ identity.clear
79
129
  if tries > 0
80
- @log.debug "loading authentication identity for #{url}"
81
- identity = @auth.load(url, response)
130
+ identity = @auth.load(req_url, response)
131
+ # httpclient
82
132
  @client.set_auth(identity['domain'], identity['username'], identity['password'])
83
- response, req_url = self.request(method, url, data, headers, tries - 1, identity)
133
+ return self.request(method, url, data, headers, tries - 1, identity)
84
134
  end
85
135
  else
86
- identity = @auth.load(url, response)
87
- @client.set_auth(identity['domain'], identity['username'], identity['password'])
88
- response, req_url = self.request(method, url, data, headers, 2, identity)
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['location'].first
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
- response, req_url = self.request(method, new_url, data, headers, tries - 1)
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.ok?
100
- @log.debug "Login succeeded, saving #{identity}"
101
- identity.save
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.status} #{response.reason} <- #{req_url}"
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