/*
* Provides Custom BIO layer to interface OpenSSL with TCL. These
* functions directly interface between the IO channel and BIO buffers.
*
* Copyright (C) 1997-2000 Matt Newman <matt@novadigm.com>
* Copyright (C) 2024 Brian O'Hagan
*
*/
#include "tlsInt.h"
#include <openssl/bio.h>
/* Define BIO methods structure */
static BIO_METHOD *BioMethods = NULL;
/*
*-----------------------------------------------------------------------------
*
* 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 BIO_write_ex() or BIO_write().
*
* 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_Channel chan;
Tcl_Size ret;
int tclEofChan, tclErrno;
chan = Tls_GetParent((State *) BIO_get_data(bio), 0);
dprintf("[chan=%p] BioWrite(%p, <buf>, %d)", (void *)chan, (void *) bio, bufLen);
ret = Tcl_WriteRaw(chan, buf, (Tcl_Size) bufLen);
tclEofChan = Tcl_Eof(chan);
tclErrno = Tcl_GetErrno();
dprintf("[chan=%p] BioWrite(%d) -> %" TCL_SIZE_MODIFIER "d [tclEof=%d; tclErrno=%d]",
(void *) chan, bufLen, ret, tclEofChan, tclErrno);
BIO_clear_flags(bio, BIO_FLAGS_WRITE | BIO_FLAGS_SHOULD_RETRY);
if (tclEofChan && ret <= 0) {
dprintf("Got EOF while reading, returning a Connection Reset error which maps to Soft EOF");
Tcl_SetErrno(ECONNRESET);
ret = 0;
} else if (ret == 0) {
dprintf("Got 0 from Tcl_WriteRaw, and EOF is not set; ret = 0");
dprintf("Setting retry read flag");
BIO_set_retry_read(bio);
} else if (ret < 0) {
dprintf("We got some kind of I/O error");
if (tclErrno == EAGAIN) {
dprintf("It's EAGAIN");
} else {
dprintf("It's an unexpected error: %s/%i", Tcl_ErrnoMsg(tclErrno), tclErrno);
}
} else {
dprintf("Successfully wrote %" TCL_SIZE_MODIFIER "d bytes of data", ret);
}
if (ret != -1 || (ret == -1 && tclErrno == EAGAIN)) {
if (BIO_should_read(bio)) {
dprintf("Setting should retry read flag");
BIO_set_retry_read(bio);
}
}
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 BIO_read_ex() or BIO_read().
*
* Results:
* Returns the number of bytes read from channel, 0 for EOF, or -1 for
* error.
*
* Side effects:
* Reads channel data into BIO.
*
*-----------------------------------------------------------------------------
*/
static int BioRead(BIO *bio, char *buf, int bufLen) {
Tcl_Channel chan;
Tcl_Size ret = 0;
int tclEofChan, tclErrno;
chan = Tls_GetParent((State *) BIO_get_data(bio), 0);
dprintf("[chan=%p] BioRead(%p, <buf>, %d)", (void *) chan, (void *) bio, bufLen);
if (buf == NULL) {
return 0;
}
ret = Tcl_ReadRaw(chan, buf, (Tcl_Size) bufLen);
tclEofChan = Tcl_Eof(chan);
tclErrno = Tcl_GetErrno();
dprintf("[chan=%p] BioRead(%d) -> %" TCL_SIZE_MODIFIER "d [tclEof=%d; tclErrno=%d]",
(void *) chan, bufLen, ret, tclEofChan, tclErrno);
BIO_clear_flags(bio, BIO_FLAGS_READ | BIO_FLAGS_SHOULD_RETRY);
if (tclEofChan && ret <= 0) {
dprintf("Got EOF while reading, returning a Connection Reset error which maps to Soft EOF");
Tcl_SetErrno(ECONNRESET);
ret = 0;
} else if (ret == 0) {
dprintf("Got 0 from Tcl_Read or Tcl_ReadRaw, and EOF is not set; ret = 0");
dprintf("Setting retry read flag");
BIO_set_retry_read(bio);
} else if (ret < 0) {
dprintf("We got some kind of I/O error");
if (tclErrno == EAGAIN) {
dprintf("It's EAGAIN");
} else {
dprintf("It's an unexpected error: %s/%i", Tcl_ErrnoMsg(tclErrno), tclErrno);
}
} else {
dprintf("Successfully read %" TCL_SIZE_MODIFIER "d bytes of data", ret);
}
if (ret != -1 || (ret == -1 && tclErrno == EAGAIN)) {
if (BIO_should_write(bio)) {
dprintf("Setting should retry write flag");
BIO_set_retry_write(bio);
}
}
dprintf("BioRead(%p, <buf>, %d) [%p] returning %" TCL_SIZE_MODIFIER "d", (void *) bio,
bufLen, (void *) chan, 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 BIO_puts().
*
* 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, <string:%p>) called", 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 BIO_ctrl().
*
* Results:
* Function dependent
*
* Side effects:
* Function dependent
*
*-----------------------------------------------------------------------------
*/
static long BioCtrl(BIO *bio, int cmd, long num, void *ptr) {
Tcl_Channel chan;
long ret = 1;
chan = Tls_GetParent((State *) BIO_get_data(bio), 0);
dprintf("BioCtrl(%p, 0x%x, 0x%lx, %p)", (void *) bio, cmd, num, ptr);
switch (cmd) {
case BIO_CTRL_RESET:
dprintf("Got BIO_CTRL_RESET");
ret = 0;
break;
case BIO_C_FILE_SEEK:
dprintf("Got BIO_C_FILE_SEEK");
ret = 0;
break;
case BIO_C_FILE_TELL:
dprintf("Got BIO_C_FILE_TELL");
ret = 0;
break;
case BIO_CTRL_INFO:
dprintf("Got BIO_CTRL_INFO");
ret = 1;
break;
case BIO_C_SET_FD:
dprintf("Unsupported call: BIO_C_SET_FD");
ret = -1;
break;
case BIO_C_GET_FD:
dprintf("Unsupported call: BIO_C_GET_FD");
ret = -1;
break;
case BIO_CTRL_GET_CLOSE:
dprintf("Got BIO_CTRL_CLOSE");
ret = BIO_get_shutdown(bio);
break;
case BIO_CTRL_SET_CLOSE:
dprintf("Got BIO_SET_CLOSE");
BIO_set_shutdown(bio, num);
break;
case BIO_CTRL_EOF:
dprintf("Got BIO_CTRL_EOF");
ret = ((chan) ? Tcl_Eof(chan) : 1);
break;
case BIO_CTRL_PENDING:
dprintf("Got BIO_CTRL_PENDING");
ret = ((chan) ? ((Tcl_InputBuffered(chan) ? 1 : 0)) : 0);
dprintf("BIO_CTRL_PENDING(%d)", (int) ret);
break;
case BIO_CTRL_WPENDING:
dprintf("Got BIO_CTRL_WPENDING");
ret = 0;
break;
case BIO_CTRL_DUP:
dprintf("Got BIO_CTRL_DUP");
break;
case BIO_CTRL_FLUSH:
dprintf("Got BIO_CTRL_FLUSH");
ret = ((chan) && (Tcl_WriteRaw(chan, "", 0) >= 0) ? 1 : -1);
dprintf("BIO_CTRL_FLUSH returning value %li", ret);
break;
case BIO_CTRL_PUSH:
dprintf("Got BIO_CTRL_PUSH");
ret = 0;
break;
case BIO_CTRL_POP:
dprintf("Got BIO_CTRL_POP");
ret = 0;
break;
case BIO_CTRL_SET:
dprintf("Got BIO_CTRL_SET");
ret = 0;
break;
case BIO_CTRL_GET :
dprintf("Got BIO_CTRL_GET ");
ret = 0;
break;
#ifdef BIO_CTRL_GET_KTLS_SEND
case BIO_CTRL_GET_KTLS_SEND:
dprintf("Got BIO_CTRL_GET_KTLS_SEND");
ret = 0;
break;
#endif
#ifdef BIO_CTRL_GET_KTLS_RECV
case BIO_CTRL_GET_KTLS_RECV:
dprintf("Got BIO_CTRL_GET_KTLS_RECV");
ret = 0;
break;
#endif
default:
dprintf("Got unknown control command (%i)", cmd);
ret = 0;
break;
}
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 BIO_new().
*
* Results:
* Returns boolean success result (1=success, 0=failure)
*
* Side effects:
* Initializes BIO structure.
*
*-----------------------------------------------------------------------------
*/
static int BioNew(BIO *bio) {
dprintf("BioNew(%p) called", bio);
BIO_set_init(bio, 0);
BIO_set_data(bio, NULL);
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 BIO_free().
*
* Results:
* Returns boolean success result
*
* Side effects:
* Initializes BIO structure.
*
*-----------------------------------------------------------------------------
*/
static int BioFree(BIO *bio) {
if (bio == NULL) {
return 0;
}
dprintf("BioFree(%p) called", bio);
if (BIO_get_shutdown(bio)) {
if (BIO_get_init(bio)) {
/*shutdown(bio->num, 2) */
/*closesocket(bio->num) */
}
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;
void *parentChannelFdIn_p, *parentChannelFdOut_p;
int parentChannelFdIn, parentChannelFdOut, parentChannelFd;
int validParentChannelFd;
int tclGetChannelHandleRet;
#endif
dprintf("BIO_new_tcl() called");
if (BioMethods == NULL) {
BioMethods = BIO_meth_new(BIO_TYPE_TCL, "tcl");
BIO_meth_set_write(BioMethods, BioWrite);
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) {
tclGetChannelHandleRet = Tcl_GetChannelHandle(parentChannel, TCL_READABLE, (ClientData) &parentChannelFdIn_p);
if (tclGetChannelHandleRet == TCL_OK) {
tclGetChannelHandleRet = Tcl_GetChannelHandle(parentChannel, TCL_WRITABLE, (ClientData) &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;
return bio;
}
dprintf("Falling back to Tcl I/O for this channel");
#endif
bio = BIO_new(BioMethods);
BIO_set_data(bio, statePtr);
BIO_set_shutdown(bio, flags);
BIO_set_init(bio, 1);
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;
}