cf-uaac 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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