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.
@@ -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: