rack-tctp 0.9.13 → 0.9.14

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