rack-tctp 0.9.13 → 0.9.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
- require 'mkmf'
2
-
3
- $defs.push '-Wno-deprecated-declarations'
4
- $libs += ' -lssl -lcrypto '
5
-
6
- create_makefile('rack/tctp/engine')
1
+ require 'mkmf'
2
+
3
+ $defs.push '-Wno-deprecated-declarations'
4
+ $libs += ' -lssl -lcrypto '
5
+
6
+ create_makefile('rack/tctp/engine')
@@ -1,261 +1,264 @@
1
- require 'radix'
2
- require 'logger'
3
-
4
- require_relative 'tctp/halec'
5
-
6
- module Rack
7
- # This middleware enables Rack to make use of the Trusted Cloud Transfer Protocol (TCTP) for HTTP end-to-end
8
- # body confidentiality and integrity.
9
- class TCTP
10
- DEFAULT_TCTP_DISCOVERY_INFORMATION = '/.*:/halecs'
11
- TCTP_DISCOVERY_MEDIA_TYPE = 'text/prs.tctp-discovery'
12
- TCTP_MEDIA_TYPE = 'binary/prs.tctp'
13
-
14
- # The slug URI can contain any HTTP compatible characters
15
- def self.slug_base
16
- Radix::Base.new(Radix::BASE::B62 + ['-', '_'])
17
- end
18
-
19
- # Generate a new random slug (2^64 possibilities)
20
- def self.new_slug
21
- slug_base.convert(rand(2**64), 10)
22
- end
23
-
24
- # The TCTP sessions
25
- attr_reader :sessions
26
-
27
- # Initializes TCTP middleware
28
- def initialize(app, logger = nil)
29
- unless logger
30
- @logger = ::Logger.new(STDOUT)
31
- @logger.level = ::Logger::FATAL
32
- else
33
- @logger = logger
34
- end
35
-
36
- @app = app
37
- @sessions = {}
38
- end
39
-
40
- # Middleware call. Supports all TCTP use cases:
41
- # * TCTP discovery
42
- # * HALEC creation
43
- # * HALEC handshake
44
- # * Decrypting TCTP secured entity-bodies
45
- # * Encrypting entity-bodies using TCTP
46
- def call(env)
47
- status, headers, body = nil, nil, nil
48
-
49
- begin
50
- req = Rack::Request.new(env)
51
-
52
- case
53
- when is_tctp_discovery?(req)
54
- # TCTP discovery
55
- # TODO Parameterize discovery information
56
- [200, {"Content-Type" => TCTP_DISCOVERY_MEDIA_TYPE, "Content-Length" => DEFAULT_TCTP_DISCOVERY_INFORMATION.length.to_s}, [DEFAULT_TCTP_DISCOVERY_INFORMATION]]
57
- when is_halec_creation?(req)
58
- # HALEC creation
59
- halec = ServerHALEC.new(url: halec_uri(req.env, "/halecs/#{TCTP::new_slug}"))
60
-
61
- # TODO Allow creation using predefined cookie
62
- session = TCTPSession.new
63
-
64
- # Send client_hello to server HALEC and read handshake_response
65
- client_hello = req.body.read
66
- halec.engine.inject client_hello
67
- halec.engine.read
68
- handshake_response = [halec.engine.extract]
69
-
70
- # Set location header and content-length
71
- header = {
72
- 'Location' => halec.url.to_s,
73
- 'Content-Length' => handshake_response[0].length.to_s,
74
- 'Content-Type' => TCTP_MEDIA_TYPE
75
- }
76
-
77
- # Set the TCTP session cookie header
78
- Rack::Utils.set_cookie_header!(header, "tctp_session_cookie", {:value => session.session_id, :path => '/', :expires => Time.now+24*60*60})
79
-
80
- # Persist session and HALEC
81
- session.halecs[halec.url] = halec
82
- sessions[session.session_id] = session
83
-
84
- [201, header, handshake_response]
85
- when is_halec_handshake?(req)
86
- # Get persisted server HALEC
87
- halec = @sessions[req.cookies['tctp_session_cookie']].halecs[halec_uri(req.env, req.path_info)]
88
-
89
- # Write handshake message to server HALEC
90
- halec.engine.inject req.body.read
91
-
92
- # Receive handshake response
93
- halec.engine.read
94
- handshake_response = halec.engine.extract
95
-
96
- # Send back server HALEC response
97
- [200, {
98
- 'Content-Length' => handshake_response.length.to_s,
99
- 'Content-Type' => TCTP_MEDIA_TYPE
100
- }, [handshake_response]]
101
- else
102
- # Decrypt TCTP secured bodies
103
- if is_tctp_encrypted_body?(req) then
104
- decrypted_body = StringIO.new
105
-
106
- halec_url = req.body.gets.chomp
107
-
108
- # Gets the HALEC
109
- halec = @sessions[req.cookies['tctp_session_cookie']].halecs[URI(halec_url)]
110
-
111
- read_body = req.body.read
112
-
113
- begin
114
- decrypted_body.write halec.decrypt_data(read_body)
115
- rescue Exception => e
116
- error(e.message + e.backtrace.join("<br/>\n"))
117
- end
118
-
119
- decrypted_body.rewind
120
-
121
- env['rack.input'] = decrypted_body
122
- end
123
-
124
- status, headers, body = @app.call(env)
125
-
126
- if is_tctp_response_requested?(req) && status >= 200 && ![204, 205, 304].include?(status)
127
- # Gets the first free server HALEC for encryption
128
- # TODO Send error if cookie is missing
129
- session = @sessions[req.cookies['tctp_session_cookie']]
130
-
131
- unless session
132
- return no_usable_halec_error
133
- end
134
-
135
- halec = session.pop_halec
136
-
137
- unless halec
138
- return no_usable_halec_error
139
- end
140
-
141
- # The length of the content body
142
- content_body_length = 0
143
-
144
- # The first line
145
- first_line = halec.url.to_s + "\r\n"
146
- content_body_length += first_line.length
147
-
148
- # Encrypt the body. The first line of the response specifies the used HALEC
149
- encrypted_body = []
150
- encrypted_body << first_line
151
-
152
- # Encrypt each body fragment
153
- body.each do |fragment|
154
- encrypted_fragment = halec.encrypt_data fragment
155
- encrypted_body << encrypted_fragment
156
- content_body_length += encrypted_fragment.length
157
- end
158
-
159
- encrypted_body.define_singleton_method :close do
160
- session.push_halec halec
161
-
162
- super() if self.class.superclass.respond_to? :close
163
- end
164
-
165
- # Finding this bug took waaaay too long ...
166
- body.close if body.respond_to?(:close)
167
-
168
- # Sets the content length and encoding
169
- headers['Content-Length'] = content_body_length.to_s
170
- headers['Content-Encoding'] = 'encrypted'
171
-
172
- [status, headers, encrypted_body]
173
- else
174
- [status, headers, body]
175
- end
176
- end
177
- rescue Exception => e
178
- # TODO Handle SSL Error
179
- @logger.fatal e
180
-
181
- error "Error in TCTP middleware. #{e} #{e.backtrace.inspect}"
182
- end
183
- end
184
-
185
- private
186
- def log_key
187
- 'TCTP Middleware'
188
- end
189
-
190
- def no_usable_halec_error
191
- error 'No useable HALEC for encryption. Please perform Handshake.'
192
- end
193
-
194
- def error(message)
195
- [500, {'Content-Type' => 'text/plain', 'Content-Length' => message.length.to_s}, [message]]
196
- end
197
-
198
- def is_tctp_discovery?(req)
199
- req.options? && !req.env['HTTP_ACCEPT'].nil? && req.env['HTTP_ACCEPT'].eql?(TCTP_DISCOVERY_MEDIA_TYPE)
200
- end
201
-
202
- def is_halec_creation?(req)
203
- req.post? && req.path_info.eql?('/halecs')
204
- end
205
-
206
- def is_halec_handshake?(req)
207
- req.post? &&
208
- !req.cookies.nil? &&
209
- req.cookies.has_key?('tctp_session_cookie') &&
210
- sessions.has_key?(req.cookies['tctp_session_cookie']) &&
211
- sessions[req.cookies['tctp_session_cookie']].halecs.has_key?(halec_uri(req.env, req.path_info))
212
- end
213
-
214
- def is_tctp_response_requested? (req)
215
- req.env['HTTP_ACCEPT_ENCODING'].eql?('encrypted')
216
- end
217
-
218
- def is_tctp_encrypted_body? (req)
219
- req.env['HTTP_CONTENT_ENCODING'].eql?('encrypted')
220
- end
221
-
222
- # Builds an URI object to be used as a HALEC +uri+
223
- # @param [Hash] env A Rack environment hash
224
- # @param [String] path A path
225
- # @return [URI] An HALEC +uri+
226
- def halec_uri(env, path)
227
- URI("#{env['rack.url_scheme']}://#{env['HTTP_HOST']}:#{env['SERVER_PORT']}#{path}")
228
- end
229
- end
230
-
231
- class TCTPSession
232
- attr_reader :session_id
233
-
234
- attr_reader :halecs
235
-
236
- attr_reader :halecs_mutex
237
-
238
- def initialize(session_id = TCTP::new_slug)
239
- @session_id = session_id
240
- @halecs = {}
241
- @halecs_mutex = Mutex.new
242
- end
243
-
244
- def pop_halec
245
- free_halec = nil
246
-
247
- @halecs_mutex.synchronize do
248
- free_halec = @halecs.first {|url, halec| halec.engine.state.eql? 'SSLOK '}
249
-
250
- @halecs.delete free_halec[0] if free_halec
251
- end
252
- return free_halec[1]
253
- end
254
-
255
- def push_halec(halec)
256
- @halecs_mutex.synchronize do
257
- @halecs[halec.url] = halec
258
- end
259
- end
260
- end
1
+ require 'radix'
2
+ require 'logger'
3
+
4
+ require_relative 'tctp/halec'
5
+
6
+ module Rack
7
+ # This middleware enables Rack to make use of the Trusted Cloud Transfer Protocol (TCTP) for HTTP end-to-end
8
+ # body confidentiality and integrity.
9
+ class TCTP
10
+ DEFAULT_TCTP_DISCOVERY_INFORMATION = '/.*:/halecs'
11
+ TCTP_DISCOVERY_MEDIA_TYPE = 'text/prs.tctp-discovery'
12
+ TCTP_MEDIA_TYPE = 'binary/prs.tctp'
13
+
14
+ # The slug URI can contain any HTTP compatible characters
15
+ def self.slug_base
16
+ Radix::Base.new(Radix::BASE::B62 + ['-', '_'])
17
+ end
18
+
19
+ # Generate a new random slug (2^64 possibilities)
20
+ def self.new_slug
21
+ slug_base.convert(rand(2**64), 10)
22
+ end
23
+
24
+ # The TCTP sessions
25
+ attr_reader :sessions
26
+
27
+ # Initializes TCTP middleware
28
+ def initialize(app, logger = nil)
29
+ unless logger
30
+ @logger = ::Logger.new(STDOUT)
31
+ @logger.level = ::Logger::FATAL
32
+ else
33
+ @logger = logger
34
+ end
35
+
36
+ @app = app
37
+ @sessions = {}
38
+ end
39
+
40
+ # Middleware call. Supports all TCTP use cases:
41
+ # * TCTP discovery
42
+ # * HALEC creation
43
+ # * HALEC handshake
44
+ # * Decrypting TCTP secured entity-bodies
45
+ # * Encrypting entity-bodies using TCTP
46
+ def call(env)
47
+ status, headers, body = nil, nil, nil
48
+
49
+ begin
50
+ req = Rack::Request.new(env)
51
+
52
+ case
53
+ when is_tctp_discovery?(req)
54
+ # TCTP discovery
55
+ # TODO Parameterize discovery information
56
+ [200, {"Content-Type" => TCTP_DISCOVERY_MEDIA_TYPE, "Content-Length" => DEFAULT_TCTP_DISCOVERY_INFORMATION.length.to_s}, [DEFAULT_TCTP_DISCOVERY_INFORMATION]]
57
+ when is_halec_creation?(req)
58
+ # HALEC creation
59
+ halec = ServerHALEC.new(url: halec_uri(req.env, "/halecs/#{TCTP::new_slug}"))
60
+
61
+ session = sessions[req.cookies['tctp_session_cookie']] || TCTPSession.new
62
+
63
+ # Send client_hello to server HALEC and read handshake_response
64
+ client_hello = req.body.read
65
+ halec.engine.inject client_hello
66
+ halec.engine.read
67
+ handshake_response = [halec.engine.extract]
68
+
69
+ # Set location header and content-length
70
+ header = {
71
+ 'Location' => halec.url.to_s,
72
+ 'Content-Length' => handshake_response[0].length.to_s,
73
+ 'Content-Type' => TCTP_MEDIA_TYPE
74
+ }
75
+
76
+ # Set the TCTP session cookie header
77
+ Rack::Utils.set_cookie_header!(header, "tctp_session_cookie", {:value => session.session_id, :path => '/', :expires => Time.now+24*60*60})
78
+
79
+ # Persist session and HALEC
80
+ session.push_halec(halec)
81
+ sessions[session.session_id] = session
82
+
83
+ [201, header, handshake_response]
84
+ when is_halec_handshake?(req)
85
+ # Get persisted server HALEC
86
+ halec = @sessions[req.cookies['tctp_session_cookie']].halecs[halec_uri(req.env, req.path_info)]
87
+
88
+ # Write handshake message to server HALEC
89
+ halec.engine.inject req.body.read
90
+
91
+ # Receive handshake response
92
+ halec.engine.read
93
+ handshake_response = halec.engine.extract
94
+
95
+ # Send back server HALEC response
96
+ [200, {
97
+ 'Content-Length' => handshake_response.length.to_s,
98
+ 'Content-Type' => TCTP_MEDIA_TYPE
99
+ }, [handshake_response]]
100
+ else
101
+ # Decrypt TCTP secured bodies
102
+ if is_tctp_encrypted_body?(req) then
103
+ decrypted_body = StringIO.new
104
+
105
+ halec_url = req.body.gets.chomp
106
+
107
+ # Gets the HALEC
108
+ halec = @sessions[req.cookies['tctp_session_cookie']].halecs[URI(halec_url)]
109
+
110
+ read_body = req.body.read
111
+
112
+ begin
113
+ decrypted_body.write halec.decrypt_data(read_body)
114
+ rescue Exception => e
115
+ error(e.message + e.backtrace.join("<br/>\n"))
116
+ end
117
+
118
+ decrypted_body.rewind
119
+
120
+ env['rack.input'] = decrypted_body
121
+ end
122
+
123
+ status, headers, body = @app.call(env)
124
+
125
+ if is_tctp_response_requested?(req) && status >= 200 && ![204, 205, 304].include?(status)
126
+ # Gets the first free server HALEC for encryption
127
+ # TODO Send error if cookie is missing
128
+ session = @sessions[req.cookies['tctp_session_cookie']]
129
+
130
+ unless session
131
+ return no_usable_halec_error
132
+ end
133
+
134
+ halec = session.pop_halec
135
+
136
+ unless halec
137
+ return no_usable_halec_error
138
+ end
139
+
140
+ # The length of the content body
141
+ content_body_length = 0
142
+
143
+ # The first line
144
+ first_line = halec.url.to_s + "\r\n"
145
+ content_body_length += first_line.length
146
+
147
+ # Encrypt the body. The first line of the response specifies the used HALEC
148
+ encrypted_body = []
149
+ encrypted_body << first_line
150
+
151
+ # Encrypt each body fragment
152
+ body.each do |fragment|
153
+ encrypted_fragment = halec.encrypt_data fragment
154
+ encrypted_body << encrypted_fragment
155
+ content_body_length += encrypted_fragment.length
156
+ end
157
+
158
+ encrypted_body.define_singleton_method :close do
159
+ session.push_halec halec
160
+
161
+ super() if self.class.superclass.respond_to? :close
162
+ end
163
+
164
+ # Finding this bug took waaaay too long ...
165
+ body.close if body.respond_to?(:close)
166
+
167
+ # Sets the content length and encoding
168
+ headers['Content-Length'] = content_body_length.to_s
169
+ headers['Content-Encoding'] = 'encrypted'
170
+
171
+ [status, headers, encrypted_body]
172
+ else
173
+ [status, headers, body]
174
+ end
175
+ end
176
+ rescue Exception => e
177
+ # TODO Handle SSL Error
178
+ @logger.fatal e
179
+
180
+ error "Error in TCTP middleware. #{e} #{e.backtrace.inspect}"
181
+ end
182
+ end
183
+
184
+ private
185
+ def log_key
186
+ 'TCTP Middleware'
187
+ end
188
+
189
+ def no_usable_halec_error
190
+ error 'No useable HALEC for encryption. Please perform Handshake.'
191
+ end
192
+
193
+ def error(message)
194
+ [500, {'Content-Type' => 'text/plain', 'Content-Length' => message.length.to_s}, [message]]
195
+ end
196
+
197
+ def is_tctp_discovery?(req)
198
+ req.options? && !req.env['HTTP_ACCEPT'].nil? && req.env['HTTP_ACCEPT'].eql?(TCTP_DISCOVERY_MEDIA_TYPE)
199
+ end
200
+
201
+ def is_halec_creation?(req)
202
+ req.post? && req.path_info.eql?('/halecs')
203
+ end
204
+
205
+ def is_halec_handshake?(req)
206
+ req.post? &&
207
+ !req.cookies.nil? &&
208
+ req.cookies.has_key?('tctp_session_cookie') &&
209
+ sessions.has_key?(req.cookies['tctp_session_cookie']) &&
210
+ sessions[req.cookies['tctp_session_cookie']].halecs.has_key?(halec_uri(req.env, req.path_info))
211
+ end
212
+
213
+ def is_tctp_response_requested? (req)
214
+ req.env['HTTP_ACCEPT_ENCODING'].eql?('encrypted')
215
+ end
216
+
217
+ def is_tctp_encrypted_body? (req)
218
+ req.env['HTTP_CONTENT_ENCODING'].eql?('encrypted')
219
+ end
220
+
221
+ # Builds an URI object to be used as a HALEC +uri+
222
+ # @param [Hash] env A Rack environment hash
223
+ # @param [String] path A path
224
+ # @return [URI] An HALEC +uri+
225
+ def halec_uri(env, path)
226
+ if(env['HTTP_HOST'])
227
+ URI("#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{path}")
228
+ else
229
+ URI("#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}")
230
+ end
231
+ end
232
+ end
233
+
234
+ class TCTPSession
235
+ attr_reader :session_id
236
+
237
+ attr_reader :halecs
238
+
239
+ attr_reader :halecs_mutex
240
+
241
+ def initialize(session_id = TCTP::new_slug)
242
+ @session_id = session_id
243
+ @halecs = {}
244
+ @halecs_mutex = Mutex.new
245
+ end
246
+
247
+ def pop_halec
248
+ free_halec = nil
249
+
250
+ @halecs_mutex.synchronize do
251
+ free_halec = @halecs.first {|url, halec| halec.engine.state.eql? 'SSLOK '}
252
+
253
+ @halecs.delete free_halec[0] if free_halec
254
+ end
255
+ return free_halec[1]
256
+ end
257
+
258
+ def push_halec(halec)
259
+ @halecs_mutex.synchronize do
260
+ @halecs[halec.url] = halec
261
+ end
262
+ end
263
+ end
261
264
  end