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,241 @@
1
+ require 'timeout'
2
+ module ICAPrb
3
+ module Server
4
+ # this module contains the code, which is required to build an ICAP service
5
+ module Services
6
+ # Base class for ICAP services
7
+ class ServiceBase
8
+ # the name of the service which is used in the response header (Service-Name)
9
+ attr_accessor :service_name
10
+ # send the ttl via options header - the options are valid for the given time
11
+ attr_accessor :options_ttl
12
+ # the supported methods for this service. This must be an +Array+ which contains symbols.
13
+ # The values are:
14
+ #
15
+ # :request_mod:: request mod is supported
16
+ # :response_mod:: response mod is supported
17
+ #
18
+ # Do not add :options - this would be wrong here!
19
+ attr_accessor :supported_methods
20
+ # The preview size has to be set if the server supports previews. Otherwise use +nil+ here.
21
+ # If your service supports previews
22
+ attr_accessor :preview_size
23
+ # +Array+ of file extensions which should be always sent to the +ICAP+ server (no preview)
24
+ attr_accessor :transfer_complete
25
+ # +Array+ of file extensions which should not be sent to the +ICAP+ server (not even a preview)
26
+ attr_accessor :transfer_ignore
27
+ # +Array+ of file extensions which need a preview sent to the +ICAP+ server
28
+ # (do not send the full file in advance)
29
+ attr_accessor :transfer_preview
30
+ # Maximum amount of concurrent connections per IP. The server will not accept more connections and answers with
31
+ # an error
32
+ attr_accessor :max_connections
33
+ # The IS-Tag is a required header. If you change it, the cache of the proxy will be flushed.
34
+ # You usually do not need to change this header. You may want to add your service name here.
35
+ attr_accessor :is_tag
36
+ # If you want to send a service id header to the +ICAP+ client, you can set it here. Use nil to disable
37
+ # this header.
38
+ attr_accessor :service_id
39
+ # the counter is used to determine if too many connections are opened by the proxy.
40
+ # If this is the case, the the server answers with an error
41
+ attr_reader :counter
42
+ # timeout for the service
43
+ attr_accessor :timeout
44
+
45
+ # initialize a new service
46
+ def initialize(service_name,supported_methods = [], preview_size = nil, options_ttl = 60,
47
+ transfer_preview = nil, transfer_ignore = nil, transfer_complete = nil, max_connections = 100000) #TODO Work in progress; sort
48
+ @service_name = service_name
49
+ @options_ttl = options_ttl
50
+ @supported_methods = supported_methods
51
+ @preview_size = preview_size
52
+
53
+ @transfer_preview = transfer_preview
54
+ @transfer_ignore = transfer_ignore
55
+ @transfer_complete = transfer_complete
56
+ @max_connections = max_connections
57
+ @is_tag = nil
58
+ @service_id = nil
59
+ @timeout = nil
60
+
61
+ @counter = {}
62
+ end
63
+
64
+ # parameters:
65
+ # server:: reference to the icap server
66
+ # ip:: ip address of the peer
67
+ # socket:: socket to communicate
68
+ # data:: the parsed request
69
+ def process_request(_,_,_,_)
70
+ raise :not_implemented
71
+ end
72
+
73
+ # returns if this service supports previews which means it can request the rest of the data if they are
74
+ # required. If you do not override this method, this will return false so you will get the complete request.
75
+ def supports_preview?
76
+ return false if @preview_size.nil?
77
+ return preview_size >= 0
78
+ end
79
+
80
+ # include the ChunkedEncodingHelper for previews
81
+ include ::ICAPrb::Server::Parser::ChunkedEncodingHelper
82
+
83
+ # returns true if we already got all data or if we are in a preview.
84
+ # if we are not in a preview, the preview header is not present => outside of a preview
85
+ # and if the ieof is set, there is no data left - we have all data
86
+ # everything else means there is data left to request.
87
+ # NOTE: this will only work once! Do not request data after calling this method and call it again -
88
+ # you will get a false negative.
89
+ def got_all_data?(data)
90
+ return true unless data[:icap_data][:header]['Preview']
91
+ return true if data[:http_response_body].ieof
92
+ return false
93
+ end
94
+
95
+ # When we get a preview, we can answer it or request the rest of the data.
96
+ # This method will send the status "100 Continue" to request the rest of the data and
97
+ # it will then request all the data which is left and returns this data as a single string.
98
+ #
99
+ # You may want to concatenate it with the data you already got in the preview using the << operator.
100
+ #
101
+ # WARNING: DO NOT CALL THIS METHOD IF YOU ARE NOT IN A PREVIEW!
102
+ def get_the_rest_of_the_data(io)
103
+ data = ''
104
+ Response.continue(io)
105
+ until (line,_ = read_chunk(io); line) && line == :eof
106
+ data += line
107
+ end
108
+ return data
109
+ end
110
+
111
+ # this method is called by the server when it receives a new ICAP request
112
+ # it will increase the counter by one, call process_request and decreases the counter by one.
113
+ def do_process(server,ip,io,data)
114
+ begin
115
+ enter(ip)
116
+ rescue
117
+ Response.display_error_page(io,503,{'title' => 'ICAP Error',
118
+ 'content' => 'Sorry, too much work for me',
119
+ :http_version => '1.1',
120
+ :http_status => 500})
121
+ return
122
+ end
123
+
124
+ begin
125
+ unless @supported_methods.include? data[:icap_data][:request_line][:icap_method]
126
+ Response.display_error_page(io,501,{'title' => 'Method not implemented',
127
+ 'content' => 'I do not know what to do with that...',
128
+ :http_version => '1.1',
129
+ :http_status => 500})
130
+ return
131
+ end
132
+ if @timeout
133
+ begin
134
+ Timeout::timeout(@timeout) do
135
+ process_request(server,ip,io,data)
136
+ end
137
+ rescue Timeout::Error => e
138
+ # do not do a graceful shutdown of the connection as the client may fail
139
+ server.logger.error e
140
+ io.close
141
+ end
142
+ else
143
+ process_request(server,ip,io,data)
144
+ end
145
+ rescue
146
+ leave(ip)
147
+ raise
148
+ end
149
+ leave(ip)
150
+ end
151
+
152
+ # when the connection enters this method will increase the counter. If the counter exceeds the limit,
153
+ # the request will be rejected
154
+ def enter(ip)
155
+ if @counter[ip]
156
+ raise :connection_limit_exceeded unless (@counter[ip] < @max_connections) || @max_connections.nil?
157
+ @counter[ip] += 1
158
+ else
159
+ @counter[ip] = 1
160
+ end
161
+ end
162
+
163
+ # when the request is answered we can allow the next one by decrementing the counter
164
+ def leave(ip)
165
+ @counter[ip] -= 1
166
+ end
167
+
168
+ # This method is called by the server when the client sends an options request which is not a
169
+ # mandatory upgrade.
170
+ #
171
+ # The data used here is set by the constructor and it should be configured when the Service
172
+ # is initialized.
173
+ #
174
+ # Parameters:
175
+ # +io+ the socket used to answer the request
176
+ def generate_options_response(io)
177
+ response = ::ICAPrb::Server::Response.new
178
+ response.components << ::ICAPrb::Server::NullBody.new
179
+ methods = []
180
+ methods << 'REQMOD' if @supported_methods.include? :request_mod
181
+ methods << 'RESPMOD' if @supported_methods.include? :response_mod
182
+ response.icap_header['Methods'] = methods.join(', ')
183
+ set_generic_icap_headers(response.icap_header)
184
+ response.icap_header['Max-Connections'] = @max_connections if @max_connections
185
+ response.icap_header['Options-TTL'] = @options_ttl if @options_ttl
186
+ response.icap_header['Preview'] = @preview_size if @preview_size
187
+ response.icap_header['Transfer-Ignore'] = @transfer_ignore.join(', ') if @transfer_ignore
188
+ response.icap_header['Transfer-Complete'] = @transfer_complete.join(', ') if @transfer_complete
189
+ response.icap_header['Transfer-Preview'] = @transfer_preview.join(', ') if @transfer_preview
190
+ response.icap_header['Allow'] = '204'
191
+ response.write_headers_to_socket io
192
+ end
193
+
194
+ # set headers independently from the response type
195
+ #
196
+ # parameters:
197
+ # +icap_header+:: The hash which holds the ICAP headers.
198
+ def set_generic_icap_headers(icap_header)
199
+ icap_header['Service-Name'] = @service_name
200
+ icap_header['ISTag'] = @is_tag if @is_tag
201
+ icap_header['Service-ID'] = @service_id if @service_id
202
+ end
203
+ end
204
+
205
+ # Sample Service to test the server
206
+ # it will echo the complete request to the client
207
+ class EchoService < ServiceBase
208
+ # initializes the EchoService - the name of the echo service is echo
209
+ def initialize
210
+ super('echo',[:request_mod, :response_mod],1024,60,nil,nil,nil,1000)
211
+ @timeout = nil
212
+ end
213
+
214
+ # return the request to the client
215
+ def process_request(icap_server,ip,socket,data)
216
+ logger = icap_server.logger
217
+ logger.debug 'Start processing data via echo service...'
218
+ response = ::ICAPrb::Server::Response.new
219
+ response.icap_status_code = 200
220
+ if data[:icap_data][:request_line][:icap_method] == :response_mod
221
+ http_resp_header = data[:http_response_header]
222
+ http_resp_body = data[:http_response_body]
223
+ else
224
+ http_resp_header = data[:http_request_header]
225
+ http_resp_body = data[:http_request_body]
226
+ end
227
+
228
+ http_resp_body << get_the_rest_of_the_data(socket) if http_resp_body && !(got_all_data? data)
229
+ response.components << http_resp_header
230
+ response.components << http_resp_body
231
+ response.write_headers_to_socket socket
232
+ if http_resp_body.instance_of? ResponseBody
233
+ socket.write(http_resp_body.to_chunk)
234
+ ::ICAPrb::Server::Response.send_last_chunk(socket,false)
235
+ end
236
+ logger.debug 'Answered request in echo service'
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,6 @@
1
+ module ICAPrb
2
+ module Server
3
+ # current version number of this library
4
+ VERSION = '0.0.1'
5
+ end
6
+ end
@@ -0,0 +1,211 @@
1
+ require 'icaprb/server/version'
2
+ require 'openssl'
3
+ require 'socket'
4
+ require 'logger'
5
+
6
+ require_relative './server/request_parser'
7
+ require_relative './server/response'
8
+ require_relative './server/services'
9
+ # nodoc
10
+ module ICAPrb
11
+ # The server code of our project.
12
+ module Server
13
+ # This class contains the network related stuff like waiting for connections.
14
+ # It is the main class of this project.
15
+ class ICAPServer
16
+ # supported ICAP versions
17
+ SUPPORTED_ICAP_VERSIONS = ['1.0']
18
+ # logger for the server; default level is Logger::WARN and it writes to STDOUT
19
+ attr_accessor :logger
20
+ # services registered on the server
21
+ attr_accessor :services
22
+
23
+ # Create a new ICAP server
24
+ #
25
+ # * <b>host</b> the host on which the socket should be bound to
26
+ # * <b>port</b> the port on which the socket should be bound to - this is usually 1344
27
+ # * <b>options</b> when you want to use TLS, you can pass a Hash containing the following information
28
+ # :secure:: true if TLS should be used
29
+ # :certificate:: the path of the certificate
30
+ # :key:: the path of the key file
31
+ def initialize(host = 'localhost', port = 1344, options = nil)
32
+ @host, @port = host,port
33
+ @secure = false
34
+ @certificate = nil
35
+ @key = nil
36
+ if (options.is_a? Hash) && (@secure = options[:secure])
37
+ @key = options[:key]
38
+ @certificate = options[:certificate]
39
+ end
40
+
41
+ if (options.is_a? Hash) && options[:logfile]
42
+ @logger = Logger.new(options[:logfile])
43
+ else
44
+ @logger = Logger.new(STDOUT)
45
+ end
46
+
47
+ if (options.is_a? Hash) && options[:log_level]
48
+ @logger.level = options[:log_level]
49
+ else
50
+ @logger.level = Logger::WARN
51
+ end
52
+
53
+ @services = {}
54
+
55
+ @enable_tls_1_1 = options[:enable_tls_1_1] unless options.nil?
56
+
57
+ @tls_socket = false
58
+ if (options.is_a? Hash) && options[:tls_socket]
59
+ @tls_socket = options[:tls_socket]
60
+ end
61
+ end
62
+
63
+ # this methods starts the server and passes the connection to the method handle_request
64
+ # as well as the ip and the port.
65
+ # It will log the information about the connection if the level is set to info or lower.
66
+ #
67
+ # this method will most likely never crash. It is blocking so you may want to run it in
68
+ # its own thread.
69
+ def run
70
+ # run the server
71
+ server = create_server
72
+ loop do
73
+
74
+ Thread.start(server.accept) do |connection|
75
+
76
+ if connection.is_a? OpenSSL::SSL::SSLSocket
77
+ port, ip = Socket.unpack_sockaddr_in(connection.io.getpeername)
78
+ else
79
+ port, ip = Socket.unpack_sockaddr_in(connection.getpeername)
80
+ end
81
+ @logger.info "[CONNECT] Client from #{ip}:#{port} connected to this server"
82
+ begin
83
+ until connection.closed? do
84
+ handle_request(connection,ip)
85
+ end
86
+ rescue Errno::ECONNRESET => e
87
+ @logger.error "[CONNECTION ERROR] Client #{ip}:#{port} got disconnected (CONNECTION RESET BY PEER): #{e}"
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+
94
+ # this method handles the connection to the client. It will call the parser and sends the request to the service.
95
+ # The service must return anything and handle the request. The important classes are in response.rb
96
+ # This method includes a lot of error handling. It will respond with an error page if
97
+ # * The ICAP version is not supported
98
+ # * It cannot read the header
99
+ # * The method is not supported by the service
100
+ # * The request has an upgrade header, which is not supported
101
+ # * the client requested an upgrade to tls, but the server has not been configured to use it
102
+ # * the client requested a service, which does not exist
103
+ def handle_request(connection, ip)
104
+ # handles the request
105
+ begin
106
+ parser = RequestParser.new(connection, ip, self)
107
+ parsed_data = parser.parse
108
+ rescue Exception => e
109
+ #puts $@
110
+ logger.error "[PARSER ERROR] Error while parsing request - Error Message is: #{e}"
111
+ Response.display_error_page(connection,400,
112
+ {http_version: '1.0',http_status: 400, 'title' => 'Invalid Request',
113
+ 'content' => 'Your client sent a malformed request - please fix it and try it again.'})
114
+ return
115
+ end
116
+
117
+ unless SUPPORTED_ICAP_VERSIONS.include? parsed_data[:icap_data][:request_line][:version]
118
+ Response.display_error_page(connection,505,
119
+ {http_version: '1.0',
120
+ http_status: 500,
121
+ 'title' => 'Unknown ICAP-version used',
122
+ 'content' => 'We are sorry but your ICAP version is not known by this server.'})
123
+ end
124
+
125
+ # send the data to the service framework
126
+ path = parsed_data[:icap_data][:request_line][:uri].path
127
+ path = path[1...path.length] if path != '*'
128
+ if (service = @services[path])
129
+ icap_method = parsed_data[:icap_data][:request_line][:icap_method]
130
+ if icap_method == :options
131
+ return service.generate_options_response(connection)
132
+ else
133
+ if service.supported_methods.include? icap_method
134
+ service.do_process(self,ip,connection,parsed_data)
135
+ return
136
+ else
137
+ Response.display_error_page(connection,405,
138
+ {http_version: '1.0',http_status: 500, 'title' => 'ICAP Error',
139
+ 'content' => 'Your client accessed the service with the wrong method.'})
140
+ end
141
+ end
142
+
143
+ elsif (path == '*') && (parsed_data[:icap_data][:request_line][:icap_method] == :options)
144
+ # check for an upgrade header
145
+ icap_data = parsed_data[:icap_data]
146
+ if icap_data[:header]['Connection'] == 'Upgrade' && connection.class == OpenSSL::SSL::SSLSocket
147
+ case icap_data[:header]['Upgrade']
148
+ when /^TLS\/[\d\.]+, ICAP\/[\d\.]+$/
149
+ response = Response.new
150
+ response.icap_status_code = 101
151
+ response.icap_header['Upgrade'] = "TLS/1.2, ICAP/#{icap_data[:request_line][:version]}"
152
+ response.write_headers_to_socket connection
153
+ connection.accept # upgrade connection to use tls
154
+ else
155
+ Response.display_error_page(connection,400,{'title' => 'ICAP Error',
156
+ 'content' => 'Upgrade header is missing',
157
+ :http_version => '1.1',
158
+ :http_status => 500})
159
+ end
160
+ else
161
+ Response.display_error_page(connection,500,{'title' => 'ICAP Error',
162
+ 'content' => 'This server has no TLS support.',
163
+ :http_version => '1.1',
164
+ :http_status => 500})
165
+ end
166
+ return
167
+ else
168
+ Response.display_error_page(connection,404,
169
+ {http_version: '1.0',http_status: 500, 'title' => 'Not Found',
170
+ 'content' => 'Sorry, but the ICAP service does not exist.'})
171
+ return
172
+ end
173
+
174
+ end
175
+
176
+ private
177
+ # this method will create a server based on the information we got on initialisation.
178
+ # It will create an +TCPServer+ with the host and port given at initialisation.
179
+ # If @secure evaluates to true, a +SSLServer+ will be crated and wraps this +TCPServer+.
180
+ # By default, only TLS 1.2 is supported for security reasons but TLS 1.1 can be enabled
181
+ # as well when the option is set at initialization.
182
+ # For security reasons, the encryption algorithms +RC4+ and +DES+ are disabled as well as the
183
+ # digest algorithm +SHA1+.
184
+ # returns: An instance of TCPServer or SSLServer
185
+ def create_server
186
+ tcp_server = TCPServer.new(@host, @port)
187
+ if @secure
188
+ ctx = OpenSSL::SSL::SSLContext.new(:TLSv1_2_server)
189
+ ctx.cert = OpenSSL::X509::Certificate.new(File.read(@certificate))
190
+ ctx.key = OpenSSL::PKey::RSA.new(File.read(@key))
191
+ # secure OpenSSL
192
+ ###############################
193
+ # do not allow ssl v2 or ssl v3
194
+ ctx.options |= (OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 | OpenSSL::SSL::OP_NO_TLSv1)
195
+ # disable TLS 1.1 unless the user requests it
196
+ ctx.options |= OpenSSL::SSL::OP_NO_TLSv1_1 unless @enable_tls_1_1
197
+
198
+ # I do not want to have something encrypted with RC4 or with a DES variant and it should not use the digest
199
+ # algorithm SHA1
200
+ ctx.ciphers =
201
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:ciphers].split(':').select do |cipher_suite|
202
+ !((cipher_suite =~ /RC4|DES/) || (cipher_suite =~ /SHA$/))
203
+ end.join(':')
204
+ tcp_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
205
+ tcp_server.start_immediately = @tls_socket # requires accept call later
206
+ end
207
+ @tcp_server = tcp_server
208
+ end
209
+ end
210
+ end
211
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: icaprb-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Fabian Franz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-06-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description:
56
+ email:
57
+ - fabian.franz@students.fh-hagenberg.at
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".travis.yml"
65
+ - Gemfile
66
+ - LICENSE
67
+ - README.md
68
+ - README.rdoc
69
+ - Rakefile
70
+ - bin/console
71
+ - bin/setup
72
+ - bin/start_server.rb
73
+ - icaprb-server.gemspec
74
+ - lib/icaprb/icapuri.rb
75
+ - lib/icaprb/server.rb
76
+ - lib/icaprb/server/constants.rb
77
+ - lib/icaprb/server/data_structures.rb
78
+ - lib/icaprb/server/request_parser.rb
79
+ - lib/icaprb/server/response.rb
80
+ - lib/icaprb/server/services.rb
81
+ - lib/icaprb/server/version.rb
82
+ homepage: https://github.com/fabianfrz/ICAPrb-Server
83
+ licenses: []
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.5.1
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: This project includes an ICAP server fully implemented in Ruby but it does
105
+ not include services.
106
+ test_files: []
107
+ has_rdoc: