icaprb-server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,295 @@
1
+ require 'uri'
2
+ module ICAPrb
3
+ module Server
4
+ # The Parser module contains methods to parse ICAP or HTTP messages.
5
+ module Parser
6
+
7
+ # This class indicates an error while parsing the request
8
+ class ICAP_Parse_Error < RuntimeError
9
+ end
10
+ # This class indicates an error while parsing the request
11
+ class HTTP_Parse_Error < RuntimeError
12
+ end
13
+
14
+ # This module contains the Methods included in the request parser as well as in the Service.
15
+ # It is used to provide parsing methods which are used to read the full request.
16
+ module ChunkedEncodingHelper
17
+ # this method reads a chunk from an Object, which supports the method "gets" and it is usually a socket.
18
+ # it will return the data sent over the socket and return it. It will also return the information,
19
+ # if the chunk had an ieof flag set. ieof means there is no data available and the server must not ask
20
+ # to continue.
21
+ #
22
+ # params:
23
+ # +io+:: socket
24
+ #
25
+ # returns:
26
+ # [string data, ieof_set]
27
+ private
28
+ def read_chunk(io)
29
+ str = "\r\n"
30
+ until (str = io.gets) != "\r\n"
31
+ end
32
+ total_length, ieof = read_chunk_length(str)
33
+ result = ''
34
+ until result.length >= total_length
35
+ result += io.gets
36
+ end
37
+ if total_length == 0
38
+ return :eof,ieof
39
+ end
40
+ # cut the protocol overhead
41
+ return result[0...total_length], ieof
42
+ end
43
+
44
+ # returns the length of the chunk as first argument and if ieof is set as second
45
+ private
46
+ def read_chunk_length(line)
47
+ x = line.scan(/\A([A-Fa-f0-9]+)(; ieof)?\r\n/).first
48
+ [x[0].to_i(16), !x[1].nil?]
49
+ end
50
+ end
51
+
52
+ # parse header line and returns the parsed line as Array which has the name on index 0 and the value on index 1
53
+ def parse_header(line)
54
+ # remove newlines
55
+ line = line.gsub!("\r",'')
56
+ line = line.gsub!("\n",'')
57
+ line.split(':',2).map {|x| x.strip}
58
+ end
59
+
60
+
61
+ end
62
+ # This class is used to parse ICAP requests. It includes the Parser module.
63
+ class ICAPRequestParser
64
+ include Parser
65
+
66
+ # initializes a new ICAP request parser
67
+ # params:
68
+ # +io+:: the Socket
69
+ def initialize(io)
70
+ @io = io
71
+ end
72
+
73
+ # This method parses the request line and returns an Hash with the components
74
+ # * icap_method:: can be :req_mod, :resp_mod or :options
75
+ # * uri:: an URI
76
+ # * version:: the used version of ICAP
77
+ # @raise ICAP_Parse_Error if something is invalid
78
+ def parse_icap_request_line(line)
79
+ str_method, str_uri,_,_,_, str_version = line.scan(/(REQMOD|RESPMOD|OPTIONS) (((icap[s]?:)?[^\s]+)|(\*)) ICAP\/([\d\.]+)/i).first
80
+ raise ICAP_Parse_Error.new "invalid icap Method in RequestLine #{line}" if str_method.nil?
81
+ case str_method.upcase
82
+ when 'REQMOD'
83
+ icap_method = :request_mod
84
+ when 'RESPMOD'
85
+ icap_method = :response_mod
86
+ when 'OPTIONS'
87
+ icap_method = :options
88
+ else
89
+ raise ICAP_Parse_Error.new 'The request type is not known'
90
+ end
91
+ uri = URI(str_uri)
92
+ unless icap_method && uri && str_version
93
+ raise ICAP_Parse_Error.new 'The request line is not complete.'
94
+ end
95
+ {icap_method: icap_method, uri: uri, version: str_version}
96
+ end
97
+
98
+ # parse all headers
99
+ def parse
100
+ line = "\r\n"
101
+ while line == "\r\n"
102
+ line = @io.gets
103
+ end
104
+ return nil unless line
105
+ icap_req_line = parse_icap_request_line(line)
106
+ icap_headers = {}
107
+ until (line = @io.gets) == "\r\n"
108
+ parsed_header = parse_header(line)
109
+ icap_headers[parsed_header[0]] = parsed_header[1]
110
+ end
111
+ {request_line: icap_req_line, header: icap_headers}
112
+ end
113
+ end
114
+
115
+ # parses HTTP Headers
116
+ class HTTPHeaderParser
117
+ include Parser
118
+
119
+ # initializes a new HTTPHeaderParser
120
+ # params:
121
+ # +io+:: the socket
122
+ # +is_request+:: value to say if it is an response or an request because the request line / status line
123
+ # look different
124
+ def initialize(io,is_request = true)
125
+ @io = io
126
+ @length_read = 0
127
+ @is_request = is_request
128
+ end
129
+ # This method parses the request line and returns an Hash with the components
130
+ # * http_method:: a string
131
+ # * uri:: an URI
132
+ # * version:: the used version of HTTP
133
+ # @raise HTTP_Parse_Error if something is invalid
134
+ def parse_http_request_line(line)
135
+ @length_read += line.length
136
+ str_method, str_uri, str_version = line.scan(/(GET|POST|PUT|DELETE|PATCH|OPTIONS|TRACE|HEAD|CONNECT) (\S+) HTTP\/([\d\.]+)/i).first
137
+ raise HTTP_Parse_Error.new 'invalid http Method' if str_method.nil?
138
+ uri = URI(str_uri)
139
+ unless str_method && uri && str_version
140
+ raise HTTP_Parse_Error.new 'The request line is not complete.'
141
+ end
142
+ {http_method: str_method, uri: uri, version: str_version}
143
+ end
144
+ # This method parses the response line and returns an Hash with the components
145
+ # * status:: an integer
146
+ # * version:: the used version of HTTP
147
+ # @raise HTTP_Parse_Error if something is invalid
148
+ def parse_http_response_line(line)
149
+ @length_read += line.length
150
+ str_version, str_code, _ = line.scan(/HTTP\/([\d\.]+) (\d+) ([A-Za-z0-9 \-]+)\r\n/i).first
151
+ raise HTTP_Parse_Error.new 'invalid Code' if str_code.nil?
152
+ code = str_code.to_i
153
+ unless code && str_version
154
+ raise HTTP_Parse_Error.new 'The request line is not complete.'
155
+ end
156
+ {code: code, version: str_version}
157
+ end
158
+
159
+ # parse all headers
160
+ def parse
161
+ if @is_request
162
+ http_req_line = parse_http_request_line(@io.gets)
163
+ header = RequestHeader.new(http_req_line[:http_method],http_req_line[:uri],http_req_line[:version])
164
+ else
165
+ http_response_line = parse_http_response_line(@io.gets)
166
+ header = ResponseHeader.new(http_response_line[:version],http_response_line[:code])
167
+ end
168
+ until (line = @io.gets) == "\r\n"
169
+ parsed_header = parse_header(line)
170
+ header[parsed_header[0]] = parsed_header[1]
171
+ end
172
+ header
173
+ end
174
+ end
175
+
176
+ # The request parser uses the ICAP and HTTP parsers to parse the complete request.
177
+ # It is the main parser used by the server which gets the socket to read the +ICAP+
178
+ # headers which are sent by the client.
179
+ # Depending on the headers and the values we got, we will decide, which other parsers
180
+ # we need and how to read it. the parsed request will be returned and depending on the
181
+ # data, the server will decide what it will do with the request.
182
+ class RequestParser
183
+
184
+ # create a new instance of a +RequestParser+
185
+ # params:
186
+ # +io+:: a socket to communicate
187
+ # +ip+:: the peer ip address
188
+ # +server+:: the instance of the server which uses the parser to get the service names.
189
+ def initialize(io,ip,server)
190
+ @io = io
191
+ @ip = ip
192
+ @server = server
193
+ end
194
+
195
+ # Parses the complete request and returns the parsed result.
196
+ # It will return the parsed result.
197
+ #
198
+ # if an +Encapsulated+ header is set, it will also parse the encapsulated +HTTP+ header and the body if
199
+ # available
200
+ def parse
201
+ # parse ICAP headers
202
+ icap_parser = ICAPRequestParser.new(@io)
203
+ icap_data = icap_parser.parse
204
+ return nil unless icap_data
205
+ if icap_data[:header]['Encapsulated']
206
+ encapsulation = icap_data[:header]['Encapsulated']
207
+ encapsulated_parts = encapsulation.split(',').map do |part|
208
+ part.split('=').map(&:strip)
209
+ end
210
+ else
211
+ encapsulated_parts = []
212
+ end
213
+ parsed_data = {icap_data: icap_data, encapsulated: encapsulated_parts}
214
+ parts = []
215
+ service_name = icap_data[:request_line][:uri].path
216
+ service_name = service_name[1...service_name.length]
217
+ service = @server.services[service_name]
218
+ if service
219
+ disable_preview = !service.supports_preview?
220
+ preview_size = service.preview_size
221
+ else
222
+ disable_preview = true
223
+ preview_size = nil
224
+ end
225
+ encapsulated_parts.each do |ep|
226
+ parts << case ep[0]
227
+ when 'null-body'
228
+ NullBody.new
229
+ when 'req-hdr'
230
+ http_parser = HTTPHeaderParser.new(@io,true)
231
+ parsed_data[:http_request_header] = http_parser.parse
232
+ when 'res-hdr'
233
+ http_parser = HTTPHeaderParser.new(@io,false)
234
+ parsed_data[:http_response_header] = http_parser.parse
235
+ when 'req-body'
236
+ bp = BodyParser.new(@io, disable_preview, (icap_data[:header]['Preview'] || nil))
237
+ p_data = bp.parse
238
+ parsed_data[:http_request_body] = RequestBody.new(p_data[0],p_data[1])
239
+ when 'res-body'
240
+ bp = BodyParser.new(@io, disable_preview, (icap_data[:header]['Preview'] || nil))
241
+ p_data = bp.parse
242
+ parsed_data[:http_response_body] = ResponseBody.new(p_data[0],p_data[1])
243
+ else
244
+ nil
245
+ end
246
+ end
247
+
248
+ parsed_data
249
+ end
250
+ end
251
+
252
+ # this class will parse an http body which has to be chunked encoded.
253
+ class BodyParser
254
+ # By default, it will try to receive all data if the service does not provide information,
255
+ # if it supports previews.
256
+ # params:
257
+ # +io+:: the Socket
258
+ # +read_everything+:: read all data before forwarding them to the service - true by default
259
+ # (set a preview size in the service to override)
260
+ # +preview_header+:: if a preview header is set, we can find out how long the preview will be.
261
+ # so we know how much data we can expect.
262
+ def initialize(io, read_everything = true, preview_header = nil)
263
+ @io = io
264
+ @read_everything = read_everything
265
+ if preview_header
266
+ @preview_size = preview_header.to_i
267
+ else
268
+ @preview_size = nil
269
+ end
270
+ end
271
+
272
+ # parses all chunks and concatenates then until:
273
+ # * the end of the preview is reached and the service is not correctly configured
274
+ # * die end of the data is reached
275
+ # it will return the data it got and if the ieof has been set.
276
+ def parse
277
+ data = ''
278
+ ieof = false
279
+ until (line,ieof = read_chunk(@io); line) && line == :eof
280
+ data += line
281
+ end
282
+ if !ieof && @read_everything && !@preview_size.nil? && (@preview_size >= data.length)
283
+ Response.continue(@io)
284
+ until (line,ieof2 = read_chunk(@io); line) && line == :eof
285
+ data += line
286
+ ieof ||= ieof2
287
+ end
288
+ end
289
+ return data, ieof
290
+ end
291
+
292
+ include ::ICAPrb::Server::Parser::ChunkedEncodingHelper
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,168 @@
1
+ require_relative './data_structures'
2
+ require_relative './constants'
3
+ require 'date'
4
+ require 'erb'
5
+ module ICAPrb
6
+ module Server
7
+ # The Response class creates a valid ICAP response to send over the socket.
8
+ class Response
9
+ # A +Hash+ containing the header of the ICAP response
10
+ attr_accessor :icap_header
11
+ # The ICAP Version - usually 1.0
12
+ attr_accessor :icap_version
13
+ # The ICAP status code like 200 for OK
14
+ attr_accessor :icap_status_code
15
+ # the parts of the ICAP response
16
+ attr_accessor :components
17
+ # creates a new instance of ICAPrb::Server::Response and initialises some headers
18
+ def initialize
19
+ # the ISTag is used to let the proxy know that the old response (probably cached)
20
+ # is invalid if this value changes
21
+ @icap_header = {'Date' => Time.now.gmtime, 'Server' => 'ICAP.rb', 'Connection' => 'Close', 'ISTag' => '"replace"'}
22
+ @icap_version = '1.0'
23
+ @icap_status_code = 200
24
+ @components = []
25
+ end
26
+
27
+ # creates the status line for the ICAP protocol
28
+ # Returns the status line as a +String+
29
+ def response_line
30
+ "ICAP/#{@icap_version} #{@icap_status_code} #{ICAP_STATUS_CODES[@icap_status_code]}\r\n"
31
+ end
32
+
33
+ # convert an hash of keys (header names) and values to a +String+
34
+ #
35
+ # Params:
36
+ # +value+:: the hash to convert
37
+ #
38
+ # Returns:: the ICAP headers as +String+
39
+ def hash_to_header
40
+ headers = []
41
+ # add Encapsulated Header if we have a body
42
+ encapsulated = encapsulated_header
43
+ value = @icap_header
44
+ value = @icap_header.merge(encapsulated) if encapsulated['Encapsulated'].length > 0
45
+ value.each do |key, value|
46
+ headers << "#{key}: #{value}"
47
+ end
48
+ headers.join("\r\n") + "\r\n\r\n"
49
+ end
50
+
51
+ # creates the encapsulated header from an array of components which it is for
52
+ # Params:
53
+ # +components+:: an array of the components of the ICAP response The components can be an instance of
54
+ # RequestHeader, RequestBody, ResponseHeader or ResponseBody
55
+ #
56
+ # Returns::
57
+ # A Hash containing only one entry with the key 'Encapsulated' which holds the offsets of the components
58
+ def encapsulated_header
59
+ encapsulated_hdr = 'Encapsulated'
60
+ encapsulated_hdr_list = []
61
+ offset = 0
62
+ @components.sort.each do |component|
63
+ case component
64
+ when RequestHeader
65
+ encapsulated_hdr_list << "req-hdr=#{offset}"
66
+ when ResponseHeader
67
+ encapsulated_hdr_list << "res-hdr=#{offset}"
68
+ when RequestBody
69
+ encapsulated_hdr_list << "req-body=#{offset}"
70
+ when ResponseBody
71
+ encapsulated_hdr_list << "res-body=#{offset}"
72
+ when NullBody
73
+ encapsulated_hdr_list << "null-body=#{offset}"
74
+ end
75
+ offset += component.to_s.length
76
+ end
77
+ {encapsulated_hdr => encapsulated_hdr_list.join(', ')}
78
+ end
79
+ # writes the headers into a string and returns them
80
+ # it raises an exception if the response would be incorrectly created (for example multiple headers)
81
+ # it will create the full ICAP + HTTP header (if available)
82
+ def write_headers
83
+ output = response_line
84
+ output += hash_to_header
85
+ s_comp = @components.sort
86
+
87
+ # add request header if it exists
88
+ request_header = s_comp.select {|component| component.class == RequestHeader}
89
+ raise 'The request header can be included only once' if request_header.count > 1
90
+ request_header.each do |rh|
91
+ output += rh.to_s
92
+ end
93
+
94
+ # add response header to the response if it exists
95
+ response_header = s_comp.select {|component| component.class == ResponseHeader}
96
+ raise 'The request header can be included only once' if response_header.count > 1
97
+ response_header.each do |rh|
98
+ output += rh.to_s
99
+ end
100
+ # return the output
101
+ output
102
+ end
103
+
104
+ # send headers to the client.
105
+ #
106
+ # Params:
107
+ # +io+:: Socket where the headers should be sent to
108
+ def write_headers_to_socket(io)
109
+ io.write write_headers
110
+ end
111
+ # basic template for a HTML error page
112
+ ERROR_TEMPLATE = ERB.new('<html><head><meta charset="utf-8" /><title>ICAP.rb<% unless params["title"].nil? %> ::'+
113
+ ' <%= params["title"] %><% end %></title></head>'+
114
+ '<body><h1><%= params["title"] || "Error" %></h1><div>'+
115
+ '<%= params["content"] || "Content missing" %></div></body></html>')
116
+ # display an error page when something is not ok
117
+ # this is a server function because it is also required for errors which cannot be caught by a service
118
+ #
119
+ # Params
120
+ # +io+:: The object, where the response should be written to
121
+ # +status+:: ICAP status code to send
122
+ # +params+:: parameters for the template and the response as well
123
+ def self.display_error_page(io,status, params)
124
+ response = Response.new
125
+ response.icap_status_code = status
126
+ http_resp_header = ResponseHeader.new(params[:http_version],params[:http_status])
127
+ http_resp_header['Content-Type'] = 'text/html; charset=utf-8'
128
+ http_resp_body = ResponseBody.new(ERROR_TEMPLATE.result(binding), false)
129
+ http_resp_header['Content-Length'] = http_resp_body.length
130
+ response.components << http_resp_header
131
+ response.components << http_resp_body
132
+ response.write_headers_to_socket io
133
+ io.write(http_resp_body.to_chunk)
134
+ send_last_chunk(io,false)
135
+ io.close
136
+ end
137
+
138
+ # this method is an alternative to display_error_page. It does not send any http information to the client.
139
+ # instead it will send an ICAP header which will indicate an error.
140
+ def self.error_response(io)
141
+ response = Response.new
142
+ response.icap_status_code = 500
143
+ response.components << NullBody.new
144
+ response.write_headers_to_socket io
145
+ end
146
+
147
+ # sends the information to the client, that it should send the rest of the file. This method does not keep
148
+ # track of your connection and if you call it twice, your client may have trouble with your response. Use
149
+ # it only once and only in +preview+ mode.
150
+ def self.continue(io,icap_version = '1.0')
151
+ io.write "ICAP/#{icap_version} 100 Continue\r\n\r\n"
152
+ end
153
+
154
+ # this method sends the last (empty) chunk and it will add the ieof marker if it is requested.
155
+ # this empty chunk is used to indicate the end of the body encoded in chunked encondig.
156
+ def self.send_last_chunk(io,in_preview = false)
157
+ data = '0'
158
+ if in_preview
159
+ data += '; ieof'
160
+ end
161
+ data += "\r\n\r\n"
162
+ io.write(data)
163
+ end
164
+
165
+ end
166
+ end
167
+ end
168
+