doze 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- data/README +6 -0
- data/lib/doze/application.rb +92 -0
- data/lib/doze/collection/object.rb +14 -0
- data/lib/doze/entity.rb +62 -0
- data/lib/doze/error.rb +75 -0
- data/lib/doze/media_type.rb +135 -0
- data/lib/doze/negotiator.rb +107 -0
- data/lib/doze/request.rb +119 -0
- data/lib/doze/resource/error.rb +21 -0
- data/lib/doze/resource/proxy.rb +81 -0
- data/lib/doze/resource.rb +193 -0
- data/lib/doze/responder/error.rb +34 -0
- data/lib/doze/responder/main.rb +41 -0
- data/lib/doze/responder/resource.rb +262 -0
- data/lib/doze/responder.rb +58 -0
- data/lib/doze/response.rb +78 -0
- data/lib/doze/router/anchored_route_set.rb +68 -0
- data/lib/doze/router/route.rb +88 -0
- data/lib/doze/router/route_set.rb +34 -0
- data/lib/doze/router.rb +100 -0
- data/lib/doze/serialization/entity.rb +34 -0
- data/lib/doze/serialization/form_data_helpers.rb +40 -0
- data/lib/doze/serialization/html.rb +116 -0
- data/lib/doze/serialization/json.rb +29 -0
- data/lib/doze/serialization/multipart_form_data.rb +162 -0
- data/lib/doze/serialization/resource.rb +30 -0
- data/lib/doze/serialization/resource_proxy.rb +14 -0
- data/lib/doze/serialization/www_form_encoded.rb +42 -0
- data/lib/doze/serialization/yaml.rb +25 -0
- data/lib/doze/uri_template.rb +220 -0
- data/lib/doze/utils.rb +53 -0
- data/lib/doze/version.rb +3 -0
- data/lib/doze.rb +5 -0
- data/test/functional/auth_test.rb +69 -0
- data/test/functional/base.rb +159 -0
- data/test/functional/cache_header_test.rb +76 -0
- data/test/functional/direct_response_test.rb +16 -0
- data/test/functional/error_handling_test.rb +131 -0
- data/test/functional/get_and_conneg_test.rb +182 -0
- data/test/functional/media_type_extensions_test.rb +102 -0
- data/test/functional/media_type_test.rb +40 -0
- data/test/functional/method_support_test.rb +49 -0
- data/test/functional/non_get_method_test.rb +173 -0
- data/test/functional/precondition_test.rb +84 -0
- data/test/functional/raw_path_info_test.rb +69 -0
- data/test/functional/resource_representation_test.rb +14 -0
- data/test/functional/router_test.rb +196 -0
- data/test/functional/serialization_test.rb +142 -0
- data/test/functional/uri_template_test.rb +51 -0
- metadata +221 -0
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'doze/media_type'
|
2
|
+
require 'doze/serialization/entity'
|
3
|
+
require 'doze/serialization/form_data_helpers'
|
4
|
+
require 'doze/error'
|
5
|
+
require 'doze/utils'
|
6
|
+
require 'tempfile'
|
7
|
+
|
8
|
+
module Doze::Serialization
|
9
|
+
# Also ripped off largely from Merb::Parse.
|
10
|
+
#
|
11
|
+
# Small differences in the hash it returns for an uploaded file - it will have string keys,
|
12
|
+
# use media_type rather than content_type (for consistency with rest of doze) and adds a temp_path
|
13
|
+
# key.
|
14
|
+
#
|
15
|
+
# These enable it to be used interchangably with nginx upload module if you use config like eg:
|
16
|
+
#
|
17
|
+
# upload_set_form_field $upload_field_name[filename] "$upload_file_name";
|
18
|
+
# upload_set_form_field $upload_field_name[media_type] "$upload_content_type";
|
19
|
+
# upload_set_form_field $upload_field_name[temp_path] "$upload_tmp_path";
|
20
|
+
# upload_aggregate_form_field $upload_field_name[size] "$upload_file_size";
|
21
|
+
#
|
22
|
+
class Entity::MultipartFormData < Entity
|
23
|
+
include FormDataHelpers
|
24
|
+
|
25
|
+
NAME_REGEX = /Content-Disposition:.* name="?([^\";]*)"?/ni.freeze
|
26
|
+
CONTENT_TYPE_REGEX = /Content-Type: (.*)\r\n/ni.freeze
|
27
|
+
FILENAME_REGEX = /Content-Disposition:.* filename="?([^\";]*)"?/ni.freeze
|
28
|
+
CRLF = "\r\n".freeze
|
29
|
+
EOL = CRLF
|
30
|
+
|
31
|
+
def object_data(try_deserialize=true)
|
32
|
+
@object_data ||= if @lazy_object_data
|
33
|
+
@lazy_object_data.call
|
34
|
+
elsif try_deserialize
|
35
|
+
@binary_data_stream && deserialize_stream
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def deserialize_stream
|
40
|
+
boundary = @media_type_params && @media_type_params['boundary'] or raise "missing boundary parameter for multipart/form-data"
|
41
|
+
boundary = "--#{boundary}"
|
42
|
+
paramhsh = {}
|
43
|
+
buf = ""
|
44
|
+
input = @binary_data_stream
|
45
|
+
input.binmode if defined? input.binmode
|
46
|
+
boundary_size = boundary.size + EOL.size
|
47
|
+
bufsize = 16384
|
48
|
+
length = @binary_data_length or raise "expected Content-Length for multipart/form-data"
|
49
|
+
length -= boundary_size
|
50
|
+
# status is boundary delimiter line
|
51
|
+
status = input.read(boundary_size)
|
52
|
+
return {} if status == nil || status.empty?
|
53
|
+
raise "bad content body:\n'#{status}' should == '#{boundary + EOL}'" unless status == boundary + EOL
|
54
|
+
# second argument to Regexp.quote is for KCODE
|
55
|
+
rx = /(?:#{EOL})?#{Regexp.quote(boundary,'n')}(#{EOL}|--)/
|
56
|
+
loop {
|
57
|
+
head = nil
|
58
|
+
body = ''
|
59
|
+
filename = content_type = name = nil
|
60
|
+
read_size = 0
|
61
|
+
until head && buf =~ rx
|
62
|
+
i = buf.index("\r\n\r\n")
|
63
|
+
if( i == nil && read_size == 0 && length == 0 )
|
64
|
+
length = -1
|
65
|
+
break
|
66
|
+
end
|
67
|
+
if !head && i
|
68
|
+
head = buf.slice!(0, i+2) # First \r\n
|
69
|
+
buf.slice!(0, 2) # Second \r\n
|
70
|
+
|
71
|
+
# String#[] with 2nd arg here is returning
|
72
|
+
# a group from match data
|
73
|
+
filename = head[FILENAME_REGEX, 1]
|
74
|
+
content_type = head[CONTENT_TYPE_REGEX, 1]
|
75
|
+
name = head[NAME_REGEX, 1]
|
76
|
+
|
77
|
+
if filename && !filename.empty?
|
78
|
+
body = Tempfile.new(:Doze)
|
79
|
+
body.binmode if defined? body.binmode
|
80
|
+
end
|
81
|
+
next
|
82
|
+
end
|
83
|
+
|
84
|
+
# Save the read body part.
|
85
|
+
if head && (boundary_size+4 < buf.size)
|
86
|
+
body << buf.slice!(0, buf.size - (boundary_size+4))
|
87
|
+
end
|
88
|
+
|
89
|
+
read_size = bufsize < length ? bufsize : length
|
90
|
+
if( read_size > 0 )
|
91
|
+
c = input.read(read_size)
|
92
|
+
raise "bad content body" if c.nil? || c.empty?
|
93
|
+
buf << c
|
94
|
+
length -= c.size
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Save the rest.
|
99
|
+
if i = buf.index(rx)
|
100
|
+
# correct value of i for some edge cases
|
101
|
+
if (i > 2) && (j = buf.index(rx, i-2)) && (j < i)
|
102
|
+
i = j
|
103
|
+
end
|
104
|
+
body << buf.slice!(0, i)
|
105
|
+
buf.slice!(0, boundary_size+2)
|
106
|
+
|
107
|
+
length = -1 if $1 == "--"
|
108
|
+
end
|
109
|
+
|
110
|
+
if filename && !filename.empty?
|
111
|
+
body.rewind
|
112
|
+
data = {
|
113
|
+
"filename" => File.basename(filename),
|
114
|
+
"media_type" => content_type,
|
115
|
+
"tempfile" => body,
|
116
|
+
"temp_path" => body.path,
|
117
|
+
"size" => File.size(body.path)
|
118
|
+
}
|
119
|
+
else
|
120
|
+
data = body
|
121
|
+
end
|
122
|
+
paramhsh = normalize_params(paramhsh,name,data)
|
123
|
+
break if buf.empty? || length == -1
|
124
|
+
}
|
125
|
+
paramhsh
|
126
|
+
end
|
127
|
+
|
128
|
+
# This is designed to work with either actual file upload fields, or the corresponding
|
129
|
+
# fields generated by nginx upload module as described above.
|
130
|
+
#
|
131
|
+
# yields or returns a Doze::Entity for the uploaded file, with the correct media_type and binary_data_length.
|
132
|
+
# ensures to close and unlinks the underlying tempfile afterwards where used with a block.
|
133
|
+
def param_entity(name)
|
134
|
+
meta = object_data[name]; return unless meta.is_a?(Hash)
|
135
|
+
media_type = meta["media_type"] and media_type = Doze::MediaType[media_type] or return
|
136
|
+
size = meta["size"] && meta["size"].to_i
|
137
|
+
if (tempfile = meta["tempfile"])
|
138
|
+
temp_path = tempfile.path
|
139
|
+
elsif (temp_path = meta["temp_path"])
|
140
|
+
tempfile = File.open(meta["temp_path"], "rb")
|
141
|
+
end
|
142
|
+
return unless tempfile
|
143
|
+
entity = media_type.new_entity(:binary_data_stream => tempfile, :binary_data_length => size)
|
144
|
+
|
145
|
+
return entity unless block_given?
|
146
|
+
begin
|
147
|
+
yield entity
|
148
|
+
ensure
|
149
|
+
tempfile.close
|
150
|
+
begin
|
151
|
+
File.unlink(temp_path)
|
152
|
+
rescue StandardError
|
153
|
+
# we made an effort to clean up - but it is a tempfile, so no biggie
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# A browser-friendly media type for use with Doze::Serialization::Resource.
|
160
|
+
MULTIPART_FORM_DATA = Doze::MediaType.register('multipart/form-data', :entity_class => Entity::MultipartFormData)
|
161
|
+
end
|
162
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'doze/serialization/json'
|
2
|
+
require 'doze/serialization/www_form_encoded'
|
3
|
+
require 'doze/serialization/multipart_form_data'
|
4
|
+
require 'doze/serialization/html'
|
5
|
+
|
6
|
+
# A resource whose representations are all serializations of some ruby data.
|
7
|
+
# A good example of how to do media type negotiation
|
8
|
+
module Doze::Serialization
|
9
|
+
module Resource
|
10
|
+
# You probably want to override these
|
11
|
+
def serialization_media_types
|
12
|
+
[JSON, HTML]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Analogous to get, but returns data which may be serialized into entities of any one of serialization_media_types
|
16
|
+
def get_data
|
17
|
+
end
|
18
|
+
|
19
|
+
def get
|
20
|
+
serialization_media_types.map do |media_type|
|
21
|
+
media_type.entity_class.new(media_type, :lazy_object_data => proc {get_data})
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# You may want to be more particular than this if you can only deal with certain serialization types
|
26
|
+
def accepts_method_with_media_type?(method, entity)
|
27
|
+
entity.is_a?(Doze::Serialization::Entity)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'doze/serialization/resource'
|
2
|
+
require 'doze/resource/proxy'
|
3
|
+
|
4
|
+
class Doze::Serialization::ResourceProxy < Doze::Resource::Proxy
|
5
|
+
include Doze::Serialization::Resource
|
6
|
+
|
7
|
+
def serialization_media_types
|
8
|
+
target && target.serialization_media_types
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_data
|
12
|
+
target && target.get_data
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'doze/media_type'
|
2
|
+
require 'doze/serialization/entity'
|
3
|
+
require 'doze/serialization/form_data_helpers'
|
4
|
+
require 'doze/error'
|
5
|
+
require 'doze/utils'
|
6
|
+
|
7
|
+
module Doze::Serialization
|
8
|
+
# ripped off largely from Merb::Parse
|
9
|
+
# Supports PHP-style nested hashes via foo[bar][baz]=boz
|
10
|
+
class Entity::WWWFormEncoded < Entity
|
11
|
+
include FormDataHelpers
|
12
|
+
|
13
|
+
def serialize(value, prefix=nil)
|
14
|
+
case value
|
15
|
+
when Array
|
16
|
+
value.map {|v| serialize(v, "#{prefix}[]")}.join("&")
|
17
|
+
when Hash
|
18
|
+
value.map {|k,v| serialize(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))}.join("&")
|
19
|
+
else
|
20
|
+
"#{prefix}=#{escape(value)}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def deserialize(data)
|
25
|
+
query = {}
|
26
|
+
for pair in data.split(/[&;] */n)
|
27
|
+
key, value = unescape(pair).split('=',2)
|
28
|
+
next if key.nil?
|
29
|
+
if key.include?('[')
|
30
|
+
normalize_params(query, key, value)
|
31
|
+
else
|
32
|
+
query[key] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
query
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# A browser-friendly media type for use with Doze::Serialization::Resource.
|
40
|
+
WWW_FORM_ENCODED = Doze::MediaType.register('application/x-www-form-urlencoded', :entity_class => Entity::WWWFormEncoded)
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'doze/media_type'
|
3
|
+
require 'doze/serialization/entity'
|
4
|
+
require 'doze/error'
|
5
|
+
|
6
|
+
# Note that it isn't safe to accept YAML input, unless you trust the sender, as
|
7
|
+
# it is possible to craft a YAML message to allow remote code execution (see
|
8
|
+
# cve-2013-0156)
|
9
|
+
module Doze::Serialization
|
10
|
+
class Entity::YAML < Entity
|
11
|
+
def serialize(ruby_data)
|
12
|
+
ruby_data.to_yaml
|
13
|
+
end
|
14
|
+
|
15
|
+
def deserialize(binary_data)
|
16
|
+
begin
|
17
|
+
::YAML.load(binary_data)
|
18
|
+
rescue ::YAML::ParseError, ArgumentError
|
19
|
+
raise Doze::ClientEntityError, "Could not parse YAML"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
YAML = Doze::MediaType.register('application/yaml', :plus_suffix => 'yaml', :entity_class => Entity::YAML, :extension => 'yaml')
|
25
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# Implements a subset of URI template spec.
|
2
|
+
# This is somewhat optimised for fast matching and generation of URI strings, although probably
|
3
|
+
# a fair bit of mileage still to be gotten out of it.
|
4
|
+
class Doze::URITemplate
|
5
|
+
def self.compile(string, var_regexps={})
|
6
|
+
is_varexp = true
|
7
|
+
parts = string.split(/\{(.*?)\}/).map do |bit|
|
8
|
+
if (is_varexp = !is_varexp)
|
9
|
+
case bit
|
10
|
+
when /^\/(.*).quadhexbytes\*$/
|
11
|
+
QuadHexBytesVariable.new($1.to_sym)
|
12
|
+
else
|
13
|
+
var = bit.to_sym
|
14
|
+
Variable.new(var, var_regexps[var] || Variable::DEFAULT_REGEXP)
|
15
|
+
end
|
16
|
+
else
|
17
|
+
String.new(bit)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
template = parts.length > 1 ? Composite.new(parts) : parts.first
|
21
|
+
template.compile_expand!
|
22
|
+
template
|
23
|
+
end
|
24
|
+
|
25
|
+
# Compile a ruby string substitution expression for the 'expand' method to make filling out these templates blazing fast.
|
26
|
+
# This was actually a bottleneck in some simple cache lookups by list of URIs
|
27
|
+
def compile_expand!
|
28
|
+
instance_eval "def expand(vars); \"#{expand_code_fragment}\"; end", __FILE__, __LINE__
|
29
|
+
end
|
30
|
+
|
31
|
+
def anchored_regexp
|
32
|
+
@anchored_regexp ||= Regexp.new("^#{regexp_fragment}$")
|
33
|
+
end
|
34
|
+
|
35
|
+
def start_anchored_regexp
|
36
|
+
@start_anchored_regexp ||= Regexp.new("^#{regexp_fragment}")
|
37
|
+
end
|
38
|
+
|
39
|
+
def parts; @parts ||= [self]; end
|
40
|
+
|
41
|
+
def +(other)
|
42
|
+
other = String.new(other.to_s) unless other.is_a?(Doze::URITemplate)
|
43
|
+
Composite.new(parts + other.parts)
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect
|
47
|
+
"#<#{self.class} #{to_s}>"
|
48
|
+
end
|
49
|
+
|
50
|
+
def match(uri)
|
51
|
+
match = anchored_regexp.match(uri) or return
|
52
|
+
result = {}; captures = match.captures
|
53
|
+
variables.each_with_index do |var, index|
|
54
|
+
result[var.name] = var.translate_captured_string(captures[index])
|
55
|
+
end
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
def match_with_trailing(uri)
|
60
|
+
match = start_anchored_regexp.match(uri) or return
|
61
|
+
result = {}; captures = match.captures
|
62
|
+
variables.each_with_index do |var, index|
|
63
|
+
result[var.name] = var.translate_captured_string(captures[index])
|
64
|
+
end
|
65
|
+
trailing = match.post_match
|
66
|
+
trailing = nil if trailing.empty?
|
67
|
+
[result, match.to_s, trailing]
|
68
|
+
end
|
69
|
+
|
70
|
+
class Variable < Doze::URITemplate
|
71
|
+
DEFAULT_REGEXP = "[^\/.,;?]+"
|
72
|
+
|
73
|
+
attr_reader :name
|
74
|
+
|
75
|
+
def initialize(name, regexp=DEFAULT_REGEXP)
|
76
|
+
@name = name; @regexp = regexp
|
77
|
+
end
|
78
|
+
|
79
|
+
def regexp_fragment
|
80
|
+
"(#{@regexp})"
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
"{#{@name}}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def variables; [self]; end
|
88
|
+
|
89
|
+
def expand(vars)
|
90
|
+
Doze::Utils.escape(vars[@name].to_s)
|
91
|
+
end
|
92
|
+
|
93
|
+
def partially_expand(vars)
|
94
|
+
if vars.has_key?(@name)
|
95
|
+
String.new(expand(vars))
|
96
|
+
else
|
97
|
+
self
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def translate_captured_string(string)
|
102
|
+
# inlines Doze::Utils.unescape, but with gsub! rather than gsub since this is faster and the matched string is throwaway
|
103
|
+
string.gsub!(/((?:%[0-9a-fA-F]{2})+)/n) {[$1.delete('%')].pack('H*')}; string
|
104
|
+
end
|
105
|
+
|
106
|
+
# String#size under Ruby 1.8 and String#bytesize under 1.9.
|
107
|
+
BYTESIZE_METHOD = ''.respond_to?(:bytesize) ? 'bytesize' : 'size'
|
108
|
+
|
109
|
+
# inlines Doze::Utils.escape (optimised from Rack::Utils.escape) with further effort to avoid an extra method call for bytesize 1.9 compat.
|
110
|
+
def expand_code_fragment
|
111
|
+
"\#{vars[#{@name.inspect}].to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) {'%'+$1.unpack('H2'*$1.#{BYTESIZE_METHOD}).join('%').upcase}}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class QuadHexBytesVariable < Variable
|
116
|
+
REGEXP = "(?:/[0-9a-f]{2}){4}"
|
117
|
+
|
118
|
+
def initialize(name)
|
119
|
+
super(name, REGEXP)
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_s
|
123
|
+
"{/#{@name}.quadhexbytes*}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def expand(vars)
|
127
|
+
hex = vars[@name].to_i.to_s(16).rjust(8,'0')
|
128
|
+
"/#{hex[0..1]}/#{hex[2..3]}/#{hex[4..5]}/#{hex[6..7]}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def expand_code_fragment
|
132
|
+
"/\#{hex=vars[#{@name.inspect}].to_i.to_s(16).rjust(8,'0');hex[0..1]}/\#{hex[2..3]}/\#{hex[4..5]}/\#{hex[6..7]}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def translate_captured_string(string)
|
136
|
+
string.tr('/','').to_i(16)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class String < Doze::URITemplate
|
141
|
+
attr_reader :string
|
142
|
+
|
143
|
+
def initialize(string)
|
144
|
+
@string = string
|
145
|
+
end
|
146
|
+
|
147
|
+
def regexp_fragment
|
148
|
+
Regexp.escape(@string)
|
149
|
+
end
|
150
|
+
|
151
|
+
def to_s
|
152
|
+
@string
|
153
|
+
end
|
154
|
+
|
155
|
+
def expand(vars)
|
156
|
+
@string
|
157
|
+
end
|
158
|
+
|
159
|
+
def partially_expand(vars); self; end
|
160
|
+
|
161
|
+
NO_VARS = [].freeze
|
162
|
+
def variables; NO_VARS; end
|
163
|
+
|
164
|
+
def expand_code_fragment
|
165
|
+
@string.inspect[1...-1]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class Composite < Doze::URITemplate
|
170
|
+
def initialize(parts)
|
171
|
+
@parts = parts
|
172
|
+
end
|
173
|
+
|
174
|
+
def regexp_fragment
|
175
|
+
@parts.map {|p| p.regexp_fragment}.join
|
176
|
+
end
|
177
|
+
|
178
|
+
def to_s
|
179
|
+
@parts.join
|
180
|
+
end
|
181
|
+
|
182
|
+
def expand(vars)
|
183
|
+
@parts.map {|p| p.expand(vars)}.join
|
184
|
+
end
|
185
|
+
|
186
|
+
def partially_expand(vars)
|
187
|
+
Composite.new(@parts.map {|p| p.partially_expand(vars)})
|
188
|
+
end
|
189
|
+
|
190
|
+
def variables
|
191
|
+
@variables ||= @parts.map {|p| p.variables}.flatten
|
192
|
+
end
|
193
|
+
|
194
|
+
def expand_code_fragment
|
195
|
+
@parts.map {|p| p.expand_code_fragment}.join
|
196
|
+
end
|
197
|
+
|
198
|
+
attr_reader :parts
|
199
|
+
end
|
200
|
+
|
201
|
+
# A simple case of Composite where a template is prefixed by a string.
|
202
|
+
# This allows the same compiled URI template to be used with many different prefixes
|
203
|
+
# without having to re-compile the expand method for each of them, or use the slower
|
204
|
+
# default implementation
|
205
|
+
class WithPrefix < Composite
|
206
|
+
def initialize(template, prefix)
|
207
|
+
@template = template
|
208
|
+
@prefix = prefix
|
209
|
+
@parts = [String.new(prefix.to_s), *@template.parts]
|
210
|
+
end
|
211
|
+
|
212
|
+
def expand(vars)
|
213
|
+
"#{@prefix}#{@template.expand(vars)}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def with_prefix(prefix)
|
218
|
+
WithPrefix.new(self, prefix)
|
219
|
+
end
|
220
|
+
end
|
data/lib/doze/utils.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Various stateless utility functions which aid the conversion back and forth between HTTP syntax and the more abstracted ruby representations we use.
|
2
|
+
module Doze::Utils
|
3
|
+
# Strictly this is a WebDAV extension but very useful in the wider HTTP context
|
4
|
+
# see http://tools.ietf.org/html/rfc4918#section-11.2
|
5
|
+
Rack::Utils::HTTP_STATUS_CODES[422] = 'Unprocessable entity'
|
6
|
+
|
7
|
+
Rack::Utils::HTTP_STATUS_CODES.each do |code,text|
|
8
|
+
const_set('STATUS_' << text.upcase.gsub(/[^A-Z]+/, '_'), code)
|
9
|
+
end
|
10
|
+
|
11
|
+
URI_SCHEMES = Hash.new(URI::Generic).merge!(
|
12
|
+
'http' => URI::HTTP,
|
13
|
+
'https' => URI::HTTPS
|
14
|
+
)
|
15
|
+
|
16
|
+
def request_base_uri(request)
|
17
|
+
URI_SCHEMES[request.scheme].build(
|
18
|
+
:scheme => request.scheme,
|
19
|
+
:port => request.port,
|
20
|
+
:host => request.host
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def quote(str)
|
25
|
+
'"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
|
26
|
+
end
|
27
|
+
|
28
|
+
# Note: unescape and escape proved bottlenecks in URI template matching and URI template generation which in turn
|
29
|
+
# were bottlenecks for serving some simple requests and for generating URIs to use for cache lookups.
|
30
|
+
# So perhaps a bit micro-optimised here, but there was a reason :)
|
31
|
+
|
32
|
+
# Rack::Utils.unescape, but without turning '+' into ' '
|
33
|
+
# Also must be passed a string.
|
34
|
+
def unescape(s)
|
35
|
+
s.gsub(/((?:%[0-9a-fA-F]{2})+)/n) {[$1.delete('%')].pack('H*')}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Rack::Utils.escape, but turning ' ' into '%20' rather than '+' (which is not a necessary part of the URI spec) to save an extra call to tr.
|
39
|
+
# Also must be passed a string.
|
40
|
+
# Also avoids an extra call to 1.8/1.9 compatibility wrapper for bytesize/size.
|
41
|
+
if ''.respond_to?(:bytesize)
|
42
|
+
def escape(s)
|
43
|
+
s.gsub(/([^a-zA-Z0-9_.-]+)/n) {'%'+$1.unpack('H2'*$1.bytesize).join('%').upcase}
|
44
|
+
end
|
45
|
+
else
|
46
|
+
def escape(s)
|
47
|
+
s.gsub(/([^a-zA-Z0-9_.-]+)/n) {'%'+$1.unpack('H2'*$1.size).join('%').upcase}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# So utility functions are accessible as Doze::Utils.foo as well as via including the module
|
52
|
+
extend self
|
53
|
+
end
|
data/lib/doze/version.rb
ADDED
data/lib/doze.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'functional/base'
|
2
|
+
|
3
|
+
class AuthTest < Test::Unit::TestCase
|
4
|
+
include Doze::Utils
|
5
|
+
include Doze::TestCase
|
6
|
+
|
7
|
+
def test_deny_unauthenticated_user
|
8
|
+
root.expects(:authorize).with(nil, :get).returns(false).once
|
9
|
+
assert_equal STATUS_UNAUTHORIZED, get.status
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_deny_authenticated_user
|
13
|
+
root.expects(:authorize).with('username', :get).returns(false).once
|
14
|
+
get('REMOTE_USER' => 'username')
|
15
|
+
assert_equal STATUS_FORBIDDEN, last_response.status
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_allow_authenticated_user
|
19
|
+
root.expects(:authorize).with('username', :get).returns(true).once
|
20
|
+
get('REMOTE_USER' => 'username')
|
21
|
+
assert_equal STATUS_OK, last_response.status
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_post_auth
|
25
|
+
root.expects(:supports_post?).returns(true)
|
26
|
+
root.expects(:authorize).with('username', :post).returns(false).once
|
27
|
+
root.expects(:post).never
|
28
|
+
post('REMOTE_USER' => 'username')
|
29
|
+
assert_equal STATUS_FORBIDDEN, last_response.status
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_put_auth
|
33
|
+
root.expects(:supports_put?).returns(true)
|
34
|
+
root.expects(:authorize).with('username', :put).returns(false).once
|
35
|
+
root.expects(:put).never
|
36
|
+
put('REMOTE_USER' => 'username')
|
37
|
+
assert_equal STATUS_FORBIDDEN, last_response.status
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_delete_auth
|
41
|
+
root.expects(:supports_delete?).returns(true)
|
42
|
+
root.expects(:authorize).with('username', :delete).returns(false).once
|
43
|
+
root.expects(:delete).never
|
44
|
+
delete('REMOTE_USER' => 'username')
|
45
|
+
assert_equal STATUS_FORBIDDEN, last_response.status
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_other_method_auth
|
49
|
+
app(:recognized_methods => [:get, :patch])
|
50
|
+
root.expects(:supports_patch?).returns(true)
|
51
|
+
root.expects(:authorize).with('username', :patch).returns(false).once
|
52
|
+
root.expects(:patch).never
|
53
|
+
other_request_method('PATCH', {'REMOTE_USER' => 'username'})
|
54
|
+
assert_equal STATUS_FORBIDDEN, last_response.status
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_can_raise_unauthd_errors
|
58
|
+
root.expects(:get).raises(Doze::UnauthorizedError.new('no homers allowed'))
|
59
|
+
assert_equal STATUS_UNAUTHORIZED, get.status
|
60
|
+
assert_match /Unauthorized\: no homers allowed/, last_response.body
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_can_raise_forbidden_errors
|
64
|
+
root.expects(:get).raises(Doze::ForbiddenError.new('do not go there, girlfriend'))
|
65
|
+
assert_equal STATUS_FORBIDDEN, get.status
|
66
|
+
assert_match /Forbidden\: do not go there, girlfriend/, last_response.body
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|