File: scanner.rb

package info (click to toggle)
ruby-http-cookie 1.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 296 kB
  • sloc: ruby: 3,478; makefile: 2
file content (232 lines) | stat: -rw-r--r-- 5,436 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# frozen_string_literal: true
require 'http/cookie'
require 'strscan'
require 'time'

class HTTP::Cookie::Scanner < StringScanner
  # Whitespace.
  RE_WSP = /[ \t]+/

  # A pattern that matches a cookie name or attribute name which may
  # be empty, capturing trailing whitespace.
  RE_NAME = /(?!#{RE_WSP})[^,;\\"=]*/

  RE_BAD_CHAR = /([\x00-\x20\x7F",;\\])/

  # A pattern that matches the comma in a (typically date) value.
  RE_COOKIE_COMMA = /,(?=#{RE_WSP}?#{RE_NAME}=)/

  def initialize(string, logger = nil)
    @logger = logger
    super(string)
  end

  class << self
    def quote(s)
      return s unless s.match(RE_BAD_CHAR)
      (+'"') << s.gsub(/([\\"])/, "\\\\\\1") << '"'
    end
  end

  def skip_wsp
    skip(RE_WSP)
  end

  def scan_dquoted
    (+'').tap { |s|
      case
      when skip(/"/)
        break
      when skip(/\\/)
        s << getch
      when scan(/[^"\\]+/)
        s << matched
      end until eos?
    }
  end

  def scan_name
    scan(RE_NAME).tap { |s|
      s.rstrip! if s
    }
  end

  def scan_value(comma_as_separator = false)
    (+'').tap { |s|
      case
      when scan(/[^,;"]+/)
        s << matched
      when skip(/"/)
        # RFC 6265 2.2
        # A cookie-value may be DQUOTE'd.
        s << scan_dquoted
      when check(/;/)
        break
      when comma_as_separator && check(RE_COOKIE_COMMA)
        break
      else
        s << getch
      end until eos?
      s.rstrip!
    }
  end

  def scan_name_value(comma_as_separator = false)
    name = scan_name
    if skip(/\=/)
      value = scan_value(comma_as_separator)
    else
      scan_value(comma_as_separator)
      value = nil
    end
    [name, value]
  end

  if Time.respond_to?(:strptime)
    def tuple_to_time(day_of_month, month, year, time)
      Time.strptime(
        '%02d %s %04d %02d:%02d:%02d UTC' % [day_of_month, month, year, *time],
        '%d %b %Y %T %Z'
      ).tap { |date|
        date.day == day_of_month or return nil
      }
    end
  else
    def tuple_to_time(day_of_month, month, year, time)
      Time.parse(
        '%02d %s %04d %02d:%02d:%02d UTC' % [day_of_month, month, year, *time]
      ).tap { |date|
        date.day == day_of_month or return nil
      }
    end
  end
  private :tuple_to_time

  def parse_cookie_date(s)
    # RFC 6265 5.1.1
    time = day_of_month = month = year = nil

    s.split(/[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]+/).each { |token|
      case
      when time.nil? && token.match(/\A(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?=\D|\z)/)
        sec =
          if $3
            $3.to_i
          else
            # violation of the RFC
            @logger.warn("Time lacks the second part: #{token}") if @logger
            0
          end
        time = [$1.to_i, $2.to_i, sec]
      when day_of_month.nil? && token.match(/\A(\d{1,2})(?=\D|\z)/)
        day_of_month = $1.to_i
      when month.nil? && token.match(/\A(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i)
        month = $1.capitalize
      when year.nil? && token.match(/\A(\d{2,4})(?=\D|\z)/)
        year = $1.to_i
      end
    }

    if day_of_month.nil? || month.nil? || year.nil? || time.nil?
      return nil
    end

    case day_of_month
    when 1..31
    else
      return nil
    end

    case year
    when 100..1600
      return nil
    when 70..99
      year += 1900
    when 0..69
      year += 2000
    end

    hh, mm, ss = time
    if hh > 23 || mm > 59 || ss > 59
      return nil
    end

    tuple_to_time(day_of_month, month, year, time)
  end

  def scan_set_cookie
    # RFC 6265 4.1.1 & 5.2
    until eos?
      start = pos
      len = nil

      skip_wsp

      name, value = scan_name_value(true)
      if value.nil?
        @logger.warn("Cookie definition lacks a name-value pair.") if @logger
      elsif name.empty?
        @logger.warn("Cookie definition has an empty name.") if @logger
        value = nil
      end
      attrs = {}

      case
      when skip(/,/)
        # The comma is used as separator for concatenating multiple
        # values of a header.
        len = (pos - 1) - start
        break
      when skip(/;/)
        skip_wsp
        aname, avalue = scan_name_value(true)
        next if aname.empty? || value.nil?
        aname.downcase!
        case aname
        when 'expires'
          # RFC 6265 5.2.1
          avalue &&= parse_cookie_date(avalue) or next
        when 'max-age'
          # RFC 6265 5.2.2
          next unless /\A-?\d+\z/.match(avalue)
        when 'domain'
          # RFC 6265 5.2.3
          # An empty value SHOULD be ignored.
          next if avalue.nil? || avalue.empty?
        when 'path'
          # RFC 6265 5.2.4
          # A relative path must be ignored rather than normalizing it
          # to "/".
          next unless /\A\//.match(avalue)
        when 'secure', 'httponly'
          # RFC 6265 5.2.5, 5.2.6
          avalue = true
        end
        attrs[aname] = avalue
      end until eos?

      len ||= pos - start

      if len > HTTP::Cookie::MAX_LENGTH
        @logger.warn("Cookie definition too long: #{name}") if @logger
        next
      end

      yield name, value, attrs if value
    end
  end

  def scan_cookie
    # RFC 6265 4.1.1 & 5.4
    until eos?
      skip_wsp

      # Do not treat comma in a Cookie header value as separator; see CVE-2016-7401
      name, value = scan_name_value(false)

      yield name, value if value

      skip(/;/)
    end
  end
end