cf-uaac 1.3.0

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.
@@ -0,0 +1,18 @@
1
+ #--
2
+ # Cloud Foundry 2012.02.03 Beta
3
+ # Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
4
+ #
5
+ # This product is licensed to you under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this product except in compliance with the License.
7
+ #
8
+ # This product includes a number of subcomponents with
9
+ # separate copyright notices and license terms. Your use of these
10
+ # subcomponents is subject to the terms and conditions of the
11
+ # subcomponent's license, as noted in the LICENSE file.
12
+ #++
13
+
14
+ module CF
15
+ module UAA
16
+ CLI_VERSION = "1.3.0"
17
+ end
18
+ end
data/lib/stub/scim.rb ADDED
@@ -0,0 +1,387 @@
1
+ #--
2
+ # Cloud Foundry 2012.02.03 Beta
3
+ # Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
4
+ #
5
+ # This product is licensed to you under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this product except in compliance with the License.
7
+ #
8
+ # This product includes a number of subcomponents with
9
+ # separate copyright notices and license terms. Your use of these
10
+ # subcomponents is subject to the terms and conditions of the
11
+ # subcomponent's license, as noted in the LICENSE file.
12
+ #++
13
+
14
+ require 'set'
15
+ require 'time'
16
+ require 'uaa/util'
17
+
18
+ module CF::UAA
19
+
20
+ class SchemaViolation < RuntimeError; end
21
+ class AlreadyExists < RuntimeError; end
22
+ class BadFilter < RuntimeError; end
23
+ class BadVersion < RuntimeError; end
24
+
25
+ class StubScim
26
+
27
+ private
28
+
29
+ # attribute types. Anything not listed is case-ignore string
30
+ HIDDEN_ATTRS = [:rtype, :password, :client_secret].to_set
31
+ READ_ONLY_ATTRS = [:rtype, :id, :meta, :groups].to_set
32
+ BOOLEANS = [:active].to_set
33
+ NUMBERS = [:access_token_validity, :refresh_token_validity].to_set
34
+ GROUPS = [:groups, :auto_approved_scope, :scope, :authorities].to_set
35
+ REFERENCES = [*GROUPS, :members, :owners, :readers].to_set # users or groups
36
+ ENUMS = { authorized_grant_types: ["client_credentials", "implicit",
37
+ "authorization_code", "password", "refresh_token"].to_set }
38
+ GENERAL_MULTI = [:emails, :phonenumbers, :ims, :photos, :entitlements,
39
+ :roles, :x509certificates].to_set
40
+ GENERAL_SUBATTRS = [:value, :display, :primary, :type].to_set
41
+ EXPLICIT_SINGLE = {
42
+ name: [:formatted, :familyname, :givenname, :middlename,
43
+ :honorificprefix, :honorificsuffix].to_set,
44
+ meta: [:created, :lastmodified, :location, :version].to_set }
45
+ EXPLICIT_MULTI = {
46
+ addresses: [:formatted, :streetaddress, :locality, :region,
47
+ :postal_code, :country, :primary, :type].to_set,
48
+ authorizations: [:client_id, :approved, :denied, :exp].to_set }
49
+
50
+ # resource class definitions: naming and legal attributes
51
+ NAME_ATTR = { user: :username, client: :client_id, group: :displayname }
52
+ COMMON_ATTRS = [:rtype, :externalid, :id, :meta].to_set
53
+ LEGAL_ATTRS = {
54
+ user: [*COMMON_ATTRS, :displayname, :username, :nickname,
55
+ :profileurl, :title, :usertype, :preferredlanguage, :locale,
56
+ :timezone, :active, :password, :emails, :phonenumbers, :ims, :photos,
57
+ :entitlements, :roles, :x509certificates, :name, :addresses,
58
+ :authorizations, :groups].to_set,
59
+ client: [*COMMON_ATTRS, :client_id, :client_secret, :authorities,
60
+ :authorized_grant_types, :scope, :auto_approved_scope,
61
+ :access_token_validity, :refresh_token_validity, :redirect_uri].to_set,
62
+ group: [*COMMON_ATTRS, :displayname, :members, :owners, :readers].to_set }
63
+ VISIBLE_ATTRS = {user: Set.new(LEGAL_ATTRS[:user] - HIDDEN_ATTRS),
64
+ client: Set.new(LEGAL_ATTRS[:client] - HIDDEN_ATTRS),
65
+ group: Set.new(LEGAL_ATTRS[:group] - HIDDEN_ATTRS)}
66
+ ATTR_NAMES = LEGAL_ATTRS.each_with_object(Set.new) { |(k, v), s|
67
+ v.each {|a| s << a.to_s }
68
+ }
69
+ SUBATTR_NAMES = GENERAL_SUBATTRS.each_with_object(Set.new) { |sa, s| s << sa.to_s } +
70
+ [*EXPLICIT_SINGLE, *EXPLICIT_MULTI].each_with_object(Set.new) { |(k, v), s|
71
+ v.each {|sa| s << sa.to_s }
72
+ }
73
+
74
+ def self.remove_hidden(attrs = nil) attrs - HIDDEN_ATTRS if attrs end
75
+ def self.searchable_attribute(attr)
76
+ attr if ATTR_NAMES.include?(attr) && !HIDDEN_ATTRS.include?(attr = attr.to_sym)
77
+ end
78
+
79
+ def remove_attrs(stuff, attrs = HIDDEN_ATTRS)
80
+ attrs.each { |a| stuff.delete(a.to_s) }
81
+ stuff
82
+ end
83
+
84
+ def valid_id?(id, rtype)
85
+ id && (t = @things_by_id[id]) && (rtype.nil? || t[:rtype] == rtype)
86
+ end
87
+
88
+ def ref_by_name(name, rtype) @things_by_name[rtype.to_s + name.downcase] end
89
+
90
+ def ref_by_id(id, rtype = nil)
91
+ (t = @things_by_id[id]) && (rtype.nil? || t[:rtype] == rtype) ? t : nil
92
+ end
93
+
94
+ def valid_complex?(value, subattrs, simple_ok = false)
95
+ return true if simple_ok && value.is_a?(String)
96
+ return unless value.is_a?(Hash) && (!simple_ok || value.key?("value"))
97
+ value.each { |k, v| return unless SUBATTR_NAMES.include?(k) && subattrs.include?(k.to_sym) }
98
+ end
99
+
100
+ def valid_multi?(values, subattrs, simple_ok = false)
101
+ return unless values.is_a?(Array)
102
+ values.each { |value| return unless valid_complex?(value, subattrs, simple_ok) }
103
+ end
104
+
105
+ def valid_ids?(value, rtype = nil)
106
+ return unless value.is_a?(Array)
107
+ value.each do |ref|
108
+ return unless ref.is_a?(String) && valid_id?(ref, rtype) ||
109
+ ref.is_a?(Hash) && valid_id?(ref["value"], rtype)
110
+ end
111
+ end
112
+
113
+ def enforce_schema(rtype, stuff)
114
+ stuff.each do |ks, v|
115
+ unless ATTR_NAMES.include?(ks.to_s) && LEGAL_ATTRS[rtype].include?(k = ks.to_sym)
116
+ raise SchemaViolation, "illegal #{ks} on #{rtype}"
117
+ end
118
+ if READ_ONLY_ATTRS.include?(k)
119
+ raise SchemaViolation, "attempt to modify read-only attribute #{k} on #{rtype}"
120
+ end
121
+ valid_attr = case k
122
+ when *BOOLEANS then v == !!v
123
+ when *NUMBERS then v.is_a?(Integer)
124
+ when *GENERAL_MULTI then valid_multi?(v, GENERAL_SUBATTRS, true)
125
+ when *GROUPS then valid_ids?(v, :group)
126
+ when *REFERENCES then valid_ids?(v)
127
+ when ENUMS[k] then ENUMS[k].include?(v)
128
+ when *EXPLICIT_SINGLE.keys then valid_complex?(v, EXPLICIT_SINGLE[k])
129
+ when *EXPLICIT_MULTI.keys then valid_multi?(v, EXPLICIT_MULTI[k])
130
+ else k.is_a?(String) || k.is_a?(Symbol)
131
+ end
132
+ raise SchemaViolation, "#{v} is an invalid #{k}" unless valid_attr
133
+ end
134
+ end
135
+
136
+ def input(stuff)
137
+ thing = Util.hash_keys(stuff.dup, :tosym)
138
+ REFERENCES.each {|a|
139
+ next unless thing[a]
140
+ thing[a] = thing[a].each_with_object(Set.new) { |r, s|
141
+ s << (r.is_a?(Hash)? r[:value] : r )
142
+ }
143
+ }
144
+ GENERAL_MULTI.each {|a|
145
+ next unless thing[a]
146
+ thing[a] = thing[a].each_with_object({}) { |v, o|
147
+ v = {value: v} unless v.is_a?(Hash)
148
+ # enforce values are unique by type and value
149
+ k = URI.encode_www_form(t: [v[:type], v: v[:value]]).downcase
150
+ o[k] = v
151
+ }
152
+ }
153
+ thing
154
+ end
155
+
156
+ def output(thing, attrs = nil)
157
+ attrs = thing.keys if attrs.nil? || attrs.empty?
158
+ attrs.each_with_object({}) {|a, o|
159
+ next unless thing[a]
160
+ case a
161
+ when *REFERENCES then o[a] = thing[a].to_a
162
+ when *GENERAL_MULTI then o[a] = thing[a].values
163
+ else o[a] = thing[a]
164
+ end
165
+ }
166
+ end
167
+
168
+ def add_user_groups(gid, members)
169
+ members.each {|m| (m[:groups] ||= Set.new) << gid if m = ref_by_id(m, :user)} if members
170
+ end
171
+
172
+ def remove_user_groups(gid, members)
173
+ members.each {|m| m[:groups].delete(gid) if m = ref_by_id(m, :user) } if members
174
+ end
175
+
176
+ public
177
+
178
+ def initialize; @things_by_id, @things_by_name = {}, {} end
179
+ def name(id, rtype = nil) (t = ref_by_id(id, rtype))? t[NAME_ATTR[t[:rtype]]]: nil end
180
+ def id(name, rtype) (t = ref_by_name(name, rtype))? t[:id] : nil end
181
+
182
+ def add(rtype, stuff)
183
+ unless stuff.is_a?(Hash) && (name = stuff[NAME_ATTR[rtype].to_s])
184
+ raise SchemaViolation, "new #{rtype} has no name #{NAME_ATTR[rtype]}"
185
+ end
186
+ raise AlreadyExists if @things_by_name.key?(name = rtype.to_s + name.downcase)
187
+ enforce_schema(rtype, stuff)
188
+ thing = input(stuff).merge!(rtype: rtype, id: (id = SecureRandom.uuid),
189
+ meta: { created: Time.now.iso8601, last_modified: Time.now.iso8601, version: 1 })
190
+ add_user_groups(id, thing[:members])
191
+ @things_by_id[id] = @things_by_name[name] = thing
192
+ id
193
+ end
194
+
195
+ def update(id, stuff, match_version = nil, match_type = nil)
196
+ raise NotFound unless thing = ref_by_id(id, match_type)
197
+ raise BadVersion if match_version && match_version != thing[:meta][:version]
198
+ enforce_schema(rtype = thing[:rtype], remove_attrs(stuff, READ_ONLY_ATTRS))
199
+ new_thing = input(stuff)
200
+ if newname = new_thing[NAME_ATTR[rtype]]
201
+ oldname = rtype.to_s + thing[NAME_ATTR[rtype]].downcase
202
+ unless (newname = rtype.to_s + newname.downcase) == oldname
203
+ raise AlreadyExists if @things_by_name.key?(newname)
204
+ @things_by_name.delete(oldname)
205
+ @things_by_name[newname] = thing
206
+ end
207
+ end
208
+ if new_thing[:members] || thing[:members]
209
+ old_members = thing[:members] || Set.new
210
+ new_members = new_thing[:members] || Set.new
211
+ remove_user_groups(id, old_members - new_members)
212
+ add_user_groups(id, new_members - old_members)
213
+ end
214
+ READ_ONLY_ATTRS.each { |a| new_thing[a] = thing[a] if thing[a] }
215
+ HIDDEN_ATTRS.each { |a| new_thing[a] = thing[a] if thing[a] }
216
+ thing.replace new_thing
217
+ thing[:meta][:version] += 1
218
+ thing[:meta][:lastmodified] == Time.now.iso8601
219
+ id
220
+ end
221
+
222
+ def add_member(gid, member)
223
+ return unless g = ref_by_id(gid, :group)
224
+ (g[:members] ||= Set.new) << member
225
+ add_user_groups(gid, Set[member])
226
+ end
227
+
228
+ def set_hidden_attr(id, attr, value)
229
+ raise NotFound unless thing = ref_by_id(id)
230
+ raise ArgumentError unless HIDDEN_ATTRS.include?(attr)
231
+ thing[attr] = value
232
+ end
233
+
234
+ def remove(id, rtype = nil)
235
+ return unless thing = ref_by_id(id, rtype)
236
+ rtype = thing[:rtype]
237
+ remove_user_groups(id, thing[:members])
238
+ @things_by_id.delete(id)
239
+ thing = @things_by_name.delete(rtype.to_s + thing[NAME_ATTR[rtype]].downcase)
240
+ remove_attrs(output(thing))
241
+ end
242
+
243
+ def get(id, rtype = nil, *attrs)
244
+ return unless thing = ref_by_id(id, rtype)
245
+ output(thing, attrs)
246
+ end
247
+
248
+ def get_by_name(name, rtype, *attrs)
249
+ return unless thing = ref_by_name(name, rtype)
250
+ output(thing, attrs)
251
+ end
252
+
253
+ def find(rtype, start = 0, count = nil, filter_string = nil, attrs = nil)
254
+ filter, total = ScimFilter.new(filter_string), 0
255
+ objs = @things_by_id.each_with_object([]) { |(k, v), o|
256
+ next unless rtype == v[:rtype] && filter.match?(v)
257
+ o << output(v, attrs) if total >= start && (count.nil? || o.length < count)
258
+ total += 1
259
+ }
260
+ [objs, total]
261
+ end
262
+
263
+ end
264
+
265
+ class ScimFilter
266
+
267
+ private
268
+
269
+ def eat_json_string
270
+ raise BadFilter unless @input.skip(/\s*"/)
271
+ str = ""
272
+ while true
273
+ case
274
+ when @input.scan(/[^\\"]+/); str << @input.matched
275
+ when @input.scan(%r{\\["\\/]}); str << @input.matched[-1]
276
+ when @input.scan(/\\[bfnrt]/); str << eval(%Q{"#{@input.matched}"})
277
+ when @input.scan(/\\u[0-9a-fA-F]{4}/); str << [Integer("0x#{@input.matched[2..-1]}")].pack("U")
278
+ else break
279
+ end
280
+ end
281
+ raise BadFilter unless @input.skip(/"\s*/)
282
+ str
283
+ end
284
+
285
+ def eat_word(*words)
286
+ @input.skip(/\s*/)
287
+ return unless s = @input.scan(/(\S+)\s*/)
288
+ w = @input[1].downcase
289
+ return w if words.empty? || words.include?(w)
290
+ @input.unscan
291
+ false
292
+ end
293
+
294
+ def eat_expr
295
+ if @input.skip(/\s*\(\s*/)
296
+ phrase = eat_phrase
297
+ raise BadFilter unless @input.skip(/\s*\)\s*/)
298
+ return phrase
299
+ end
300
+ raise BadFilter unless (attr = eat_word) &&
301
+ (op = eat_word("eq", "co", "sw", "pr", "gt", "ge", "lt", "le")) &&
302
+ (op == "pr" || value = eat_json_string)
303
+ (attr_sym = StubScim.searchable_attribute(attr)) ?
304
+ [:item, attr_sym, op, value] : [:undefined, attr, op, value]
305
+ end
306
+
307
+ # AND level
308
+ def eat_subphrase
309
+ phrase = [:and, eat_expr]
310
+ while eat_word("and"); phrase << eat_expr end
311
+ phrase.length == 2 ? phrase[1] : phrase
312
+ end
313
+
314
+ # OR level
315
+ def eat_phrase
316
+ phrase = [:or, eat_subphrase]
317
+ while eat_word("or"); phrase << eat_subphrase end
318
+ phrase.length == 2 ? phrase[1] : phrase
319
+ end
320
+
321
+ def eval_expr(entry, attr, op, value)
322
+ return false unless val = entry[attr]
323
+ return true if op == "pr"
324
+ case attr
325
+ when *StubScim::REFERENCES
326
+ return nil unless op == "eq"
327
+ val.each {|v| return true if v.casecmp(value) == 0 }
328
+ false
329
+ when *StubScim::GENERAL_MULTI
330
+ return nil unless op == "eq"
331
+ val.each {|k, v| return true if v.casecmp(value) == 0 }
332
+ false
333
+ else
334
+ case op
335
+ when "eq"; val.casecmp(value) == 0
336
+ when "sw"; val =~ /^#{Regexp.escape(value)}/i
337
+ when "co"; val =~ /#{Regexp.escape(value)}/i
338
+ when "gt"; val.casecmp(value) > 0
339
+ when "ge"; val.casecmp(value) >= 0
340
+ when "lt"; val.casecmp(value) < 0
341
+ when "le"; val.casecmp(value) <= 0
342
+ end
343
+ end
344
+ end
345
+
346
+ def eval(entry, filtr)
347
+ undefd = 0
348
+ case filtr[0]
349
+ when :undefined ; nil
350
+ when :item ; eval_expr(entry, filtr[1], filtr[2], filtr[3])
351
+ when :or
352
+ filtr[1..-1].each { |f|
353
+ return true if (res = eval(entry, f)) == true
354
+ undefd += 1 if res.nil?
355
+ }
356
+ filtr.length == undefd + 1 ? nil: false
357
+ when :and
358
+ filtr[1..-1].each { |f|
359
+ return false if (res = eval(entry, f)) == false
360
+ undefd += 1 if res.nil?
361
+ }
362
+ filtr.length == undefd + 1 ? nil: true
363
+ end
364
+ end
365
+
366
+ public
367
+
368
+ def initialize(filter_string)
369
+ if filter_string.nil?
370
+ @filter = true
371
+ else
372
+ @input = StringScanner.new(filter_string)
373
+ @filter = eat_phrase
374
+ raise BadFilter unless @input.eos?
375
+ end
376
+ self
377
+ rescue BadFilter => b
378
+ raise BadFilter, "invalid filter expression at offset #{@input.pos}: #{@input.string}"
379
+ end
380
+
381
+ def match?(entry)
382
+ @filter == true || eval(entry, @filter)
383
+ end
384
+
385
+ end
386
+
387
+ end
@@ -0,0 +1,310 @@
1
+ #--
2
+ # Cloud Foundry 2012.02.03 Beta
3
+ # Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
4
+ #
5
+ # This product is licensed to you under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this product except in compliance with the License.
7
+ #
8
+ # This product includes a number of subcomponents with
9
+ # separate copyright notices and license terms. Your use of these
10
+ # subcomponents is subject to the terms and conditions of the
11
+ # subcomponent's license, as noted in the LICENSE file.
12
+ #++
13
+
14
+ require 'eventmachine'
15
+ require 'date'
16
+ require 'logger'
17
+ require 'pp'
18
+ require 'erb'
19
+ require 'multi_json'
20
+
21
+ module Stub
22
+
23
+ class StubError < RuntimeError; end
24
+ class BadHeader < StubError; end
25
+
26
+ #------------------------------------------------------------------------------
27
+ class Request
28
+
29
+ attr_reader :headers, :body, :path, :method
30
+ def initialize; @state, @prelude = :init, "" end
31
+
32
+ private
33
+
34
+ def bslice(str, range)
35
+ # byteslice is available in ruby 1.9.3
36
+ str.respond_to?(:byteslice) ? str.byteslice(range) : str.slice(range)
37
+ end
38
+
39
+ def add_lines(str)
40
+ return @body << str if @state == :body
41
+ processed = 0
42
+ str.each_line("\r\n") do |ln|
43
+ processed += ln.bytesize
44
+ unless ln.chomp!("\r\n")
45
+ raise BadHeader unless ln.ascii_only?
46
+ return @prelude = ln # must be partial header at end of str
47
+ end
48
+ if @state == :init
49
+ start = ln.split(/\s+/)
50
+ @method, @path, @headers, @body = start[0].downcase, start[1], {}, ""
51
+ raise BadHeader unless @method.ascii_only? && @path.ascii_only?
52
+ @state = :headers
53
+ elsif ln.empty?
54
+ @state, @content_length = :body, headers["content-length"].to_i
55
+ return @body << bslice(str, processed..-1)
56
+ else
57
+ raise BadHeader unless ln.ascii_only?
58
+ key, sep, val = ln.partition(/:\s+/)
59
+ @headers[key.downcase] = val
60
+ end
61
+ end
62
+ end
63
+
64
+ public
65
+
66
+ # adds data to the request, returns true if request is complete
67
+ def completed?(str)
68
+ str, @prelude = @prelude + str, "" unless @prelude.empty?
69
+ add_lines(str)
70
+ return unless @state == :body && @body.bytesize >= @content_length
71
+ @prelude = bslice(@body, @content_length..-1)
72
+ @body = bslice(@body, 0..@content_length)
73
+ @state = :init
74
+ end
75
+
76
+ def cookies
77
+ return {} unless chdr = @headers["cookie"]
78
+ chdr.strip.split(/\s*;\s*/).each_with_object({}) do |pair, o|
79
+ k, v = pair.split(/\s*=\s*/)
80
+ o[k.downcase] = v
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ #------------------------------------------------------------------------------
87
+ class Reply
88
+ attr_accessor :status, :headers, :body
89
+ def initialize(status = 200) @status, @headers, @cookies, @body = status, {}, [], "" end
90
+ def to_s
91
+ reply = "HTTP/1.1 #{@status} OK\r\n"
92
+ headers["server"] = "stub server"
93
+ headers["date"] = DateTime.now.httpdate
94
+ headers["content-length"] = body.bytesize
95
+ headers.each { |k, v| reply << "#{k}: #{v}\r\n" }
96
+ @cookies.each { |c| reply << "Set-Cookie: #{c}\r\n" }
97
+ reply << "\r\n" << body
98
+ end
99
+ def json(status = nil, info)
100
+ info = {message: info} unless info.respond_to? :each
101
+ @status = status if status
102
+ headers["content-type"] = "application/json"
103
+ @body = MultiJson.dump(info)
104
+ nil
105
+ end
106
+ def text(status = nil, info)
107
+ @status = status if status
108
+ headers["content-type"] = "text/plain"
109
+ @body = info.pretty_inspect
110
+ nil
111
+ end
112
+ def html(status = nil, info)
113
+ @status = status if status
114
+ headers["content-type"] = "text/html"
115
+ info = ERB::Util.html_escape(info.pretty_inspect) unless info.is_a?(String)
116
+ @body = "<html><body>#{info}</body></html>"
117
+ nil
118
+ end
119
+ def set_cookie(name, value, options = {})
120
+ @cookies << options.each_with_object("#{name}=#{value}") { |(k, v), o|
121
+ o << (v.nil? ? "; #{k}" : "; #{k}=#{v}")
122
+ }
123
+ end
124
+ end
125
+
126
+ #------------------------------------------------------------------------------
127
+ # request handler logic -- server is initialized with a class derived from this.
128
+ # there will be one instance of this object per connection.
129
+ class Base
130
+ attr_accessor :request, :reply, :match, :server
131
+
132
+ def self.route(http_methods, matcher, filters = {}, &handler)
133
+ fail unless !EM.reactor_running? || EM.reactor_thread?
134
+ matcher = Regexp.new("^#{Regexp.escape(matcher.to_s)}$") unless matcher.is_a?(Regexp)
135
+ filters = filters.each_with_object({}) { |(k, v), o|
136
+ o[k.downcase] = v.is_a?(Regexp) ? v : Regexp.new("^#{Regexp.escape(v.to_s)}$")
137
+ }
138
+ @routes ||= {}
139
+ @route_number = @route_number.to_i + 1
140
+ route_name = "route_#{@route_number}".to_sym
141
+ define_method(route_name, handler)
142
+ [*http_methods].each do |m|
143
+ m = m.to_s.downcase
144
+ @routes[m] ||= []
145
+ i = @routes[m].index { |r| r[0].to_s.length < matcher.to_s.length }
146
+ unless i && @routes[m][i][0] == matcher
147
+ @routes[m].insert(i || -1, [matcher, filters, route_name])
148
+ end
149
+ end
150
+ end
151
+
152
+ def self.find_route(request)
153
+ fail unless EM.reactor_thread?
154
+ if @routes && (rary = @routes[request.method])
155
+ rary.each { |r; m|
156
+ next unless (m = r[0].match(request.path))
157
+ r[1].each { |k, v|
158
+ next if v.match(request.headers[k])
159
+ return reply_in_kind(400, "header '#{k}: #{request.headers[k]}' is not accepted")
160
+ }
161
+ return [m, r[2]]
162
+ }
163
+ end
164
+ [nil, :default_route]
165
+ end
166
+
167
+ def initialize(server)
168
+ @server, @request, @reply, @match = server, Request.new, Reply.new, nil
169
+ end
170
+
171
+ def process
172
+ @reply = Reply.new
173
+ @match, handler = self.class.find_route(request)
174
+ server.logger.debug "processing request to path #{request.path} for route #{@match ? @match.regexp : 'default'}"
175
+ send handler
176
+ reply.headers['connection'] ||= request.headers['connection'] if request.headers['connection']
177
+ server.logger.debug "replying to path #{request.path} with #{reply.body.length} bytes of #{reply.headers['content-type']}"
178
+ #server.logger.debug "full reply is: #{reply.body.inspect}"
179
+ rescue Exception => e
180
+ server.logger.debug "exception from route handler: #{e.message}"
181
+ server.trace { e.backtrace }
182
+ reply_in_kind 500, e
183
+ end
184
+
185
+ def reply_in_kind(status = nil, info)
186
+ case request.headers['accept']
187
+ when /application\/json/ then reply.json(status, info)
188
+ when /text\/html/ then reply.html(status, info)
189
+ else reply.text(status, info)
190
+ end
191
+ end
192
+
193
+ def default_route
194
+ reply_in_kind(404, error: "path not handled")
195
+ end
196
+
197
+ end
198
+
199
+ #------------------------------------------------------------------------------
200
+ module Connection
201
+ attr_accessor :req_handler
202
+ def unbind; req_handler.server.delete_connection(self) end
203
+
204
+ def receive_data(data)
205
+ #req_handler.server.logger.debug "got #{data.bytesize} bytes: #{data.inspect}"
206
+ return unless req_handler.request.completed? data
207
+ req_handler.process
208
+ send_data req_handler.reply.to_s
209
+ if req_handler.reply.headers['connection'] =~ /^close$/i || req_handler.server.status != :running
210
+ close_connection_after_writing
211
+ end
212
+ rescue Exception => e
213
+ req_handler.server.logger.debug "exception from receive_data: #{e.message}"
214
+ req_handler.server.trace { e.backtrace }
215
+ close_connection
216
+ end
217
+ end
218
+
219
+ #--------------------------------------------------------------------------
220
+ class Server
221
+ attr_reader :host, :port, :status, :logger
222
+ attr_accessor :info
223
+ def url; "http://#{@host}:#{@port}" end
224
+ def trace(msg = nil, &blk); logger.trace(msg, &blk) if logger.respond_to?(:trace) end
225
+
226
+ def initialize(req_handler, logger = Logger.new($stdout), info = nil)
227
+ @req_handler, @logger, @info = req_handler, logger, info
228
+ @connections, @status, @sig, @em_thread = [], :stopped, nil, nil
229
+ end
230
+
231
+ def start(hostname = "localhost", port = nil)
232
+ raise ArgumentError, "attempt to start a server that's already running" unless @status == :stopped
233
+ @host = hostname
234
+ logger.debug "starting #{self.class} server #{@host}"
235
+ EM.schedule do
236
+ @sig = EM.start_server(@host, port || 0, Connection) { |c| initialize_connection(c) }
237
+ @port = Socket.unpack_sockaddr_in(EM.get_sockname(@sig))[0]
238
+ logger.debug "#{self.class} server started at #{url}, signature #{@sig}"
239
+ end
240
+ @status = :running
241
+ self
242
+ end
243
+
244
+ def run_on_thread(hostname = "localhost", port = 0)
245
+ raise ArgumentError, "can't run on thread, EventMachine already running" if EM.reactor_running?
246
+ logger.debug { "starting eventmachine on thread" }
247
+ cthred = Thread.current
248
+ @em_thread = Thread.new do
249
+ begin
250
+ EM.run { start(hostname, port); cthred.run }
251
+ logger.debug "server thread done"
252
+ rescue Exception => e
253
+ logger.debug { "unhandled exception on stub server thread: #{e.message}" }
254
+ trace { e.backtrace }
255
+ raise
256
+ end
257
+ end
258
+ Thread.stop
259
+ logger.debug "running on thread"
260
+ self
261
+ end
262
+
263
+ def run(hostname = "localhost", port = 0)
264
+ raise ArgumentError, "can't run, EventMachine already running" if EM.reactor_running?
265
+ @em_thread = Thread.current
266
+ EM.run { start(hostname, port) }
267
+ logger.debug "server and event machine done"
268
+ end
269
+
270
+ # if on reactor thread, start shutting down but return if connections still
271
+ # in process, and let them disconnect when complete -- server is not really
272
+ # done until it's status is stopped.
273
+ # if not on reactor thread, wait until everything's cleaned up and stopped
274
+ def stop
275
+ logger.debug "stopping server"
276
+ @status = :stopping
277
+ EM.stop_server @sig
278
+ done if @connections.empty?
279
+ sleep 0.1 while @status != :stopped unless EM.reactor_thread?
280
+ end
281
+
282
+ def delete_connection(conn)
283
+ logger.debug "deleting connection"
284
+ fail unless EM.reactor_thread?
285
+ @connections.delete(conn)
286
+ done if @status != :running && @connections.empty?
287
+ end
288
+
289
+ private
290
+
291
+ def done
292
+ fail unless @connections.empty?
293
+ EM.stop if @em_thread && EM.reactor_running?
294
+ @connections, @status, @sig, @em_thread = [], :stopped, nil, nil
295
+ sleep 0.1 unless EM.reactor_thread? # give EM a chance to stop
296
+ logger.debug EM.reactor_running? ?
297
+ "server done but EM still running" : "server really done"
298
+ end
299
+
300
+ def initialize_connection(conn)
301
+ logger.debug "starting connection"
302
+ fail unless EM.reactor_thread?
303
+ @connections << conn
304
+ conn.req_handler = @req_handler.new(self)
305
+ conn.comm_inactivity_timeout = 30
306
+ end
307
+
308
+ end
309
+
310
+ end