zokor 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/double-proxy.rb ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+ require 'uri'
3
+ require 'webrick'
4
+ require 'webrick/httpproxy'
5
+
6
+ def usage
7
+ STDERR.puts <<-EOM
8
+ usage: #{File.basename($0)} PORT [PROXY_URL]
9
+ EOM
10
+ end
11
+
12
+ def serve(port, proxy_url=nil)
13
+ puts 'Starting up double proxy server!'
14
+
15
+ puts "Listening on port #{port.inspect}"
16
+ opts = {Port: port}
17
+
18
+ if proxy_url
19
+ puts "Forwarding through proxy #{proxy_url.inspect}"
20
+ opts[:ProxyURI] = URI.parse(proxy_url)
21
+ end
22
+
23
+ proxy = WEBrick::HTTPProxyServer.new(opts)
24
+
25
+ Signal.trap('INT') { proxy.shutdown }
26
+ Signal.trap('QUIT') { proxy.shutdown }
27
+ Signal.trap('TERM') { proxy.shutdown }
28
+
29
+ proxy.start
30
+ end
31
+
32
+ def main(args)
33
+ begin
34
+ port = args.fetch(0)
35
+ rescue IndexError
36
+ usage
37
+ exit 1
38
+ end
39
+
40
+ begin
41
+ port = Integer(port)
42
+ rescue ArgumentError => err
43
+ usage
44
+ STDERR.puts err
45
+ exit 1
46
+ end
47
+
48
+ proxy_url = args[1]
49
+ if proxy_url && proxy_url.empty?
50
+ proxy_url = nil
51
+ end
52
+
53
+ serve(port, proxy_url)
54
+ end
55
+
56
+ if $0 == __FILE__
57
+ main(ARGV)
58
+ end
data/lib/zokor.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'openssl'
2
+
3
+ require_relative 'zokor/version'
4
+ require_relative 'zokor/logger'
5
+
6
+ require_relative 'zokor/config'
7
+ require_relative 'zokor/proxy_connection'
8
+ require_relative 'zokor/proxy_magic'
9
+
10
+ module Zokor
11
+ unless defined?(self::SafeOpenSSLSettings)
12
+ SafeOpenSSLSettings = true
13
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] |= OpenSSL::SSL::OP_NO_COMPRESSION
14
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] |= OpenSSL::SSL::OP_NO_SSLv2
15
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] |= OpenSSL::SSL::OP_NO_SSLv3
16
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:ciphers] = 'HIGH:!TLSv1:!SSLv3:!aNULL:!eNULL'
17
+ end
18
+ end
@@ -0,0 +1,178 @@
1
+ require 'etc'
2
+ require 'fileutils'
3
+ require 'openssl'
4
+ require 'socket'
5
+ require 'yaml'
6
+
7
+ module Zokor
8
+ class Config
9
+ DEFAULT_CONFIG_DIR = File.join(File.expand_path('~'), '.config', 'zokor')
10
+
11
+ attr_reader :config_dir
12
+
13
+ # @param config_dir [String] (DEFAULT_CONFIG_DIR)
14
+ def initialize(config_dir=nil)
15
+ @config_dir = config_dir || DEFAULT_CONFIG_DIR
16
+ end
17
+
18
+ def load_config(filename=nil)
19
+ filename ||= config_yaml_file
20
+ YAML.load_file(filename)
21
+ end
22
+
23
+ def config_yaml_file
24
+ config_file('zokor.yaml')
25
+ end
26
+
27
+ def config_file(name)
28
+ File.join(config_dir, name)
29
+ end
30
+
31
+ def interactive_install_cert(filename='client.crt')
32
+ path = config_file(filename)
33
+ log.debug('Will install certificate to ' + path.inspect)
34
+ puts 'Please paste your certificate now from -----BEGIN CERTIFICATE-----'
35
+
36
+ cert_data = ''
37
+
38
+ in_cert = false
39
+ while line = STDIN.gets
40
+ next if line.strip.empty?
41
+
42
+ # check for begin marker
43
+ if !in_cert
44
+ if line.strip == '-----BEGIN CERTIFICATE-----'
45
+ in_cert = true
46
+ else
47
+ log.warn "Certificate should start with: -----BEGIN CERTIFICATE-----"
48
+ return false
49
+ end
50
+ end
51
+
52
+ cert_data << line
53
+
54
+ # end
55
+ break if line.strip == '-----END CERTIFICATE-----'
56
+ end
57
+
58
+ File.open(path, File::WRONLY|File::CREAT|File::EXCL, 0644) do |f|
59
+ f.write(cert_data)
60
+ end
61
+
62
+ log.info("Saved certificate to #{path.inspect}")
63
+
64
+ path
65
+ end
66
+
67
+ def interactive_init(opts)
68
+ unless opts[:remote_host]
69
+ log.error('Please pass --ext-host')
70
+ return false
71
+ end
72
+ unless opts[:remote_port]
73
+ log.error('Please pass --ext-port')
74
+ return false
75
+ end
76
+
77
+ init_config(opts.fetch(:remote_host), opts.fetch(:remote_port))
78
+ end
79
+
80
+ def init_config(remote_host, remote_port,
81
+ local_host: '127.0.0.1', local_port: 8080)
82
+ unless Dir.exist?(config_dir)
83
+ log.info("mkdir #{config_dir}")
84
+ FileUtils.mkdir_p(config_dir)
85
+ end
86
+
87
+ path = config_yaml_file
88
+
89
+ if File.exist?(path)
90
+ log.warn('Config file already exists: ' + path)
91
+ return false
92
+ end
93
+
94
+ data = {
95
+ use_ssl: true,
96
+ ssl_opts: {
97
+ ca_file: :builtin,
98
+ cert_file: config_file('client.crt'),
99
+ key_file: config_file('client.key'),
100
+ },
101
+ local_host: local_host,
102
+ local_port: local_port,
103
+ remote_host: remote_host,
104
+ remote_port: remote_port,
105
+ }
106
+
107
+ log.info('Initializing config: ' + YAML.dump(data))
108
+
109
+ File.write(path, YAML.dump(data))
110
+
111
+ create_client_keypair(config_file('client.key'),
112
+ config_file('client.csr'))
113
+ end
114
+
115
+ # @param key_file [String] Key filename
116
+ # @param csr_file [String] CSR filename
117
+ def create_client_keypair(key_file, csr_file)
118
+ log.info('Generating SSL/TLS key and certificate request')
119
+
120
+ key = generate_rsa_key
121
+ File.open(key_file, File::WRONLY|File::CREAT|File::EXCL, 0600) do |f|
122
+ f.write(key.to_s)
123
+ end
124
+
125
+ log.info("Wrote key to #{key_file.inspect}")
126
+
127
+ csr = generate_csr(key, user_address)
128
+ csr.to_s
129
+
130
+ File.write(csr_file, csr.to_s)
131
+
132
+ log.info("Wrote request to #{csr_file.inspect}")
133
+
134
+ log.warn('Certificate request follows:')
135
+
136
+ puts csr.to_s
137
+
138
+ log.warn('Please send the above certificate request.')
139
+
140
+ return true
141
+ end
142
+
143
+ private
144
+
145
+ def generate_rsa_key(bits=2048)
146
+ OpenSSL::PKey::RSA.generate(bits)
147
+ end
148
+
149
+ def generate_csr(key, common_name)
150
+ request = OpenSSL::X509::Request.new
151
+ request.version = 0
152
+
153
+ # don't bother including much of anything in the subject
154
+ request.subject = OpenSSL::X509::Name.new([
155
+ # ['C', options[:country], OpenSSL::ASN1::PRINTABLESTRING],
156
+ # ['ST', options[:state], OpenSSL::ASN1::UTF8STRING],
157
+ # ['L', options[:city], OpenSSL::ASN1::UTF8STRING],
158
+ # ['O', options[:organization], OpenSSL::ASN1::UTF8STRING],
159
+ # ['OU', options[:department], OpenSSL::ASN1::UTF8STRING],
160
+ ['CN', common_name, OpenSSL::ASN1::UTF8STRING],
161
+ # ['emailAddress', options[:email], OpenSSL::ASN1::UTF8STRING]
162
+ ])
163
+
164
+ request.public_key = key.public_key
165
+ request.sign(key, OpenSSL::Digest::SHA256.new)
166
+
167
+ request
168
+ end
169
+
170
+ def user_address
171
+ "#{Etc.getlogin}@#{Socket.gethostname}"
172
+ end
173
+
174
+ def log
175
+ @log ||= Zokor::ProgLogger.new('config')
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,97 @@
1
+ require 'logger'
2
+
3
+ module Zokor
4
+ def self.log_level
5
+ @log_level ||= log_level!
6
+ end
7
+ def self.log_level=(level)
8
+ @log_level = Integer(level)
9
+ end
10
+ def self.log_level!
11
+ level = ENV['LOG_LEVEL']
12
+ return Integer(level) if level && !level.empty?
13
+
14
+ Logger::INFO
15
+ end
16
+
17
+ module TermColors
18
+ NOTHING = '0;0'
19
+ BLACK = '0;30'
20
+ RED = '0;31'
21
+ GREEN = '0;32'
22
+ BROWN = '0;33'
23
+ BLUE = '0;34'
24
+ PURPLE = '0;35'
25
+ CYAN = '0;36'
26
+ LIGHT_GRAY = '0;37'
27
+ DARK_GRAY = '1;30'
28
+ LIGHT_RED = '1;31'
29
+ LIGHT_GREEN = '1;32'
30
+ YELLOW = '1;33'
31
+ LIGHT_BLUE = '1;34'
32
+ LIGHT_PURPLE = '1;35'
33
+ LIGHT_CYAN = '1;36'
34
+ WHITE = '1;37'
35
+ BG_BLACK = '1;40'
36
+ BG_RED = '1;41'
37
+ BG_GREEN = '1;42'
38
+ BG_YELLOW = '1;43'
39
+ BG_BLUE = '1;44'
40
+ BG_PURPLE = '1;45'
41
+ BG_CYAN = '1;46'
42
+ BG_WHITE = '1;47'
43
+ RED_ON_WHITE = '1;31;47'
44
+
45
+ SCHEMA = {
46
+ STDOUT => %w[nothing green brown red purple cyan],
47
+ STDERR => %w[dark_gray nothing yellow light_red bg_red light_cyan],
48
+ }
49
+ end
50
+
51
+ class ColoredLogger < Logger
52
+ def format_message(level, *args)
53
+ if TermColors::SCHEMA[@logdev.dev] && @logdev.dev.tty?
54
+ begin
55
+ index = self.class.const_get(level.sub('ANY', 'UNKNOWN'))
56
+ color_name = TermColors::SCHEMA[@logdev.dev][index]
57
+ color = TermColors.const_get(color_name.to_s.upcase)
58
+ rescue NameError
59
+ color = '0;0'
60
+ end
61
+ message = super(level, *args)
62
+ if message.end_with?("\n")
63
+ # make sure color is turned off before any trailing newline
64
+ "\e[#{color}m#{message[0...-1]}\e[0;0m\n"
65
+ else
66
+ "\e[#{color}m#{message}\e[0;0m"
67
+ end
68
+ else
69
+ super(level, *args)
70
+ end
71
+ end
72
+
73
+ def rainbow(*args)
74
+ SEV_LABEL.each_with_index do |level, i|
75
+ add(i, *args)
76
+ end
77
+ end
78
+ end
79
+
80
+ class ProgLogger < ColoredLogger
81
+ def initialize(name, opts={})
82
+ opts[:stream] ||= STDERR
83
+
84
+ @chunder = !!ENV['LOG_CHUNDER']
85
+
86
+ super(opts.fetch(:stream))
87
+ self.level = Zokor.log_level
88
+ self.progname = name
89
+ end
90
+
91
+ def chunder(*args, &blk)
92
+ return unless @chunder
93
+ debug(*args, &blk)
94
+ end
95
+ end
96
+ end
97
+
@@ -0,0 +1,329 @@
1
+ require 'openssl'
2
+ require 'socket'
3
+
4
+ require 'proxifier'
5
+
6
+ module Zokor
7
+ class ProxyConnection
8
+ BlockSize = 1024 * 4
9
+
10
+ BUILTIN_CA_FILE = File.join(File.dirname(__FILE__), '..', '..',
11
+ 'ca-certs-small.crt')
12
+
13
+ # Create a new connection object to wrap a local client connection and
14
+ # ferry packets through the proxies.
15
+ #
16
+ # @param local_socket [TCPSocket] The local inbound connection.
17
+ # @param remote_host [String]
18
+ # @param remote_port [Integer]
19
+ # @param opts [Hash]
20
+ #
21
+ # @option opts [String] :proxy_url Intermediate proxy URL to connect
22
+ # through.
23
+ # @option opts [Boolean] :use_ssl Whether to use SSL/TLS for the external
24
+ # proxy connection
25
+ # @option opts [Hash] :ssl_opts A hash of SSL options to pass to
26
+ # {ProxyConnection#create_ssl_socket}. Supports some custom options like
27
+ # :key_file and :cert_file (override :key and :cert). Pass
28
+ # :ca_file => :builtin to use the CA bundle that ships with this library.
29
+ #
30
+ def initialize(local_socket, remote_host, remote_port, opts={})
31
+ @local_socket = local_socket
32
+ @remote_host = remote_host
33
+ @remote_port = remote_port
34
+
35
+ @proxy_url = opts[:proxy_url]
36
+ @use_ssl = opts[:use_ssl]
37
+ @ssl_opts = opts.fetch(:ssl_opts, {})
38
+
39
+ # process ssl_opts
40
+ if @ssl_opts[:ca_file] == :builtin || @ssl_opts[:ca_file] == ':builtin'
41
+ log.debug('Using built-in CA file')
42
+ @ssl_opts[:ca_file] = BUILTIN_CA_FILE
43
+ end
44
+
45
+ key_file = @ssl_opts.delete(:key_file)
46
+ if key_file
47
+ # TODO: support other keys besides RSA
48
+ @ssl_opts[:key] = OpenSSL::PKey::RSA.new(File.open(key_file))
49
+ end
50
+
51
+ cert_file = @ssl_opts.delete(:cert_file)
52
+ if cert_file
53
+ @ssl_opts[:cert] = OpenSSL::X509::Certificate.new(File.open(cert_file))
54
+ end
55
+
56
+ log.info('new local connection')
57
+ end
58
+
59
+ # Connect to the proxies and begin ferrying packets. This method will loop
60
+ # indefinitely until the connection is closed by client or server.
61
+ def connect
62
+
63
+ local = @local_socket
64
+
65
+ # open connection to remote server
66
+ remote = create_outbound_tcp_socket
67
+
68
+ # SSL remote main loop
69
+ loop do
70
+ log.debug('IO.select()')
71
+ read_set = [local, remote]
72
+ if remote.is_a?(OpenSSL::SSL::SSLSocket)
73
+ # TODO: determine whether this is needed
74
+ read_set << remote.io
75
+ end
76
+
77
+ rd_ready, _, _ = IO.select(read_set, nil, nil, 2)
78
+
79
+ if rd_ready.nil?
80
+ log.chunder('select TIMEOUT')
81
+ next
82
+ end
83
+
84
+ log.chunder {'read ready: ' + rd_ready.inspect}
85
+
86
+ if rd_ready.include?(local)
87
+ data = local.recv(BlockSize)
88
+ if data.empty?
89
+ log.info('Local end closed connection')
90
+ return
91
+ end
92
+ log.debug("=> #{data.length} bytes to remote")
93
+ socket_write(remote, data)
94
+ log.chunder('writen')
95
+ end
96
+ if rd_ready.include?(remote)
97
+ while true
98
+ data = socket_read(remote, BlockSize)
99
+ if data.empty?
100
+ log.info('Remote end closed connection')
101
+ return
102
+ end
103
+ log.debug("<= #{data.length} bytes from remote")
104
+ local.write(data)
105
+ log.chunder('written')
106
+
107
+ if data.length < BlockSize
108
+ log.chunder("data.length < blocksize, done")
109
+ break
110
+ else
111
+ log.chunder("data.length >= blocksize, continuing")
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ rescue Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EPIPE, EOFError => err
118
+ log.warn(err.inspect)
119
+
120
+ ensure
121
+ local.close if local && !local.closed?
122
+ remote.close if remote && !remote.closed?
123
+
124
+ log.info('Connection closed')
125
+ end
126
+
127
+ def to_s
128
+ "<#{self.class.name} to #{label}>"
129
+ end
130
+
131
+ private
132
+
133
+ # Initiate a TCP connection to our configured remote host.
134
+ #
135
+ # If @proxy_url is set, create a fake connection object that goes
136
+ # through the proxy.
137
+ #
138
+ # If @use_ssl is set, open an SSL socket on this connection.
139
+ #
140
+ # @return [TCPSocket, OpenSSL::SSL::SSLSocket]
141
+ #
142
+ def create_outbound_tcp_socket
143
+ label = "#{@remote_host}:#{@remote_port}"
144
+ if @proxy_url
145
+ log.info("Connecting to #{label} through proxy #{@proxy_url}")
146
+ @proxy = Proxifier::Proxy(@proxy_url)
147
+ tcp_socket = @proxy.open(@remote_host, @remote_port)
148
+ else
149
+ log.info("Connecting to #{label}")
150
+ tcp_socket = TCPSocket.new(@remote_host, @remote_port)
151
+ end
152
+
153
+ if @use_ssl
154
+ create_ssl_socket(tcp_socket, @ssl_opts)
155
+ else
156
+ tcp_socket
157
+ end
158
+ end
159
+
160
+ # @param [TCPSocket] tcp_socket
161
+ # @param [Hash] opts
162
+ #
163
+ # @option opts [String] :ca_file
164
+ # @option opts [String] :ca_path
165
+ # @option opts [OpenSSL::X509::Certificate] :cert
166
+ # @option opts [OpenSSL::PKey::PKey] :key
167
+ #
168
+ # @return [OpenSSL::SSL::SSLSocket]
169
+ def create_ssl_socket(tcp_socket, opts)
170
+ log.info('Beginning SSL handshake')
171
+ ssl_context = OpenSSL::SSL::SSLContext.new()
172
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
173
+
174
+ # by default, use default cert store
175
+ if !opts[:ca_file] && !opts[:ca_path]
176
+ ssl_context.cert_store = default_cert_store
177
+ end
178
+
179
+ ssl_context.set_params(opts)
180
+
181
+ # ssl_context.cert = File.open(opts[:cert]) if opts[:cert]
182
+ # ssl_context.key = File.open(opts[:key]) if opts[:key]
183
+ # ssl_context.ca_file = opts[:ca_file] if opts[:ca_file]
184
+ # ssl_context.ca_path = opts[:ca_path] if opts[:ca_path]
185
+
186
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
187
+ ssl_socket.sync_close = true
188
+
189
+ begin
190
+ ssl_socket.connect
191
+ rescue StandardError => err
192
+ log.warn(err.message)
193
+ raise
194
+ end
195
+
196
+ log.info('Connected!')
197
+
198
+ ssl_socket
199
+ end
200
+
201
+ # Abstract over SSL and TCP socket read().
202
+ #
203
+ # @param [TCPSocket, OpenSSL::SSL::SSLSocket] socket
204
+ # @param [Integer] bytes
205
+ #
206
+ # @return [String] data
207
+ #
208
+ def socket_read(socket, bytes, until_blocked=false)
209
+ case socket
210
+ when TCPSocket
211
+ socket.recv(bytes)
212
+ when OpenSSL::SSL::SSLSocket
213
+ ssl_socket_read(socket, bytes)
214
+ else
215
+ raise ArgumentError.new("Unexpected socket type: #{socket.inspect}")
216
+ end
217
+ end
218
+
219
+ # Abstract over SSL ant TCP socket write().
220
+ #
221
+ # @param [TCPSocket, OpenSSL::SSL::SSLSocket] socket
222
+ # @param [String] data
223
+ #
224
+ # @return [Integer] bytes written
225
+ #
226
+ def socket_write(socket, data)
227
+ case socket
228
+ when TCPSocket
229
+ socket.write(data)
230
+ when OpenSSL::SSL::SSLSocket
231
+ ssl_socket_write(socket, data)
232
+ else
233
+ raise ArgumentError.new("Unexpected socket type: #{socket.inspect}")
234
+ end
235
+ end
236
+
237
+ # Write in a blocking fashion to the given SSLSocket.
238
+ # This handles the appropriate subtleties of waiting for necessary
239
+ # reads/writes with the underlying IO, which makes a simple IO.select and
240
+ # normal blocking write impossible.
241
+ # https://bugs.ruby-lang.org/issues/8875
242
+ #
243
+ # @param [OpenSSL::SSL::SSLSocket] ssl_socket
244
+ # @param [String] data
245
+ #
246
+ # @return [Integer] number of bytes written
247
+ #
248
+ # @see [OpenSSL::Buffering#write_nonblock]
249
+ #
250
+ def ssl_socket_write(ssl_socket, data)
251
+ log.chunder('ssl_socket_write')
252
+
253
+ begin
254
+ return ssl_socket.write_nonblock(data)
255
+ rescue IO::WaitReadable
256
+ log.chunder('WaitReadable') # XXX
257
+ IO.select([ssl_socket.io])
258
+ log.chunder('WaitReadable retry') # XXX
259
+ retry
260
+ rescue IO::WaitWritable
261
+ log.chunder('WaitWritable') # XXX
262
+ IO.select(nil, [ssl_socket.io])
263
+ log.chunder('WaitWritable retry') # XXX
264
+ retry
265
+ end
266
+ ensure
267
+ log.chunder('done ssl_socket_write')
268
+ end
269
+
270
+ # Read in a blocking fashion from the given SSLSocket.
271
+ # This handles the appropriate subtleties of waiting for necessary
272
+ # reads/writes with the underlying IO, which makes a simple IO.select and
273
+ # normal blocking read impossible.
274
+ # https://bugs.ruby-lang.org/issues/8875
275
+ #
276
+ # @param [OpenSSL::SSL::SSLSocket] ssl_socket
277
+ # @param [Integer] bytes Maximum number of bytes to read
278
+ #
279
+ # @return [String] data read
280
+ #
281
+ # @see [OpenSSL::Buffering#write_nonblock]
282
+ #
283
+ def ssl_socket_read(ssl_socket, bytes)
284
+ log.chunder('ssl_socket_read')
285
+
286
+ begin
287
+ return ssl_socket.read_nonblock(bytes)
288
+ rescue IO::WaitReadable
289
+ log.chunder('WaitReadable') # XXX
290
+ IO.select([ssl_socket.io])
291
+ log.chunder('WaitReadable retry') # XXX
292
+ retry
293
+ rescue IO::WaitWritable
294
+ log.chunder('WaitWritable') # XXX
295
+ IO.select(nil, [ssl_socket.io])
296
+ log.chunder('WaitWritable retry') # XXX
297
+ retry
298
+ end
299
+
300
+ ensure
301
+ log.chunder('done ssl_socket_read')
302
+ end
303
+
304
+ # Return an OpenSSL X509 CA certificate store wrapping the system default
305
+ # certificate authorities.
306
+ #
307
+ # TODO: make this work on Windows
308
+ #
309
+ # @return [OpenSSL::X509::Store]
310
+ def default_cert_store
311
+ store = OpenSSL::X509::Store.new
312
+ store.set_default_paths
313
+
314
+ store
315
+ end
316
+
317
+ def label
318
+ @label ||= label!
319
+ end
320
+ def label!
321
+ port, name = @local_socket.peeraddr[1..2]
322
+ "#{name}:#{port}"
323
+ end
324
+
325
+ def log
326
+ @log ||= Zokor::ProgLogger.new("<#{label}>")
327
+ end
328
+ end
329
+ end