xenon 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +18 -0
  3. data/.gitignore +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +10 -0
  6. data/Guardfile +0 -32
  7. data/README.md +59 -5
  8. data/examples/hello_world/config.ru +3 -0
  9. data/examples/hello_world/hello_world.rb +17 -0
  10. data/lib/xenon.rb +62 -49
  11. data/lib/xenon/auth.rb +63 -0
  12. data/lib/xenon/errors.rb +1 -0
  13. data/lib/xenon/etag.rb +48 -0
  14. data/lib/xenon/headers.rb +2 -4
  15. data/lib/xenon/headers/accept.rb +3 -2
  16. data/lib/xenon/headers/accept_charset.rb +5 -5
  17. data/lib/xenon/headers/accept_encoding.rb +5 -5
  18. data/lib/xenon/headers/accept_language.rb +5 -5
  19. data/lib/xenon/headers/authorization.rb +7 -53
  20. data/lib/xenon/headers/cache_control.rb +3 -3
  21. data/lib/xenon/headers/content_type.rb +1 -1
  22. data/lib/xenon/headers/if_match.rb +53 -0
  23. data/lib/xenon/headers/if_modified_since.rb +22 -0
  24. data/lib/xenon/headers/if_none_match.rb +53 -0
  25. data/lib/xenon/headers/if_range.rb +45 -0
  26. data/lib/xenon/headers/if_unmodified_since.rb +22 -0
  27. data/lib/xenon/headers/user_agent.rb +65 -0
  28. data/lib/xenon/headers/www_authenticate.rb +70 -0
  29. data/lib/xenon/media_type.rb +24 -2
  30. data/lib/xenon/parsers/basic_rules.rb +38 -7
  31. data/lib/xenon/parsers/header_rules.rb +49 -3
  32. data/lib/xenon/parsers/media_type.rb +4 -3
  33. data/lib/xenon/quoted_string.rb +11 -1
  34. data/lib/xenon/routing/directives.rb +14 -0
  35. data/lib/xenon/routing/header_directives.rb +32 -0
  36. data/lib/xenon/routing/method_directives.rb +26 -0
  37. data/lib/xenon/routing/param_directives.rb +22 -0
  38. data/lib/xenon/routing/path_directives.rb +37 -0
  39. data/lib/xenon/routing/route_directives.rb +51 -0
  40. data/lib/xenon/routing/security_directives.rb +20 -0
  41. data/lib/xenon/version.rb +1 -1
  42. data/spec/spec_helper.rb +3 -0
  43. data/spec/xenon/etag_spec.rb +19 -0
  44. data/spec/xenon/headers/if_match_spec.rb +73 -0
  45. data/spec/xenon/headers/if_modified_since_spec.rb +19 -0
  46. data/spec/xenon/headers/if_none_match_spec.rb +79 -0
  47. data/spec/xenon/headers/if_range_spec.rb +45 -0
  48. data/spec/xenon/headers/if_unmodified_since_spec.rb +19 -0
  49. data/spec/xenon/headers/user_agent_spec.rb +67 -0
  50. data/spec/xenon/headers/www_authenticate_spec.rb +43 -0
  51. data/xenon.gemspec +4 -3
  52. metadata +60 -10
  53. data/lib/xenon/routing.rb +0 -133
@@ -1,4 +1,5 @@
1
1
  module Xenon
2
2
  class Error < StandardError; end
3
3
  class ParseError < Error; end
4
+ class ProtocolError < Error; end
4
5
  end
@@ -0,0 +1,48 @@
1
+ require 'xenon/parsers/header_rules'
2
+
3
+ module Xenon
4
+ class ETag
5
+ attr_reader :opaque_tag
6
+
7
+ def initialize(opaque_tag, weak: false)
8
+ @opaque_tag = opaque_tag
9
+ @weak = weak
10
+ end
11
+
12
+ def self.parse(s)
13
+ tree = Parsers::ETag.new.etag.parse(s)
14
+ Parsers::ETagHeaderTransform.new.apply(tree)
15
+ end
16
+
17
+ def weak?
18
+ @weak
19
+ end
20
+
21
+ def strong?
22
+ !weak?
23
+ end
24
+
25
+ def strong_eq?(other)
26
+ strong? && other.strong? && @opaque_tag == other.opaque_tag
27
+ end
28
+
29
+ def weak_eq?(other)
30
+ @opaque_tag == other.opaque_tag
31
+ end
32
+
33
+ def ==(other)
34
+ strong? == other.strong? && @opaque_tag == other.opaque_tag
35
+ end
36
+
37
+ def to_s
38
+ s = weak? ? "W/" : ""
39
+ s << '"' << @opaque_tag << '"'
40
+ end
41
+ end
42
+
43
+ module Parsers
44
+ class ETag < Parslet::Parser
45
+ include ETagHeaderRules
46
+ end
47
+ end
48
+ end
@@ -106,9 +106,7 @@ module Xenon
106
106
  @value
107
107
  end
108
108
  end
109
-
110
- [:Accept, :AcceptCharset, :AcceptEncoding, :CacheControl, :ContentType].each do |sym|
111
- autoload sym, "xenon/headers/#{sym.to_s.underscore}"
112
- end
113
109
  end
114
110
  end
111
+
112
+ Dir[File.join(__dir__, 'headers', '*.rb')].each { |f| require f }
@@ -1,4 +1,5 @@
1
1
  require 'xenon/headers'
2
+ require 'xenon/parsers/header_rules'
2
3
  require 'xenon/parsers/media_type'
3
4
 
4
5
  module Xenon
@@ -20,8 +21,8 @@ module Xenon
20
21
 
21
22
  module Parsers
22
23
  class AcceptHeader < Parslet::Parser
23
- include MediaTypeRules
24
- rule(:accept) { (media_range >> (comma >> media_range).repeat).as(:accept) }
24
+ include HeaderRules, MediaTypeRules
25
+ rule(:accept) { (media_range >> (list_sep >> media_range).repeat).as(:accept) }
25
26
  root(:accept)
26
27
  end
27
28
 
@@ -41,17 +41,17 @@ module Xenon
41
41
 
42
42
  module Parsers
43
43
  class AcceptCharsetHeader < Parslet::Parser
44
- include BasicRules, WeightRules
44
+ include HeaderRules
45
45
  rule(:charset) { token.as(:charset) >> sp? }
46
46
  rule(:wildcard) { str('*').as(:charset) >> sp? }
47
47
  rule(:charset_range) { (charset | wildcard) >> weight.maybe }
48
- rule(:accept_charset) { (charset_range >> (comma >> charset_range).repeat).as(:accept_charset) }
48
+ rule(:accept_charset) { (charset_range >> (list_sep >> charset_range).repeat).as(:accept_charset) }
49
49
  root(:accept_charset)
50
50
  end
51
51
 
52
- class AcceptCharsetHeaderTransform < Parslet::Transform
53
- rule(charset: simple(:c), q: simple(:q)) { CharsetRange.new(c.str, q.str) }
54
- rule(charset: simple(:c)) { CharsetRange.new(c.str) }
52
+ class AcceptCharsetHeaderTransform < HeaderTransform
53
+ rule(charset: simple(:c), q: simple(:q)) { CharsetRange.new(c, q) }
54
+ rule(charset: simple(:c)) { CharsetRange.new(c) }
55
55
  rule(accept_charset: sequence(:cr)) { Headers::AcceptCharset.new(*cr) }
56
56
  rule(accept_charset: simple(:cr)) { Headers::AcceptCharset.new(cr) }
57
57
  end
@@ -41,20 +41,20 @@ module Xenon
41
41
 
42
42
  module Parsers
43
43
  class AcceptEncodingHeader < Parslet::Parser
44
- include BasicRules, WeightRules
44
+ include HeaderRules
45
45
  %w(identity compress x-compress deflate gzip x-gzip).each do |c|
46
46
  rule(c.tr('-', '_').to_sym) { str(c).as(:coding) >> sp? }
47
47
  end
48
48
  rule(:coding) { compress | x_compress | deflate | gzip | x_gzip }
49
49
  rule(:wildcard) { str('*').as(:coding) >> sp? }
50
50
  rule(:coding_range) { (coding | identity | wildcard) >> weight.maybe }
51
- rule(:accept_encoding) { (coding_range >> (comma >> coding_range).repeat).maybe.as(:accept_encoding) }
51
+ rule(:accept_encoding) { (coding_range >> (list_sep >> coding_range).repeat).maybe.as(:accept_encoding) }
52
52
  root(:accept_encoding)
53
53
  end
54
54
 
55
- class AcceptEncodingHeaderTransform < Parslet::Transform
56
- rule(coding: simple(:c), q: simple(:q)) { ContentCodingRange.new(c.str, q.str) }
57
- rule(coding: simple(:c)) { ContentCodingRange.new(c.str) }
55
+ class AcceptEncodingHeaderTransform < HeaderTransform
56
+ rule(coding: simple(:c), q: simple(:q)) { ContentCodingRange.new(c, q) }
57
+ rule(coding: simple(:c)) { ContentCodingRange.new(c) }
58
58
  rule(accept_encoding: sequence(:er)) { Headers::AcceptEncoding.new(*er) }
59
59
  rule(accept_encoding: simple(:cc)) { Headers::AcceptEncoding.new(cc) }
60
60
  rule(accept_encoding: nil) { Headers::AcceptEncoding.new }
@@ -41,17 +41,17 @@ module Xenon
41
41
 
42
42
  module Parsers
43
43
  class AcceptLanguageHeader < Parslet::Parser
44
- include BasicRules, WeightRules
44
+ include HeaderRules
45
45
  rule(:language) { (alpha.repeat(1, 8) >> (str('-') >> alphanum.repeat(1, 8)).maybe).as(:language) >> sp? }
46
46
  rule(:wildcard) { str('*').as(:language) >> sp? }
47
47
  rule(:language_range) { (language | wildcard) >> weight.maybe }
48
- rule(:accept_language) { (language_range >> (comma >> language_range).repeat).as(:accept_language) }
48
+ rule(:accept_language) { (language_range >> (list_sep >> language_range).repeat).as(:accept_language) }
49
49
  root(:accept_language)
50
50
  end
51
51
 
52
- class AcceptLanguageHeaderTransform < Parslet::Transform
53
- rule(language: simple(:e), q: simple(:q)) { LanguageRange.new(e.str, q.str) }
54
- rule(language: simple(:e)) { LanguageRange.new(e.str) }
52
+ class AcceptLanguageHeaderTransform < HeaderTransform
53
+ rule(language: simple(:e), q: simple(:q)) { LanguageRange.new(e, q) }
54
+ rule(language: simple(:e)) { LanguageRange.new(e) }
55
55
  rule(accept_language: sequence(:lr)) { Headers::AcceptLanguage.new(*lr) }
56
56
  rule(accept_language: simple(:lr)) { Headers::AcceptLanguage.new(lr) }
57
57
  end
@@ -1,51 +1,9 @@
1
1
  require 'base64'
2
+ require 'xenon/auth'
2
3
  require 'xenon/headers'
3
4
  require 'xenon/parsers/header_rules'
4
- require 'xenon/quoted_string'
5
5
 
6
6
  module Xenon
7
- class BasicCredentials
8
- attr_reader :username, :password
9
-
10
- def initialize(username, password)
11
- @username = username
12
- @password = password
13
- end
14
-
15
- def token
16
- Base64.strict_encode64("#{@username}:#{@password}")
17
- end
18
-
19
- def self.decode(s)
20
- str = Base64.strict_decode64(s)
21
- username, password = str.split(':', 2)
22
- BasicCredentials.new(username, password)
23
- end
24
-
25
- def to_s
26
- "Basic #{token}"
27
- end
28
- end
29
-
30
- class GenericCredentials
31
- using QuotedString
32
-
33
- attr_reader :scheme, :token, :params
34
-
35
- def initialize(scheme, token: nil, params: {})
36
- @scheme = scheme
37
- @token = token
38
- @params = params
39
- end
40
-
41
- def to_s
42
- s = @scheme.dup
43
- s << ' ' << @token if @token
44
- s << ' ' << @params.map { |n, v| "#{n}=#{v.quote}" }.join(', ')
45
- s
46
- end
47
- end
48
-
49
7
  class Headers
50
8
  # http://tools.ietf.org/html/rfc7235#section-4.2
51
9
  class Authorization < Header 'Authorization'
@@ -58,6 +16,8 @@ module Xenon
58
16
  def self.parse(s)
59
17
  tree = Parsers::AuthorizationHeader.new.parse(s)
60
18
  Parsers::AuthorizationHeaderTransform.new.apply(tree)
19
+ rescue Parslet::ParseFailed
20
+ raise Xenon::ParseError.new("Invalid Authorization header (#{s}).")
61
21
  end
62
22
 
63
23
  def to_s
@@ -68,25 +28,19 @@ module Xenon
68
28
 
69
29
  module Parsers
70
30
  class AuthorizationHeader < Parslet::Parser
71
- include BasicRules
72
- rule(:token68) { ((alpha | digit | match(/[\-\._~\+\/]/)) >> str('=').repeat).repeat(1).as(:token) }
73
- rule(:auth_scheme) { token.as(:auth_scheme) }
74
- rule(:name) { token.as(:name) }
75
- rule(:value) { token.as(:value) }
76
- rule(:auth_param) { (name >> bws >> str('=') >> bws >> (token | quoted_string).as(:value)).as(:auth_param) }
77
- rule(:auth_params) { (auth_param.maybe >> (ows >> comma >> ows >> auth_param).repeat).as(:auth_params) }
31
+ include AuthHeaderRules
78
32
  rule(:credentials) { auth_scheme >> sp >> (token68 | auth_params) }
79
33
  rule(:authorization) { credentials.as(:authorization) }
80
34
  root(:authorization)
81
35
  end
82
36
 
83
- class AuthorizationHeaderTransform < BasicTransform
37
+ class AuthorizationHeaderTransform < HeaderTransform
84
38
  rule(auth_param: { name: simple(:n), value: simple(:v) }) { [n, v] }
85
39
  rule(auth_params: subtree(:x)) { { foo: x } }
86
- rule(auth_scheme: simple(:s), token: simple(:t)) {
40
+ rule(auth_scheme: simple(:s), token: simple(:t)) {
87
41
  case s
88
42
  when 'Basic' then BasicCredentials.decode(t)
89
- else GenericCredentials.new(s, token: t)
43
+ else GenericCredentials.new(s, token: t)
90
44
  end
91
45
  }
92
46
  rule(auth_scheme: simple(:s), auth_params: subtree(:p)) { GenericCredentials.new(s, params: Hash[p]) }
@@ -38,15 +38,15 @@ module Xenon
38
38
 
39
39
  module Parsers
40
40
  class CacheControlHeader < Parslet::Parser
41
- include BasicRules
41
+ include HeaderRules
42
42
  rule(:name) { token.as(:name) }
43
43
  rule(:value) { str('=') >> (token | quoted_string).as(:value) }
44
44
  rule(:directive) { (name >> value.maybe).as(:directive) >> sp? }
45
- rule(:cache_control) { (directive >> (comma >> directive).repeat).as(:cache_control) }
45
+ rule(:cache_control) { (directive >> (list_sep >> directive).repeat).as(:cache_control) }
46
46
  root(:cache_control)
47
47
  end
48
48
 
49
- class CacheControlHeaderTransform < BasicTransform
49
+ class CacheControlHeaderTransform < HeaderTransform
50
50
  rule(directive: { name: simple(:n), value: simple(:v) }) { CacheDirective.new(n, v) }
51
51
  rule(directive: { name: simple(:n) }) { CacheDirective.new(n) }
52
52
  rule(cache_control: sequence(:d)) { Headers::CacheControl.new(*d) }
@@ -12,7 +12,7 @@ module Xenon
12
12
  end
13
13
 
14
14
  def self.parse(s)
15
- ContentType.new(Xenon::ContentType.parse(s))
15
+ new(Xenon::ContentType.parse(s))
16
16
  end
17
17
 
18
18
  def to_s
@@ -0,0 +1,53 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/parsers/header_rules'
3
+ require 'xenon/errors'
4
+ require 'xenon/etag'
5
+
6
+ module Xenon
7
+ class Headers
8
+ # http://tools.ietf.org/html/rfc7232#section-3.1
9
+ class IfMatch < ListHeader 'If-Match'
10
+ def initialize(*etags)
11
+ super(etags)
12
+ end
13
+
14
+ alias_method :etags, :values
15
+
16
+ def self.wildcard
17
+ new
18
+ end
19
+
20
+ def self.parse(s)
21
+ tree = Parsers::IfMatchHeader.new.parse(s)
22
+ Parsers::IfMatchHeaderTransform.new.apply(tree)
23
+ end
24
+
25
+ def wildcard?
26
+ etags.empty?
27
+ end
28
+
29
+ def merge(other)
30
+ raise Xenon::ProtocolError.new('Cannot merge wildcard headers') if wildcard? || other.wildcard?
31
+ super
32
+ end
33
+
34
+ def to_s
35
+ wildcard? ? '*' : super
36
+ end
37
+ end
38
+ end
39
+
40
+ module Parsers
41
+ class IfMatchHeader < Parslet::Parser
42
+ include ETagHeaderRules
43
+ rule(:if_match) { (wildcard | (etag >> (list_sep >> etag).repeat)).as(:if_match) }
44
+ root(:if_match)
45
+ end
46
+
47
+ class IfMatchHeaderTransform < ETagHeaderTransform
48
+ rule(if_match: { wildcard: simple(:w) }) { Headers::IfMatch.new }
49
+ rule(if_match: sequence(:et)) { Headers::IfMatch.new(*et) }
50
+ rule(if_match: simple(:et)) { Headers::IfMatch.new(et) }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,22 @@
1
+ require 'xenon/headers'
2
+
3
+ module Xenon
4
+ class Headers
5
+ # http://tools.ietf.org/html/rfc7232#section-3.3
6
+ class IfModifiedSince < Header 'If-Modified-Since'
7
+ attr_reader :date
8
+
9
+ def initialize(date)
10
+ @date = date
11
+ end
12
+
13
+ def self.parse(s)
14
+ new(Time.httpdate(s))
15
+ end
16
+
17
+ def to_s
18
+ @date.httpdate
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,53 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/parsers/header_rules'
3
+ require 'xenon/errors'
4
+ require 'xenon/etag'
5
+
6
+ module Xenon
7
+ class Headers
8
+ # http://tools.ietf.org/html/rfc7232#section-3.2
9
+ class IfNoneMatch < ListHeader 'If-None-Match'
10
+ def initialize(*etags)
11
+ super(etags)
12
+ end
13
+
14
+ alias_method :etags, :values
15
+
16
+ def self.wildcard
17
+ new
18
+ end
19
+
20
+ def self.parse(s)
21
+ tree = Parsers::IfNoneMatchHeader.new.parse(s)
22
+ Parsers::IfNoneMatchHeaderTransform.new.apply(tree)
23
+ end
24
+
25
+ def wildcard?
26
+ etags.empty?
27
+ end
28
+
29
+ def merge(other)
30
+ raise Xenon::ProtocolError.new('Cannot merge wildcard headers') if wildcard? || other.wildcard?
31
+ super
32
+ end
33
+
34
+ def to_s
35
+ wildcard? ? '*' : super
36
+ end
37
+ end
38
+ end
39
+
40
+ module Parsers
41
+ class IfNoneMatchHeader < Parslet::Parser
42
+ include ETagHeaderRules
43
+ rule(:if_none_match) { (wildcard | (etag >> (list_sep >> etag).repeat)).as(:if_none_match) }
44
+ root(:if_none_match)
45
+ end
46
+
47
+ class IfNoneMatchHeaderTransform < ETagHeaderTransform
48
+ rule(if_none_match: { wildcard: simple(:w) }) { Headers::IfNoneMatch.new }
49
+ rule(if_none_match: sequence(:et)) { Headers::IfNoneMatch.new(*et) }
50
+ rule(if_none_match: simple(:et)) { Headers::IfNoneMatch.new(et) }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ require 'xenon/headers'
2
+ require 'xenon/parsers/header_rules'
3
+ require 'xenon/errors'
4
+ require 'xenon/etag'
5
+
6
+ module Xenon
7
+ class Headers
8
+ # http://tools.ietf.org/html/rfc7233#section-3.2
9
+ class IfRange < Header 'If-Range'
10
+ attr_reader :date, :etag
11
+
12
+ def initialize(value)
13
+ case value
14
+ when Time, DateTime, Date then @date = value
15
+ when ETag then @etag = value
16
+ when String then @etag = ETag.parse(value)
17
+ else raise ArgumentError, 'Value must be a time or an etag.'
18
+ end
19
+
20
+ raise ProtocolError, 'If-Range headers must use strong ETags.' if @etag && @etag.weak?
21
+ end
22
+
23
+ def self.parse(s)
24
+ tree = Parsers::IfRangeHeader.new.parse(s)
25
+ Parsers::IfRangeHeaderTransform.new.apply(tree)
26
+ end
27
+
28
+ def to_s
29
+ @etag ? @etag.to_s : @date.httpdate
30
+ end
31
+ end
32
+ end
33
+
34
+ module Parsers
35
+ class IfRangeHeader < Parslet::Parser
36
+ include ETagHeaderRules
37
+ rule(:if_range) { (etag | http_date).as(:if_range) }
38
+ root(:if_range)
39
+ end
40
+
41
+ class IfRangeHeaderTransform < ETagHeaderTransform
42
+ rule(if_range: simple(:v)) { Headers::IfRange.new(v) }
43
+ end
44
+ end
45
+ end