mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
460 lines
11 KiB
V
460 lines
11 KiB
V
module openssl
|
|
|
|
import io
|
|
import net
|
|
import time
|
|
import os
|
|
|
|
// SSLConn is the current connection
|
|
pub struct SSLConn {
|
|
config SSLConnectConfig
|
|
mut:
|
|
sslctx &C.SSL_CTX = unsafe { nil }
|
|
ssl &C.SSL = unsafe { nil }
|
|
handle int
|
|
duration time.Duration
|
|
|
|
owns_socket bool
|
|
}
|
|
|
|
[params]
|
|
pub struct SSLConnectConfig {
|
|
verify string // the path to a rootca.pem file, containing trusted CA certificate(s)
|
|
cert string // the path to a cert.pem file, containing client certificate(s) for the request
|
|
cert_key string // the path to a key.pem file, containing private keys for the client certificate(s)
|
|
validate bool // set this to true, if you want to stop requests, when their certificates are found to be invalid
|
|
|
|
in_memory_verification bool // if true, verify, cert, and cert_key are read from memory, not from a file
|
|
}
|
|
|
|
// new_ssl_conn instance an new SSLCon struct
|
|
pub fn new_ssl_conn(config SSLConnectConfig) !&SSLConn {
|
|
$if trace_ssl ? {
|
|
eprintln(@METHOD)
|
|
}
|
|
mut conn := &SSLConn{
|
|
config: config
|
|
sslctx: 0
|
|
ssl: 0
|
|
handle: 0
|
|
}
|
|
conn.init() or { return err }
|
|
return conn
|
|
}
|
|
|
|
// Select operation
|
|
enum Select {
|
|
read
|
|
write
|
|
except
|
|
}
|
|
|
|
// shutdown closes the ssl connection and does cleanup
|
|
pub fn (mut s SSLConn) shutdown() ! {
|
|
$if trace_ssl ? {
|
|
eprintln(@METHOD)
|
|
}
|
|
if s.ssl != 0 {
|
|
mut res := 0
|
|
for {
|
|
res = C.SSL_shutdown(voidptr(s.ssl))
|
|
if res < 0 {
|
|
err_res := ssl_error(res, s.ssl) or {
|
|
break // We break to free rest of resources
|
|
}
|
|
if err_res == .ssl_error_want_read {
|
|
for {
|
|
ready := @select(s.handle, .read, s.duration)!
|
|
if ready {
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
} else if err_res == .ssl_error_want_write {
|
|
for {
|
|
ready := @select(s.handle, .write, s.duration)!
|
|
if ready {
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
} else {
|
|
unsafe { C.SSL_free(voidptr(s.ssl)) }
|
|
if s.sslctx != 0 {
|
|
C.SSL_CTX_free(s.sslctx)
|
|
}
|
|
return error('unexepedted ssl error ${err_res}')
|
|
}
|
|
if s.ssl != 0 {
|
|
unsafe { C.SSL_free(voidptr(s.ssl)) }
|
|
}
|
|
if s.sslctx != 0 {
|
|
C.SSL_CTX_free(s.sslctx)
|
|
}
|
|
return error('Could not connect using SSL. (${err_res}),err')
|
|
} else if res == 0 {
|
|
continue
|
|
} else if res == 1 {
|
|
break
|
|
}
|
|
}
|
|
C.SSL_free(voidptr(s.ssl))
|
|
}
|
|
if s.sslctx != 0 {
|
|
C.SSL_CTX_free(s.sslctx)
|
|
}
|
|
if s.owns_socket {
|
|
net.shutdown(s.handle)
|
|
net.close(s.handle)!
|
|
}
|
|
}
|
|
|
|
fn (mut s SSLConn) init() ! {
|
|
$if trace_ssl ? {
|
|
eprintln(@METHOD)
|
|
}
|
|
s.sslctx = unsafe { C.SSL_CTX_new(C.SSLv23_client_method()) }
|
|
if s.sslctx == 0 {
|
|
return error("Couldn't get ssl context")
|
|
}
|
|
|
|
if s.config.validate {
|
|
C.SSL_CTX_set_verify_depth(s.sslctx, 4)
|
|
C.SSL_CTX_set_options(s.sslctx, C.SSL_OP_NO_SSLv2 | C.SSL_OP_NO_SSLv3 | C.SSL_OP_NO_COMPRESSION)
|
|
}
|
|
|
|
s.ssl = unsafe { &C.SSL(C.SSL_new(s.sslctx)) }
|
|
if s.ssl == 0 {
|
|
return error("Couldn't create OpenSSL instance.")
|
|
}
|
|
|
|
mut res := 0
|
|
|
|
if s.config.validate {
|
|
mut verify := s.config.verify
|
|
mut cert := s.config.cert
|
|
mut cert_key := s.config.cert_key
|
|
if s.config.in_memory_verification {
|
|
now := time.now().unix.str()
|
|
verify = os.temp_dir() + '/v_verify' + now
|
|
cert = os.temp_dir() + '/v_cert' + now
|
|
cert_key = os.temp_dir() + '/v_cert_key' + now
|
|
if s.config.verify != '' {
|
|
os.write_file(verify, s.config.verify)!
|
|
}
|
|
if s.config.cert != '' {
|
|
os.write_file(cert, s.config.cert)!
|
|
}
|
|
if s.config.cert_key != '' {
|
|
os.write_file(cert_key, s.config.cert_key)!
|
|
}
|
|
}
|
|
if s.config.verify != '' {
|
|
res = C.SSL_CTX_load_verify_locations(voidptr(s.sslctx), &char(verify.str),
|
|
0)
|
|
if s.config.validate && res != 1 {
|
|
return error('http: openssl: SSL_CTX_load_verify_locations failed')
|
|
}
|
|
}
|
|
if s.config.cert != '' {
|
|
res = C.SSL_CTX_use_certificate_file(voidptr(s.sslctx), &char(cert.str), C.SSL_FILETYPE_PEM)
|
|
if s.config.validate && res != 1 {
|
|
return error('http: openssl: SSL_CTX_use_certificate_file failed, res: ${res}')
|
|
}
|
|
}
|
|
if s.config.cert_key != '' {
|
|
res = C.SSL_CTX_use_PrivateKey_file(voidptr(s.sslctx), &char(cert_key.str),
|
|
C.SSL_FILETYPE_PEM)
|
|
if s.config.validate && res != 1 {
|
|
return error('http: openssl: SSL_CTX_use_PrivateKey_file failed, res: ${res}')
|
|
}
|
|
}
|
|
|
|
preferred_ciphers := 'HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4'
|
|
res = C.SSL_set_cipher_list(voidptr(s.ssl), preferred_ciphers.str)
|
|
if s.config.validate && res != 1 {
|
|
println('net.openssl: set cipher failed')
|
|
}
|
|
}
|
|
}
|
|
|
|
// connect to server using OpenSSL
|
|
pub fn (mut s SSLConn) connect(mut tcp_conn net.TcpConn, hostname string) ! {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} hostname: ${hostname}')
|
|
}
|
|
s.handle = tcp_conn.sock.handle
|
|
s.duration = tcp_conn.read_timeout()
|
|
|
|
mut res := C.SSL_set_tlsext_host_name(voidptr(s.ssl), voidptr(hostname.str))
|
|
if res != 1 {
|
|
return error('cannot set host name')
|
|
}
|
|
|
|
if C.SSL_set_fd(voidptr(s.ssl), tcp_conn.sock.handle) != 1 {
|
|
return error("Couldn't assign ssl to socket.")
|
|
}
|
|
|
|
s.complete_connect() or { return err }
|
|
}
|
|
|
|
// dial opens an ssl connection on hostname:port
|
|
pub fn (mut s SSLConn) dial(hostname string, port int) ! {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} hostname: ${hostname} | port: ${port}')
|
|
}
|
|
s.owns_socket = true
|
|
mut tcp_conn := net.dial_tcp('${hostname}:${port}') or { return err }
|
|
$if macos {
|
|
tcp_conn.set_blocking(true) or { return err }
|
|
}
|
|
s.connect(mut tcp_conn, hostname) or { return err }
|
|
}
|
|
|
|
fn (mut s SSLConn) complete_connect() ! {
|
|
$if trace_ssl ? {
|
|
eprintln(@METHOD)
|
|
}
|
|
for {
|
|
mut res := C.SSL_connect(voidptr(s.ssl))
|
|
if res != 1 {
|
|
err_res := ssl_error(res, s.ssl)!
|
|
if err_res == .ssl_error_want_read {
|
|
for {
|
|
ready := @select(s.handle, .read, s.duration)!
|
|
if ready {
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
} else if err_res == .ssl_error_want_write {
|
|
for {
|
|
ready := @select(s.handle, .write, s.duration)!
|
|
if ready {
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
return error('Could not connect using SSL. (${err_res}),err')
|
|
}
|
|
break
|
|
}
|
|
|
|
if s.config.validate {
|
|
for {
|
|
mut res := C.SSL_do_handshake(voidptr(s.ssl))
|
|
if res != 1 {
|
|
err_res := ssl_error(res, s.ssl)!
|
|
if err_res == .ssl_error_want_read {
|
|
for {
|
|
ready := @select(s.handle, .read, s.duration)!
|
|
if ready {
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
} else if err_res == .ssl_error_want_write {
|
|
for {
|
|
ready := @select(s.handle, .write, s.duration)!
|
|
if ready {
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
return error('Could not validate SSL certificate. (${err_res}),err')
|
|
}
|
|
break
|
|
}
|
|
pcert := C.SSL_get_peer_certificate(voidptr(s.ssl))
|
|
defer {
|
|
if pcert != 0 {
|
|
C.X509_free(pcert)
|
|
}
|
|
}
|
|
res := C.SSL_get_verify_result(voidptr(s.ssl))
|
|
if res != C.X509_V_OK {
|
|
return error('SSL handshake failed')
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn (mut s SSLConn) socket_read_into_ptr(buf_ptr &u8, len int) !int {
|
|
mut res := 0
|
|
$if trace_ssl ? {
|
|
defer {
|
|
if len > 0 {
|
|
eprintln('${@METHOD} res: ${res}: buf_ptr: ${voidptr(buf_ptr):x}, len: ${len}, hex: ${unsafe { buf_ptr.vbytes(len).hex() }} data: `${unsafe { buf_ptr.vstring_with_len(len) }}`')
|
|
}
|
|
}
|
|
}
|
|
for {
|
|
res = C.SSL_read(voidptr(s.ssl), buf_ptr, len)
|
|
if res > 0 {
|
|
return res
|
|
} else if res == 0 {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} ---> res: io.Eof')
|
|
}
|
|
return io.Eof{}
|
|
} else {
|
|
err_res := ssl_error(res, s.ssl)!
|
|
match err_res {
|
|
.ssl_error_want_read {
|
|
ready := @select(s.handle, .read, s.duration)!
|
|
if !ready {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} ---> res: net.err_timed_out .ssl_error_want_read')
|
|
}
|
|
return net.err_timed_out
|
|
}
|
|
}
|
|
.ssl_error_want_write {
|
|
ready := @select(s.handle, .write, s.duration)!
|
|
if !ready {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} ---> res: net.err_timed_out .ssl_error_want_write')
|
|
}
|
|
return net.err_timed_out
|
|
}
|
|
}
|
|
.ssl_error_zero_return {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} ---> res: 0 .ssl_error_zero_return')
|
|
}
|
|
return 0
|
|
}
|
|
else {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} ---> res: could not read, err_res: ${err_res}')
|
|
}
|
|
return error('Could not read using SSL. (${err_res})')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
pub fn (mut s SSLConn) read(mut buffer []u8) !int {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} buffer.len: ${buffer.len}')
|
|
}
|
|
return s.socket_read_into_ptr(&u8(buffer.data), buffer.len)
|
|
}
|
|
|
|
// write_ptr writes `len` bytes from `bytes` to the ssl connection
|
|
pub fn (mut s SSLConn) write_ptr(bytes &u8, len int) !int {
|
|
mut total_sent := 0
|
|
$if trace_ssl ? {
|
|
defer {
|
|
eprintln('${@METHOD} total_sent: ${total_sent}, bytes: ${voidptr(bytes):x}, len: ${len}, hex: ${unsafe { bytes.vbytes(len).hex() }}, data:-=-=-=-\n${unsafe { bytes.vstring_with_len(len) }}\n-=-=-=-')
|
|
}
|
|
}
|
|
unsafe {
|
|
mut ptr_base := bytes
|
|
for total_sent < len {
|
|
ptr := ptr_base + total_sent
|
|
remaining := len - total_sent
|
|
mut sent := C.SSL_write(voidptr(s.ssl), ptr, remaining)
|
|
if sent <= 0 {
|
|
err_res := ssl_error(sent, s.ssl)!
|
|
if err_res == .ssl_error_want_read {
|
|
for {
|
|
ready := @select(s.handle, .read, s.duration)!
|
|
if ready {
|
|
break
|
|
}
|
|
}
|
|
} else if err_res == .ssl_error_want_write {
|
|
for {
|
|
ready := @select(s.handle, .write, s.duration)!
|
|
if ready {
|
|
break
|
|
}
|
|
}
|
|
continue
|
|
} else if err_res == .ssl_error_zero_return {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} ---> res: ssl write on closed connection .ssl_error_zero_return')
|
|
}
|
|
return error('ssl write on closed connection') // Todo error_with_code close
|
|
}
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} ---> res: could not write SSL, err_res: ${err_res}')
|
|
}
|
|
return error_with_code('Could not write SSL. (${err_res}),err', int(err_res))
|
|
}
|
|
total_sent += sent
|
|
}
|
|
}
|
|
return total_sent
|
|
}
|
|
|
|
// write writes data from `bytes` to the ssl connection
|
|
pub fn (mut s SSLConn) write(bytes []u8) !int {
|
|
return s.write_ptr(&u8(bytes.data), bytes.len)!
|
|
}
|
|
|
|
// write_string writes a string to the ssl connection
|
|
pub fn (mut s SSLConn) write_string(str string) !int {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} str: ${str}')
|
|
}
|
|
return s.write_ptr(str.str, str.len)
|
|
}
|
|
|
|
/*
|
|
This is basically a copy of Emily socket implementation of select.
|
|
This have to be consolidated into common net lib features
|
|
when merging this to V
|
|
*/
|
|
// [typedef]
|
|
// pub struct C.fd_set {
|
|
// }
|
|
|
|
// Select waits for an io operation (specified by parameter `test`) to be available
|
|
fn @select(handle int, test Select, timeout time.Duration) !bool {
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} handle: ${handle}, timeout: ${timeout}')
|
|
}
|
|
set := C.fd_set{}
|
|
|
|
C.FD_ZERO(&set)
|
|
C.FD_SET(handle, &set)
|
|
|
|
seconds := timeout.milliseconds() / 1000
|
|
microseconds := timeout - (seconds * time.second)
|
|
mut tt := C.timeval{
|
|
tv_sec: u64(seconds)
|
|
tv_usec: u64(microseconds)
|
|
}
|
|
|
|
mut timeval_timeout := &tt
|
|
|
|
// infinite timeout is signaled by passing null as the timeout to
|
|
// select
|
|
if timeout == net.infinite_timeout {
|
|
timeval_timeout = &C.timeval(0)
|
|
}
|
|
|
|
match test {
|
|
.read {
|
|
net.socket_error(C.@select(handle + 1, &set, C.NULL, C.NULL, timeval_timeout))!
|
|
}
|
|
.write {
|
|
net.socket_error(C.@select(handle + 1, C.NULL, &set, C.NULL, timeval_timeout))!
|
|
}
|
|
.except {
|
|
net.socket_error(C.@select(handle + 1, C.NULL, C.NULL, &set, timeval_timeout))!
|
|
}
|
|
}
|
|
|
|
res := C.FD_ISSET(handle, &set)
|
|
$if trace_ssl ? {
|
|
eprintln('${@METHOD} ---> res: ${res}')
|
|
}
|
|
return res
|
|
}
|