/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
Copyright (C) 2009 Red Hat, Inc.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, see .
*/
#ifdef HAVE_CONFIG_H
#include
#endif
#ifdef WIN32
#include
#endif
#include
#include
#include
#include "common/ssl_verify.h"
#include "common.h"
#include "red_peer.h"
#include "utils.h"
#include "debug.h"
#include "platform_utils.h"
static void SPICE_GNUC_NORETURN ssl_error()
{
unsigned long last_error = ERR_peek_last_error();
ERR_print_errors_fp(stderr);
THROW_ERR(SPICEC_ERROR_CODE_SSL_ERROR, "SSL Error: %s", ERR_error_string(last_error, NULL));
}
RedPeer::RedPeer()
: _peer (INVALID_SOCKET)
, _shut (false)
, _ctx (NULL)
, _ssl (NULL)
{
}
RedPeer::~RedPeer()
{
cleanup();
}
void RedPeer::cleanup()
{
if (_ssl) {
SSL_free(_ssl);
_ssl = NULL;
}
if (_ctx) {
SSL_CTX_free(_ctx);
_ctx = NULL;
}
if (_peer != INVALID_SOCKET) {
closesocket(_peer);
_peer = INVALID_SOCKET;
}
}
void RedPeer::connect_to_peer(const char* host, int portnr)
{
struct addrinfo ai, *result = NULL, *e;
char uaddr[INET6_ADDRSTRLEN+1];
char uport[33], port[33];
int err = 0, rc, no_delay = 1;
ASSERT(_ctx == NULL && _ssl == NULL && _peer == INVALID_SOCKET);
try {
memset(&ai,0, sizeof(ai));
ai.ai_flags = AI_CANONNAME;
#ifdef AI_ADDRCONFIG
ai.ai_flags |= AI_ADDRCONFIG;
#endif
ai.ai_family = PF_UNSPEC;
ai.ai_socktype = SOCK_STREAM;
snprintf(port, sizeof(port), "%d", portnr);
rc = getaddrinfo(host, port, &ai, &result);
if (rc != 0) {
THROW_ERR(SPICEC_ERROR_CODE_GETHOSTBYNAME_FAILED, "cannot resolve host address %s", host);
}
Lock lock(_lock);
_peer = INVALID_SOCKET;
for (e = result; e != NULL; e = e->ai_next) {
if ((_peer = socket(e->ai_family, e->ai_socktype, e->ai_protocol)) == INVALID_SOCKET) {
int err = sock_error();
THROW_ERR(SPICEC_ERROR_CODE_SOCKET_FAILED, "failed to create socket: %s (%d)",
sock_err_message(err), err);
}
if (setsockopt(_peer, IPPROTO_TCP, TCP_NODELAY, (const char*)&no_delay, sizeof(no_delay)) ==
SOCKET_ERROR) {
LOG_WARN("set TCP_NODELAY failed");
}
getnameinfo((struct sockaddr*)e->ai_addr, e->ai_addrlen,
uaddr,INET6_ADDRSTRLEN, uport,32,
NI_NUMERICHOST | NI_NUMERICSERV);
DBG(0, "Trying %s %s", uaddr, uport);
if (::connect(_peer, e->ai_addr, e->ai_addrlen) == SOCKET_ERROR) {
err = sock_error();
LOG_INFO("Connect failed: %s (%d)",
sock_err_message(err), err);
closesocket(_peer);
_peer = INVALID_SOCKET;
continue;
}
DBG(0, "Connected to %s %s", uaddr, uport);
break;
}
lock.unlock();
freeaddrinfo(result);
if (_peer == INVALID_SOCKET) {
THROW_ERR(SPICEC_ERROR_CODE_CONNECT_FAILED, "failed to connect: %s (%d)",
sock_err_message(err), err);
}
_serial = 0;
} catch (...) {
Lock lock(_lock);
cleanup();
throw;
}
}
void RedPeer::connect_unsecure(const char* host, int portnr)
{
connect_to_peer(host, portnr);
ASSERT(_ctx == NULL && _ssl == NULL && _peer != INVALID_SOCKET);
LOG_INFO("Connected to %s %d", host, portnr);
}
void RedPeer::connect_secure(const ConnectionOptions& options, const char* host)
{
int return_code;
SPICE_SSL_VERIFY_OP auth_flags;
SpiceOpenSSLVerify* verify = NULL;
int portnr = options.secure_port;
connect_to_peer(host, portnr);
ASSERT(_ctx == NULL && _ssl == NULL && _peer != INVALID_SOCKET);
LOG_INFO("Connected to %s %d", host, portnr);
try {
#if OPENSSL_VERSION_NUMBER >= 0x10000000L
const SSL_METHOD *ssl_method = TLSv1_method();
#else
SSL_METHOD *ssl_method = TLSv1_method();
#endif
_ctx = SSL_CTX_new(ssl_method);
if (_ctx == NULL) {
ssl_error();
}
auth_flags = options.host_auth.type_flags;
if ((auth_flags & SPICE_SSL_VERIFY_OP_HOSTNAME) ||
(auth_flags & SPICE_SSL_VERIFY_OP_SUBJECT)) {
std::string CA_file = options.host_auth.CA_file;
ASSERT(!CA_file.empty());
return_code = SSL_CTX_load_verify_locations(_ctx, CA_file.c_str(), NULL);
if (return_code != 1) {
if (auth_flags & SPICE_SSL_VERIFY_OP_PUBKEY) {
LOG_WARN("SSL_CTX_load_verify_locations failed, CA_file=%s. "
"only pubkey authentication is active", CA_file.c_str());
auth_flags = SPICE_SSL_VERIFY_OP_PUBKEY;
}
else {
LOG_ERROR("SSL_CTX_load_verify_locations failed CA_file=%s", CA_file.c_str());
ssl_error();
}
}
}
return_code = SSL_CTX_set_cipher_list(_ctx, options.ciphers.c_str());
if (return_code != 1) {
LOG_ERROR("SSL_CTX_set_cipher_list failed, ciphers=%s", options.ciphers.c_str());
ssl_error();
}
_ssl = SSL_new(_ctx);
if (!_ssl) {
THROW("create ssl failed");
}
verify = spice_openssl_verify_new(
_ssl, auth_flags,
host,
(char*)&options.host_auth.host_pubkey[0],
options.host_auth.host_pubkey.size(),
options.host_auth.host_subject.c_str());
BIO* sbio = BIO_new_socket(_peer, BIO_NOCLOSE);
if (!sbio) {
THROW("alloc new socket bio failed");
}
SSL_set_bio(_ssl, sbio, sbio);
return_code = SSL_connect(_ssl);
if (return_code <= 0) {
int ssl_error_code = SSL_get_error(_ssl, return_code);
LOG_ERROR("failed to connect w/SSL, ssl_error %s",
ERR_error_string(ssl_error_code, NULL));
ssl_error();
}
} catch (...) {
Lock lock(_lock);
spice_openssl_verify_free(verify);
cleanup();
throw;
}
spice_openssl_verify_free(verify);
}
void RedPeer::shutdown()
{
if (_peer != INVALID_SOCKET) {
if (_ssl) {
SSL_shutdown(_ssl);
}
::shutdown(_peer, SHUT_RDWR);
}
_shut = true;
}
void RedPeer::disconnect()
{
Lock lock(_lock);
shutdown();
}
void RedPeer::close()
{
Lock lock(_lock);
if (_peer != INVALID_SOCKET) {
if (_ctx) {
SSL_free(_ssl);
_ssl = NULL;
SSL_CTX_free(_ctx);
_ctx = NULL;
}
closesocket(_peer);
_peer = INVALID_SOCKET;
}
}
void RedPeer::swap(RedPeer* other)
{
Lock lock(_lock);
SOCKET temp_peer = _peer;
SSL_CTX *temp_ctx = _ctx;
SSL *temp_ssl = _ssl;
_peer = other->_peer;
other->_peer = temp_peer;
if (_ctx) {
_ctx = other->_ctx;
_ssl = other->_ssl;
other->_ctx = temp_ctx;
other->_ssl = temp_ssl;
}
if (_shut) {
shutdown();
}
}
uint32_t RedPeer::receive(uint8_t *buf, uint32_t size)
{
uint8_t *pos = buf;
while (size) {
int now;
if (_ctx == NULL) {
if ((now = recv(_peer, (char *)pos, size, 0)) <= 0) {
int err = sock_error();
if (now == SOCKET_ERROR && err == WOULDBLOCK_ERR) {
break;
}
if (now == 0 || err == SHUTDOWN_ERR) {
throw RedPeer::DisconnectedException();
}
if (err == INTERRUPTED_ERR) {
continue;
}
THROW_ERR(SPICEC_ERROR_CODE_RECV_FAILED, "%s (%d)", sock_err_message(err), err);
}
size -= now;
pos += now;
} else {
if ((now = SSL_read(_ssl, pos, size)) <= 0) {
int ssl_error = SSL_get_error(_ssl, now);
if (ssl_error == SSL_ERROR_WANT_READ) {
break;
}
if (ssl_error == SSL_ERROR_SYSCALL) {
int err = sock_error();
if (now == -1) {
if (err == WOULDBLOCK_ERR) {
break;
}
if (err == INTERRUPTED_ERR) {
continue;
}
}
if (now == 0 || (now == -1 && err == SHUTDOWN_ERR)) {
throw RedPeer::DisconnectedException();
}
THROW_ERR(SPICEC_ERROR_CODE_SEND_FAILED, "%s (%d)", sock_err_message(err), err);
} else if (ssl_error == SSL_ERROR_ZERO_RETURN) {
throw RedPeer::DisconnectedException();
}
THROW_ERR(SPICEC_ERROR_CODE_RECV_FAILED, "ssl error %d", ssl_error);
}
size -= now;
pos += now;
}
}
return pos - buf;
}
RedPeer::CompoundInMessage* RedPeer::receive()
{
SpiceDataHeader header;
AutoRef message;
receive((uint8_t*)&header, sizeof(SpiceDataHeader));
message.reset(new CompoundInMessage(header.serial, header.type, header.size, header.sub_list));
receive((*message)->data(), (*message)->compound_size());
return message.release();
}
uint32_t RedPeer::send(uint8_t *buf, uint32_t size)
{
uint8_t *pos = buf;
while (size) {
int now;
if (_ctx == NULL) {
if ((now = ::send(_peer, (char *)pos, size, 0)) == SOCKET_ERROR) {
int err = sock_error();
if (err == WOULDBLOCK_ERR) {
break;
}
if (err == SHUTDOWN_ERR) {
throw RedPeer::DisconnectedException();
}
if (err == INTERRUPTED_ERR) {
continue;
}
THROW_ERR(SPICEC_ERROR_CODE_SEND_FAILED, "%s (%d)", sock_err_message(err), err);
}
size -= now;
pos += now;
} else {
if ((now = SSL_write(_ssl, pos, size)) <= 0) {
int ssl_error = SSL_get_error(_ssl, now);
if (ssl_error == SSL_ERROR_WANT_WRITE) {
break;
}
if (ssl_error == SSL_ERROR_SYSCALL) {
int err = sock_error();
if (now == -1) {
if (err == WOULDBLOCK_ERR) {
break;
}
if (err == INTERRUPTED_ERR) {
continue;
}
}
if (now == 0 || (now == -1 && err == SHUTDOWN_ERR)) {
throw RedPeer::DisconnectedException();
}
THROW_ERR(SPICEC_ERROR_CODE_SEND_FAILED, "%s (%d)", sock_err_message(err), err);
} else if (ssl_error == SSL_ERROR_ZERO_RETURN) {
throw RedPeer::DisconnectedException();
}
THROW_ERR(SPICEC_ERROR_CODE_SEND_FAILED, "ssl error %d", ssl_error);
}
size -= now;
pos += now;
}
}
return pos - buf;
}
uint32_t RedPeer::do_send(RedPeer::OutMessage& message, uint32_t skip_bytes)
{
uint8_t *data;
int free_data;
size_t len;
uint32_t res;
data = spice_marshaller_linearize(message.marshaller(), skip_bytes,
&len, &free_data);
res = send(data, len);
if (free_data) {
free(data);
}
return res;
}
uint32_t RedPeer::send(RedPeer::OutMessage& message)
{
message.header().serial = ++_serial;
message.header().size = message.message_size() - sizeof(SpiceDataHeader);
return do_send(message, 0);
}
RedPeer::OutMessage::OutMessage(uint32_t type)
: _marshaller (spice_marshaller_new())
{
SpiceDataHeader *header;
header = (SpiceDataHeader *)
spice_marshaller_reserve_space(_marshaller, sizeof(SpiceDataHeader));
spice_marshaller_set_base(_marshaller, sizeof(SpiceDataHeader));
header->type = type;
header->sub_list = 0;
}
void RedPeer::OutMessage::reset(uint32_t type)
{
spice_marshaller_reset(_marshaller);
SpiceDataHeader *header;
header = (SpiceDataHeader *)
spice_marshaller_reserve_space(_marshaller, sizeof(SpiceDataHeader));
spice_marshaller_set_base(_marshaller, sizeof(SpiceDataHeader));
header->type = type;
header->sub_list = 0;
}
RedPeer::OutMessage::~OutMessage()
{
spice_marshaller_destroy(_marshaller);
}