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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +15 -0
- data/bin/zokor +169 -0
- data/ca-certs-small.crt +2252 -0
- data/double-proxy.rb +58 -0
- data/lib/zokor.rb +18 -0
- data/lib/zokor/config.rb +178 -0
- data/lib/zokor/logger.rb +97 -0
- data/lib/zokor/proxy_connection.rb +329 -0
- data/lib/zokor/proxy_magic.rb +65 -0
- data/lib/zokor/version.rb +3 -0
- data/zokor.gemspec +39 -0
- metadata +136 -0
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
|
data/lib/zokor/config.rb
ADDED
@@ -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
|
data/lib/zokor/logger.rb
ADDED
@@ -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
|