/*
* Provides Custom BIO layer to interface OpenSSL with TCL. These functions
* directly interface between the TCL IO channel and BIO buffers.
*
* Copyright (C) 1997-2000 Matt Newman <matt@novadigm.com>
* Copyright (C) 2024 Brian O'Hagan
*
*/
/*
tlsBIO.c tlsIO.c
+------+ +-----+ +------+
| |Tcl_WriteRaw <-- BioWrite| SSL |BIO_write <-- TlsOutputProc <-- Write| |
|socket| <encrypted> | BIO | <unencrypted> | App |
| |Tcl_ReadRaw --> BioRead| |BIO_Read --> TlsInputProc --> Read| |
+------+ +-----+ +------+
*/
#include "tlsInt.h"
#include <openssl/bio.h>
/* Define BIO methods structure */
static BIO_METHOD *BioMethods = NULL;
/*
*-----------------------------------------------------------------------------
*
* BIOShouldRetry --
*
* Determine if should retry operation based on error code. Same
* conditions as BIO_sock_should_retry function.
*
* Results:
* 1 = retry, 0 = no retry
*
* Side effects:
* None
*
*-----------------------------------------------------------------------------
*/
static int BIOShouldRetry(int code) {
int res = 0;
dprintf("BIOShouldRetry %d=%s", code, Tcl_ErrnoMsg(code));
if (code == EAGAIN || code == EWOULDBLOCK || code == ENOTCONN || code == EPROTO ||
#ifdef _WIN32
code == WSAEWOULDBLOCK ||
#endif
code == EINTR || code == EINPROGRESS || code == EALREADY) {
res = 1;
}
dprintf("BIOShouldRetry %d=%s, res=%d", code, Tcl_ErrnoMsg(code), res);
return res;
}
/*
*-----------------------------------------------------------------------------
*
* BioWrite --
*
* This function is used to read encrypted data from the BIO and write it
* into the socket. This function will be called in response to the
* application calling the BIO_write_ex() or BIO_write() functions.
*
* Results:
* Returns the number of bytes written to channel, 0 for EOF, or -1 for
* error.
*
* Side effects:
* Writes BIO data to channel.
*
*-----------------------------------------------------------------------------
*/
static int BioWrite(BIO *bio, const char *buf, int bufLen) {
Tcl_Size ret;
int is_eof, tclErrno;
State *statePtr = (State *) BIO_get_data(bio);
Tcl_Channel chan = Tls_GetParent(statePtr, 0);
dprintf("[chan=%p] BioWrite(bio=%p, buf=%p, len=%d)", (void *)chan, (void *) bio, buf, bufLen);
BIO_clear_retry_flags(bio);
Tcl_SetErrno(0);
/* Write data to underlying channel */
ret = Tcl_WriteRaw(chan, buf, (Tcl_Size) bufLen);
is_eof = Tcl_Eof(chan);
tclErrno = Tcl_GetErrno();
dprintf("[chan=%p] BioWrite(%d) -> %" TCL_SIZE_MODIFIER "d [tclEof=%d; tclErrno=%d: %s]",
(void *) chan, bufLen, ret, is_eof, tclErrno, Tcl_ErrnoMsg(tclErrno));
if (ret > 0) {
dprintf("Successfully wrote %" TCL_SIZE_MODIFIER "d bytes of data", ret);
} else if (ret == 0) {
if (is_eof) {
dprintf("Got EOF while writing, returning a Connection Reset error which maps to Soft EOF");
Tcl_SetErrno(ECONNRESET);
BIO_set_flags(bio, BIO_FLAGS_IN_EOF);
} else {
dprintf("Got 0 from Tcl_WriteRaw, and EOF is not set; ret = 0");
BIO_set_retry_write(bio);
dprintf("Setting retry read flag");
BIO_set_retry_read(bio);
}
} else {
dprintf("We got some kind of I/O error");
if (BIOShouldRetry(tclErrno)) {
dprintf("Try again for: %i=%s", tclErrno, Tcl_ErrnoMsg(tclErrno));
BIO_set_retry_write(bio);
} else {
dprintf("Unexpected error: %i=%s", tclErrno, Tcl_ErrnoMsg(tclErrno));
}
}
dprintf("BioWrite returning %" TCL_SIZE_MODIFIER "d", ret);
return (int) ret;
}
/*
*-----------------------------------------------------------------------------
*
* BioRead --
*
* This function is used to read encrypted data from the socket and
* write it into the BIO. This function will be called in response to the
* application calling the BIO_read_ex() or BIO_read() functions.
*
* Results:
* Returns the number of bytes read from channel, 0 for EOF, or -1 for
* error.
*
* Side effects:
* Reads channel data into BIO.
*
* Data is received in whole blocks known as records from the peer. A whole
* record is processed (e.g. decrypted) in one go and is buffered by OpenSSL
* until it is read by the application via a call to SSL_read. SSL_pending()
* returns the number of bytes which have been processed, buffered, and are
* available inside ssl for immediate read. SSL_has_pending() returns 1 if
* data is buffered (whether processed or unprocessed) and 0 otherwise.
*
*-----------------------------------------------------------------------------
*/
static int BioRead(BIO *bio, char *buf, int bufLen) {
Tcl_Size ret = 0;
int is_eof, tclErrno, is_blocked;
State *statePtr = (State *) BIO_get_data(bio);
Tcl_Channel chan = Tls_GetParent(statePtr, 0);
dprintf("[chan=%p] BioRead(bio=%p, buf=%p, len=%d)", (void *) chan, (void *) bio, buf, bufLen);
if (buf == NULL || bufLen <= 0) {
return 0;
}
BIO_clear_retry_flags(bio);
Tcl_SetErrno(0);
/* Read data from underlying channel */
ret = Tcl_ReadRaw(chan, buf, (Tcl_Size) bufLen);
is_eof = Tcl_Eof(chan);
tclErrno = Tcl_GetErrno();
is_blocked = Tcl_InputBlocked(chan);
dprintf("[chan=%p] BioRead(%d) -> %" TCL_SIZE_MODIFIER "d [tclEof=%d; blocked=%d; tclErrno=%d: %s]",
(void *) chan, bufLen, ret, is_eof, is_blocked, tclErrno, Tcl_ErrnoMsg(tclErrno));
if (ret > 0) {
dprintf("Successfully read %" TCL_SIZE_MODIFIER "d bytes of data", ret);
} else if (ret == 0) {
if (is_eof) {
dprintf("Got EOF while reading, returning a Connection Reset error which maps to Soft EOF");
Tcl_SetErrno(ECONNRESET);
BIO_set_flags(bio, BIO_FLAGS_IN_EOF);
} else if (is_blocked) {
dprintf("Got input blocked from Tcl_ReadRaw. Setting retry read flag");
BIO_set_retry_read(bio);
}
} else {
dprintf("We got some kind of I/O error");
if (BIOShouldRetry(tclErrno)) {
dprintf("Try again for: %i=%s", tclErrno, Tcl_ErrnoMsg(tclErrno));
BIO_set_retry_read(bio);
} else {
dprintf("Unexpected error: %i=%s", tclErrno, Tcl_ErrnoMsg(tclErrno));
}
}
dprintf("BioRead returning %" TCL_SIZE_MODIFIER "d", ret);
return (int) ret;
}
/*
*-----------------------------------------------------------------------------
*
* BioPuts --
*
* This function is used to read a NULL terminated string from the BIO and
* write it to the channel. This function will be called in response to
* the application calling the BIO_puts() function.
*
* Results:
* Returns the number of bytes written to channel or 0 for error.
*
* Side effects:
* Writes data to channel.
*
*-----------------------------------------------------------------------------
*/
static int BioPuts(BIO *bio, const char *str) {
dprintf("BioPuts(%p) \"%s\"", bio, str);
return BioWrite(bio, str, (int) strlen(str));
}
/*
*-----------------------------------------------------------------------------
*
* BioCtrl --
*
* This function is used to process control messages in the BIO. This
* function will be called in response to the application calling the
* BIO_ctrl() function.
*
* Results:
* Function dependent
*
* Side effects:
* Function dependent
*
*-----------------------------------------------------------------------------
*/
static long BioCtrl(BIO *bio, int cmd, long num, void *ptr) {
long ret = 1;
State *statePtr = (State *) BIO_get_data(bio);
Tcl_Channel chan = Tls_GetParent(statePtr, 0);
dprintf("BioCtrl(%p, 0x%x, 0x%lx, %p)", (void *) bio, cmd, num, ptr);
switch (cmd) {
case BIO_CTRL_RESET:
/* opt - Resets BIO to initial state. Implements BIO_reset. */
dprintf("Got BIO_CTRL_RESET");
/* Return 1 for success (0 for file BIOs) and -1 for failure */
ret = 0;
break;
case BIO_CTRL_EOF:
/* opt - Returns whether EOF has been reached. Implements BIO_eof. */
dprintf("Got BIO_CTRL_EOF");
/* Returns 1 if EOF has been reached, 0 if not, or <0 for failure */
ret = ((chan) ? (Tcl_Eof(chan) || BIO_test_flags(bio, BIO_FLAGS_IN_EOF)) : 1);
break;
case BIO_CTRL_INFO:
/* opt - extra info on BIO. Implements BIO_get_mem_data */
dprintf("Got BIO_CTRL_INFO");
ret = 0;
break;
case BIO_CTRL_SET:
/* man - set the 'IO' parameter */
dprintf("Got BIO_CTRL_SET");
ret = 0;
break;
case BIO_CTRL_GET:
/* man - get the 'IO' parameter */
dprintf("Got BIO_CTRL_GET ");
ret = 0;
break;
case BIO_CTRL_PUSH:
/* opt - internal, used to signify change. Implements BIO_push */
dprintf("Got BIO_CTRL_PUSH");
ret = 0;
break;
case BIO_CTRL_POP:
/* opt - internal, used to signify change. Implements BIO_pop */
dprintf("Got BIO_CTRL_POP");
ret = 0;
break;
case BIO_CTRL_GET_CLOSE:
/* man - Get the close on BIO_free() flag set by BIO_CTRL_SET_CLOSE. Implements BIO_get_close */
dprintf("Got BIO_CTRL_CLOSE");
/* Returns BIO_CLOSE, BIO_NOCLOSE, or <0 for failure */
ret = BIO_get_shutdown(bio);
break;
case BIO_CTRL_SET_CLOSE:
/* man - Set the close on BIO_free() flag. Implements BIO_set_close */
dprintf("Got BIO_SET_CLOSE");
BIO_set_shutdown(bio, num);
/* Returns 1 on success or <=0 for failure */
ret = 1;
break;
case BIO_CTRL_PENDING:
/* opt - Return number of bytes in BIO waiting to be read. Implements BIO_pending. */
dprintf("Got BIO_CTRL_PENDING");
/* Return the amount of pending data or 0 for error */
ret = ((chan) ? Tcl_InputBuffered(chan) : 0);
break;
case BIO_CTRL_FLUSH:
/* opt - Flush any buffered output. Implements BIO_flush. */
dprintf("Got BIO_CTRL_FLUSH");
/* Use Tcl_WriteRaw instead of Tcl_Flush to operate on right chan in stack */
/* Returns 1 for success, <=0 for error/retry. */
ret = ((chan) && (Tcl_WriteRaw(chan, "", 0) >= 0) ? 1 : -1);
/*ret = BioWrite(bio, NULL, 0);*/
break;
case BIO_CTRL_DUP:
/* man - extra stuff for 'duped' BIO. Implements BIO_dup_state */
dprintf("Got BIO_CTRL_DUP");
ret = 1;
break;
case BIO_CTRL_WPENDING:
/* opt - Return number of bytes in BIO still to be written. Implements BIO_wpending. */
dprintf("Got BIO_CTRL_WPENDING");
/* Return the amount of pending data or 0 for error */
ret = ((chan) ? Tcl_OutputBuffered(chan) : 0);
break;
case BIO_CTRL_SET_CALLBACK:
/* opt - Sets an informational callback. Implements BIO_set_info_callback */
ret = 0;
break;
case BIO_CTRL_GET_CALLBACK:
/* opt - Get and return the info callback. Implements BIO_get_info_callback */
ret = 0;
break;
case BIO_C_FILE_SEEK:
/* Not used for sockets. Tcl_Seek only works on top chan. Implements BIO_seek() */
dprintf("Got BIO_C_FILE_SEEK");
ret = 0; /* Return 0 success and -1 for failure */
break;
case BIO_C_FILE_TELL:
/* Not used for sockets. Tcl_Tell only works on top chan. Implements BIO_tell() */
dprintf("Got BIO_C_FILE_TELL");
ret = 0; /* Return 0 success and -1 for failure */
break;
case BIO_C_SET_FD:
/* Implements BIO_set_fd */
dprintf("Unsupported call: BIO_C_SET_FD");
ret = -1;
break;
case BIO_C_GET_FD:
/* Implements BIO_get_fd() */
dprintf("Unsupported call: BIO_C_GET_FD");
ret = -1;
break;
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && defined(BIO_CTRL_GET_KTLS_SEND)
case BIO_CTRL_GET_KTLS_SEND:
/* Implements BIO_get_ktls_send */
dprintf("Got BIO_CTRL_GET_KTLS_SEND");
/* Returns 1 if the BIO is using the Kernel TLS data-path for sending, 0 if not */
ret = 0;
break;
#endif
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && defined(BIO_CTRL_GET_KTLS_RECV)
case BIO_CTRL_GET_KTLS_RECV:
/* Implements BIO_get_ktls_recv */
dprintf("Got BIO_CTRL_GET_KTLS_RECV");
/* Returns 1 if the BIO is using the Kernel TLS data-path for receiving, 0 if not */
ret = 0;
break;
#endif
default:
dprintf("Got unknown control command (%i)", cmd);
ret = 0;
break;
}
dprintf("BioCtrl return value %li", ret);
return ret;
}
/*
*-----------------------------------------------------------------------------
*
* BioNew --
*
* This function is used to create a new instance of the BIO. This
* function will be called in response to the application calling the
* BIO_new() function.
*
* Results:
* Returns boolean success result (1=success, 0=failure)
*
* Side effects:
* Initializes BIO structure.
*
*-----------------------------------------------------------------------------
*/
static int BioNew(BIO *bio) {
dprintf("BioNew(%p) called", bio);
if (bio == NULL) {
return 0;
}
BIO_set_data(bio, NULL);
BIO_set_init(bio, 0);
BIO_clear_flags(bio, -1);
return 1;
}
/*
*-----------------------------------------------------------------------------
*
* BioFree --
*
* This function is used to destroy an instance of a BIO. This function
* will be called in response to the application calling the BIO_free()
* function.
*
* Results:
* Returns boolean success result
*
* Side effects:
* Initializes BIO structure.
*
*-----------------------------------------------------------------------------
*/
static int BioFree(BIO *bio) {
dprintf("BioFree(%p) called", bio);
if (bio == NULL) {
return 0;
}
/* Clear flags if set to BIO_CLOSE (close I/O stream when the BIO is freed) */
if (BIO_get_shutdown(bio)) {
BIO_set_data(bio, NULL);
BIO_set_init(bio, 0);
BIO_clear_flags(bio, -1);
}
return 1;
}
/*
*-----------------------------------------------------------------------------
*
* BIO_new_tcl --
*
* This function is used to initialize the BIO method handlers.
*
* Results:
* Returns pointer to BIO or NULL for failure
*
* Side effects:
* Initializes BIO Methods.
*
*-----------------------------------------------------------------------------
*/
BIO *BIO_new_tcl(State *statePtr, int flags) {
BIO *bio;
#ifdef TCLTLS_SSL_USE_FASTPATH
Tcl_Channel parentChannel;
const Tcl_ChannelType *parentChannelType;
int parentChannelFdIn, parentChannelFdOut, parentChannelFd;
int validParentChannelFd;
#endif
dprintf("BIO_new_tcl() called");
/* Create custom BIO method */
if (BioMethods == NULL) {
/* BIO_TYPE_BIO = (19|BIO_TYPE_SOURCE_SINK) -- half a BIO pair */
/* BIO_TYPE_CONNECT = (12|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR) */
/* BIO_TYPE_ACCEPT = (13|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR) */
BioMethods = BIO_meth_new(BIO_TYPE_BIO, "tcl");
if (BioMethods == NULL) {
dprintf("Memory allocation error");
return NULL;
}
/* Not used BIO_meth_set_write_ex */
BIO_meth_set_write(BioMethods, BioWrite);
/* Not used BIO_meth_set_read_ex */
BIO_meth_set_read(BioMethods, BioRead);
BIO_meth_set_puts(BioMethods, BioPuts);
BIO_meth_set_ctrl(BioMethods, BioCtrl);
BIO_meth_set_create(BioMethods, BioNew);
BIO_meth_set_destroy(BioMethods, BioFree);
}
if (statePtr == NULL) {
dprintf("Asked to setup a NULL state, just creating the initial configuration");
return NULL;
}
#ifdef TCLTLS_SSL_USE_FASTPATH
/*
* If the channel can be mapped back to a file descriptor, just use the file descriptor
* with the SSL library since it will likely be optimized for this.
*/
parentChannel = Tls_GetParent(statePtr, 0);
parentChannelType = Tcl_GetChannelType(parentChannel);
validParentChannelFd = 0;
if (strcmp(parentChannelType->typeName, "tcp") == 0) {
void *parentChannelFdIn_p, *parentChannelFdOut_p;
int tclGetChannelHandleRet;
tclGetChannelHandleRet = Tcl_GetChannelHandle(parentChannel, TCL_READABLE, &parentChannelFdIn_p);
if (tclGetChannelHandleRet == TCL_OK) {
tclGetChannelHandleRet = Tcl_GetChannelHandle(parentChannel, TCL_WRITABLE, &parentChannelFdOut_p);
if (tclGetChannelHandleRet == TCL_OK) {
parentChannelFdIn = PTR2INT(parentChannelFdIn_p);
parentChannelFdOut = PTR2INT(parentChannelFdOut_p);
if (parentChannelFdIn == parentChannelFdOut) {
parentChannelFd = parentChannelFdIn;
validParentChannelFd = 1;
}
}
}
}
if (validParentChannelFd) {
dprintf("We found a shortcut, this channel is backed by a socket: %i", parentChannelFdIn);
bio = BIO_new_socket(parentChannelFd, flags);
statePtr->flags |= TLS_TCL_FASTPATH;
BIO_set_data(bio, statePtr);
BIO_set_shutdown(bio, flags);
BIO_set_init(bio, 1);
return bio;
}
#endif
dprintf("Falling back to Tcl I/O for this channel");
bio = BIO_new(BioMethods);
BIO_set_data(bio, statePtr);
BIO_set_shutdown(bio, flags);
BIO_set_init(bio, 1); /* Enable read & write */
return bio;
}
/*
*-----------------------------------------------------------------------------
*
* BIO_cleanup --
*
* This function is used to destroy a BIO_METHOD structure and free up any
* memory associated with it.
*
* Results:
* Standard TCL result
*
* Side effects:
* Destroys BIO Methods.
*
*-----------------------------------------------------------------------------
*/
int BIO_cleanup () {
dprintf("BIO_cleanup() called");
if (BioMethods != NULL) {
BIO_meth_free(BioMethods);
BioMethods = NULL;
}
return TCL_OK;
}