zokor 0.2.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.
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