doze 0.0.11
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.
- 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
|