icaprb-server 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +11 -0
- data/README.md +50 -0
- data/README.rdoc +34 -0
- data/Rakefile +17 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/start_server.rb +36 -0
- data/icaprb-server.gemspec +23 -0
- data/lib/icaprb/icapuri.rb +24 -0
- data/lib/icaprb/server/constants.rb +83 -0
- data/lib/icaprb/server/data_structures.rb +182 -0
- data/lib/icaprb/server/request_parser.rb +295 -0
- data/lib/icaprb/server/response.rb +168 -0
- data/lib/icaprb/server/services.rb +241 -0
- data/lib/icaprb/server/version.rb +6 -0
- data/lib/icaprb/server.rb +211 -0
- metadata +107 -0
@@ -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
|
+
|