📄 odbc.c
字号:
/* Check how the back-end reacts to commit/rollback commands. If
transactions are destructive (that is, prepared statements are
cleared when a commit/rollback is performed), we have to clear the
hStmtPrepared[] array to indicate that all statements have to be
re-prepared. Fortunately this is quite rare, both because most
back-ends don't do this (for virtually all ODBC-accessible data
sources (SQL Server, Access, dBASE, Paradox, etc etc) the behaviour
is SQL_CB_CLOSE, meaning that the currently active cursor is closed
but there's no need to call SQLPrepare() again) and because it only
affects CA cert stores opened in read/write mode */
sqlStatus = SQLGetInfo( dbmsInfo->hDbc, SQL_CURSOR_COMMIT_BEHAVIOR,
&transactBehaviour, sizeof( SQLUSMALLINT ),
&bufLen );
if( sqlStatusOK( sqlStatus ) && transactBehaviour == SQL_CB_DELETE )
dbmsInfo->transactIsDestructive = TRUE;
sqlStatus = SQLGetInfo( dbmsInfo->hDbc, SQL_CURSOR_ROLLBACK_BEHAVIOR,
&transactBehaviour, sizeof( SQLUSMALLINT ),
&bufLen );
if( sqlStatusOK( sqlStatus ) && transactBehaviour == SQL_CB_DELETE )
dbmsInfo->transactIsDestructive = TRUE;
/* Finally, determine the escape char being used. This is usually '\',
but it may have been changed for some reason */
sqlStatus = SQLGetInfo( dbmsInfo->hDbc, SQL_SEARCH_PATTERN_ESCAPE,
buffer, sizeof( buffer ), &bufLen );
dbmsInfo->escapeChar = sqlStatusOK( sqlStatus ) ? buffer[ 0 ] : '\\';
return( CRYPT_OK );
}
/* Bind parameters for a query/update. The caller has to supply the bound
data storage since it still has to exist later on when the query is
executed */
static int bindParameters( const SQLHSTMT hStmt, const char *boundData,
const int boundDataLength, const time_t boundDate,
TIMESTAMP_STRUCT *timestampStorage,
SQLINTEGER *lengthStorage,
DBMS_STATE_INFO *dbmsInfo,
const BOOLEAN bindForQuery )
{
SQLUSMALLINT paramNo = 1;
assert( isWritePtr( dbmsInfo, sizeof( DBMS_STATE_INFO ) ) );
/* Bind in any necessary parameters to the hStmt. If there's a bound
date parameter present it'll always come before the bound data, so
we bind the date first */
if( boundDate > 0 )
{
SQLRETURN sqlStatus;
const struct tm *timeInfo = gmtime( &boundDate );
assert( isWritePtr( timestampStorage, sizeof( TIMESTAMP_STRUCT ) ) );
/* Sanity check on input parameters */
if( timestampStorage == NULL )
return( CRYPT_ERROR_BADDATA );
/* Bind in the date. The handling of the ColumnSize parameter is
ugly, this value should be implicit in the underlying data type,
but a small number of back-ends (e.g. ones derived from the
Sybase 4.2 code line, which includes the current Sybase and SQL
Server) may support multiple time representations and require an
explicit length indicator to decide which one they should use
(not helped by the fact that the sample code in the
SQLBindParameter() manpage gives the ColumnSize parameter for
date/time types as zero, implying that it's ignored by the
driver).
Unfortunately the fact that some drivers specifically require
this parameter means that we have to provide an explicit length
value, see the comment in getDatatypeInfo() for how this is
obtained. Luckily the majority of back-ends have a single pre-
set value for this and ignore the length value, so the chances of
running into something that both requires the parameter and fails
the guesstimation procedure used in getDatatypeInfo() is small */
memset( timestampStorage, 0, sizeof( TIMESTAMP_STRUCT ) );
timestampStorage->year = timeInfo->tm_year + 1900;
timestampStorage->month = timeInfo->tm_mon + 1;
timestampStorage->day = timeInfo->tm_mday;
timestampStorage->hour = timeInfo->tm_hour;
timestampStorage->minute = timeInfo->tm_min;
timestampStorage->second = timeInfo->tm_sec;
sqlStatus = SQLBindParameter( hStmt, paramNo++, SQL_PARAM_INPUT,
SQL_C_TIMESTAMP, SQL_TIMESTAMP,
dbmsInfo->dateTimeNameColSize, 0,
timestampStorage, 0, NULL );
if( !sqlStatusOK( sqlStatus ) )
return( getErrorInfo( dbmsInfo, SQL_ERRLVL_STMT, hStmt,
CRYPT_ERROR_BADDATA ) );
}
if( boundData != NULL )
{
SQLSMALLINT valueType, parameterType;
SQLRETURN sqlStatus;
assert( boundDataLength > 0 && \
isReadPtr( boundData, boundDataLength ) );
assert( isWritePtr( lengthStorage, sizeof( SQLINTEGER ) ) );
/* Bind the query data in one of two ways depending on whether we're
binding for a query or an update. The effective difference
between the two is mostly ODBC voodoo related to how lengths are
specified, if it isn't done this way then Access (the default
ODBC data source on most Windows systems) returns "String data,
right truncated (null)" errors at random. No-one knows what the
cause is, and the only known fix is to juggle parameters until it
stops happening, although in some cases it appears to be because
it ignores the length value for SQL_CHAR data and tries to find a
terminating null character past the end of the string */
if( bindForQuery )
valueType = parameterType = SQL_C_CHAR;
else
{
valueType = ( dbmsInfo->hasBinaryBlobs ) ? SQL_C_BINARY : \
SQL_C_CHAR;
parameterType = dbmsInfo->blobType;
}
*lengthStorage = boundDataLength;
sqlStatus = SQLBindParameter( hStmt, paramNo++, SQL_PARAM_INPUT,
valueType, parameterType,
boundDataLength, 0,
( SQLPOINTER ) boundData,
boundDataLength, lengthStorage );
if( !sqlStatusOK( sqlStatus ) )
return( getErrorInfo( dbmsInfo, SQL_ERRLVL_STMT, hStmt,
CRYPT_ERROR_BADDATA ) );
}
return( CRYPT_OK );
}
/****************************************************************************
* *
* Database Open/Close Routines *
* *
****************************************************************************/
/* Close a previously-opened ODBC connection. We have to have this before
openDatabase() since it may be called by openDatabase() if the open
process fails. This is necessary because the complex ODBC open may
require a fairly extensive cleanup afterwards */
static void closeDatabase( DBMS_STATE_INFO *dbmsInfo )
{
int i;
assert( isWritePtr( dbmsInfo, sizeof( DBMS_STATE_INFO ) ) );
/* Commit the transaction. The default transaction mode is auto-commit
so the SQLEndTran() call isn't strictly necessary, but we play it
safe anyway */
if( dbmsInfo->needsUpdate )
{
SQLEndTran( SQL_HANDLE_DBC, dbmsInfo->hDbc, SQL_COMMIT );
dbmsInfo->needsUpdate = FALSE;
}
/* Clean up */
for( i = 0; i < NO_CACHED_QUERIES; i++ )
if( dbmsInfo->hStmt[ i ] != NULL )
{
SQLFreeHandle( SQL_HANDLE_STMT, dbmsInfo->hStmt[ i ] );
dbmsInfo->hStmtPrepared[ i ] = FALSE;
dbmsInfo->hStmt[ i ] = NULL;
}
SQLDisconnect( dbmsInfo->hDbc );
SQLFreeHandle( SQL_HANDLE_DBC, dbmsInfo->hDbc );
SQLFreeHandle( SQL_HANDLE_ENV, dbmsInfo->hEnv );
dbmsInfo->hDbc = NULL;
dbmsInfo->hEnv = NULL;
}
/* Open a connection to a data source. We don't check the return codes for
many of the parameter-fiddling functions since the worst that can happen
if they fail is that performance will be somewhat suboptimal.
For the somewhat flaky Win16 ODBC 1.x/2.x, it wasn't safe to allocate
statement handles at this point since these were handled in various
strange and peculiar ways by different ODBC drivers. The main problem was
that some drivers didn't support more than one hStmt per hDbc, some
supported only one active hStmt (an hStmt with results pending) per hDbc,
and some supported multiple active hStmt's per hDbc. For this reason the
older ODBC glue code used a strategy of allocating an hStmt, performing a
transaction, and then immediately freeing it again afterwards.
For any newer ODBC driver this isn't a problem any more (particularly when
it's necessary to accomodate threads), so we can allocate the hStmt here.
In addition to the main hStmt we also allocate a number of additional
hStmts used to contain pre-prepared, cached instances of frequently-
executed queries. This means that the expensive step of parsing the SQL
query, validating it against the system catalog, preparing an access
plan, and optimising the plan, are only performed once on the first query
rather than at every single access. If it's necessary to work with a
buggy ODBC driver that can't support multiple hStmts then everything can
be directed through the primary hStmt, at some loss in performance */
static int openDatabase( DBMS_STATE_INFO *dbmsInfo, const char *name,
const int options, int *featureFlags )
{
DBMS_NAME_INFO nameInfo;
SQLRETURN sqlStatus;
int i, status;
assert( isWritePtr( dbmsInfo, sizeof( DBMS_STATE_INFO ) ) );
assert( isReadPtr( name, 2 ) );
assert( isWritePtr( featureFlags, sizeof( int ) ) );
/* Clear return values */
memset( dbmsInfo, 0, sizeof( DBMS_STATE_INFO ) );
*featureFlags = DBMS_HAS_NONE;
#ifdef DYNAMIC_LOAD
/* Make sure that the driver is bound in */
if( hODBC == NULL_INSTANCE )
return( CRYPT_ERROR_OPEN );
#endif /* DYNAMIC_LOAD */
/* Parse the data source into its individual components */
status = dbmsParseName( &nameInfo, name, SQL_NTS );
if( cryptStatusError( status ) )
return( status );
/* Allocate environment and connection handles. Before we do anything
with the environment handle we have to set the ODBC version to 3 or
any succeeding calls will fail with a function sequence error. God
knows why they couldn't assume a default setting of ODBC 3.x for this
value when it requires an ODBC 3.x function call to get here in the
first place */
sqlStatus = SQLAllocHandle( SQL_HANDLE_ENV, SQL_NULL_HANDLE,
&dbmsInfo->hEnv );
if( !sqlStatusOK( sqlStatus ) )
{
/* We can't get any error details without at least an environment
handle, so all we can do is return a generic allocation error
message. If we get a failure at this point (and in particular
on the very first ODBC call) it's usually a sign of an incorrect
ODBC install or config (on non-Windows systems where it's not
part of the OS), since the ODBC driver can't initialise itself */
#ifdef __WINDOWS__
strcpy( dbmsInfo->errorMessage, "Couldn't allocate database "
"connection handle" );
#else
strcpy( dbmsInfo->errorMessage, "Couldn't allocate database "
"connection handle, this is probably due to an incorrect "
"ODBC driver install or an invalid configuration" );
#endif /* __WINDOWS__ */
return( CRYPT_ERROR_OPEN );
}
SQLSetEnvAttr( dbmsInfo->hEnv, SQL_ATTR_ODBC_VERSION,
( SQLPOINTER ) SQL_OV_ODBC3, SQL_IS_INTEGER );
sqlStatus = SQLAllocHandle( SQL_HANDLE_DBC, dbmsInfo->hEnv,
&dbmsInfo->hDbc );
if( !sqlStatusOK( sqlStatus ) )
{
status = getErrorInfo( dbmsInfo, SQL_ERRLVL_ENV, SQL_NULL_HSTMT,
CRYPT_ERROR_OPEN );
SQLFreeHandle( SQL_HANDLE_ENV, dbmsInfo->hEnv );
return( status );
}
/* Once everything is set up the way we want it, try to connect to a data
source and allocate a statement handle */
sqlStatus = SQLConnect( dbmsInfo->hDbc,
nameInfo.name, ( SQLSMALLINT ) nameInfo.nameLen,
nameInfo.user, ( SQLSMALLINT ) nameInfo.userLen,
nameInfo.password, ( SQLSMALLINT ) nameInfo.passwordLen );
if( !sqlStatusOK( sqlStatus ) )
{
status = getErrorInfo( dbmsInfo, SQL_ERRLVL_DBC, SQL_NULL_HSTMT,
CRYPT_ERROR_OPEN );
closeDatabase( dbmsInfo );
return( status );
}
/* Now that the connection is open, allocate the statement handles */
for( i = 0; i < NO_CACHED_QUERIES && sqlStatusOK( sqlStatus ); i++ )
sqlStatus = SQLAllocHandle( SQL_HANDLE_STMT, dbmsInfo->hDbc,
&dbmsInfo->hStmt[ i ] );
if( !sqlStatusOK( sqlStatus ) )
{
status = getErrorInfo( dbmsInfo, SQL_ERRLVL_DBC, SQL_NULL_HSTMT,
CRYPT_ERROR_OPEN );
closeDatabase( dbmsInfo );
return( status );
}
/* Set the access mode to read-only if we can. The default is R/W, but
setting it to read-only optimises transaction management */
if( options == CRYPT_KEYOPT_READONLY )
SQLSetStmtAttr( dbmsInfo->hDbc, SQL_ATTR_ACCESS_MODE,
( SQLPOINTER ) SQL_MODE_READ_ONLY, SQL_IS_INTEGER );
/* Set the cursor type to forward-only (which should be the default
anyway), concurrency to read-only if we're opening the database in
read-only mode (this again should be the default), and turn off
scanning for escape clauses in the SQL strings, which lets the driver
pass the string directly to the data source. The latter improves
both performance and (to some extent) security by reducing the
chances of hostile SQL injection, or at least by requiring specially
crafted back-end specific SQL rather than generic ODBC SQL to
function */
for( i = 0; i < NO_CACHED_QUERIES; i++ )
{
SQLSetStmtAttr( dbmsInfo->hStmt[ i ], SQL_ATTR_CURSOR_TYPE,
( SQLPOINTER ) SQL_CURSOR_FORWARD_ONLY,
SQL_IS_INTEGER );
if( options == CRYPT_KEYOPT_READONLY )
SQLSetStmtAttr( dbmsInfo->hStmt[ i ], SQL_ATTR_CONCURRENCY,
( SQLPOINTER ) SQL_CONCUR_READ_ONLY,
SQL_IS_INTEGER );
SQLSetStmtAttr( dbmsInfo->hStmt[ i ], SQL_ATTR_NOSCAN,
( SQLPOINTER ) SQL_NOSCAN_ON, SQL_IS_INTEGER );
}
/* Get various driver and data source-specific information that we may
need later on */
status = getDatatypeInfo( dbmsInfo, featureFlags );
if( cryptStatusError( status ) )
{
closeDatabase( dbmsInfo );
return( status );
}
return( CRYPT_OK );
}
/****************************************************************************
* *
* Database Read Routines *
* *
****************************************************************************/
/* Fetch data from a query */
static int fetchData( const SQLHSTMT hStmt, char *data,
int *dataLength, const int maxLength,
const DBMS_QUERY_TYPE queryType,
DBMS_STATE_INFO *dbmsInfo )
{
const SQLSMALLINT dataType = ( dbmsInfo->hasBinaryBlobs ) ? \
SQL_C_BINARY : SQL_C_CHAR;
SQLRETURN sqlStatus;
SQLUINTEGER length;
/* Clear return value */
if( dataLength != NULL )
*dataLength = 0;
/* Get the results of the transaction */
sqlStatus = SQLFetch( hStmt );
if( !sqlStatusOK( sqlStatus ) )
{
/* If the fetch status is SQL_NO_DATA, indicating the end of the
result set, we handle it specially since some drivers only return
the basic error code and don't provide any further diagnostic
info to be fetched by SQLGetDiagRec() */
if( sqlStatus == SQL_NO_DATA )
{
strcpy( dbmsInfo->errorMessage, "No data found" );
return( CRYPT_ERROR_NOTFOUND );
}
return( getErrorInfo( dbmsInfo, SQL_ERRLVL_STMT, hStmt,
CRYPT_ERROR_READ ) );
}
/* If we're just doing a presence check, we don't bother fetching data */
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -