mirror of
https://github.com/postgres/postgres.git
synced 2025-07-24 14:22:24 +03:00
SSPI authentication on Windows. GSSAPI compatible client when doing Kerberos
against a Unix server, and Windows-specific server-side authentication using SSPI "negotiate" method (Kerberos or NTLM). Only builds properly with MSVC for now.
This commit is contained in:
@ -10,7 +10,7 @@
|
||||
* exceed INITIAL_EXPBUFFER_SIZE (currently 256 bytes).
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/interfaces/libpq/fe-auth.c,v 1.127 2007/07/12 14:43:21 mha Exp $
|
||||
* $PostgreSQL: pgsql/src/interfaces/libpq/fe-auth.c,v 1.128 2007/07/23 10:16:54 mha Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@ -329,11 +329,6 @@ pg_krb5_sendauth(char *PQerrormsg, int sock, const char *hostname, const char *s
|
||||
/*
|
||||
* GSSAPI authentication system.
|
||||
*/
|
||||
#if defined(HAVE_GSSAPI_H)
|
||||
#include <gssapi.h>
|
||||
#else
|
||||
#include <gssapi/gssapi.h>
|
||||
#endif
|
||||
|
||||
#if defined(WIN32) && !defined(WIN32_ONLY_COMPILER)
|
||||
/*
|
||||
@ -378,7 +373,7 @@ pg_GSS_error_int(char *mprefix, char *msg, int msglen,
|
||||
* GSSAPI errors contains two parts. Put as much as possible of
|
||||
* both parts into the string.
|
||||
*/
|
||||
void
|
||||
static void
|
||||
pg_GSS_error(char *mprefix, char *msg, int msglen,
|
||||
OM_uint32 maj_stat, OM_uint32 min_stat)
|
||||
{
|
||||
@ -407,7 +402,7 @@ pg_GSS_continue(char *PQerrormsg, PGconn *conn)
|
||||
&conn->gctx,
|
||||
conn->gtarg_nam,
|
||||
GSS_C_NO_OID,
|
||||
conn->gflags,
|
||||
GSS_C_MUTUAL_FLAG,
|
||||
0,
|
||||
GSS_C_NO_CHANNEL_BINDINGS,
|
||||
(conn->gctx==GSS_C_NO_CONTEXT)?GSS_C_NO_BUFFER:&conn->ginbuf,
|
||||
@ -504,7 +499,192 @@ pg_GSS_startup(char *PQerrormsg, PGconn *conn)
|
||||
|
||||
return pg_GSS_continue(PQerrormsg, conn);
|
||||
}
|
||||
#endif
|
||||
#endif /* ENABLE_GSS */
|
||||
|
||||
|
||||
#ifdef ENABLE_SSPI
|
||||
/*
|
||||
* SSPI authentication system (Windows only)
|
||||
*/
|
||||
|
||||
static void
|
||||
pg_SSPI_error(char *mprefix, char *msg, int msglen, SECURITY_STATUS r)
|
||||
{
|
||||
char sysmsg[256];
|
||||
|
||||
if (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, r, 0, sysmsg, sizeof(sysmsg), NULL) == 0)
|
||||
snprintf(msg, msglen, "%s: sspi error %x", mprefix, r);
|
||||
else
|
||||
snprintf(msg, msglen, "%s: %s (%x)", mprefix, sysmsg, r);
|
||||
}
|
||||
|
||||
/*
|
||||
* Continue SSPI authentication with next token as needed.
|
||||
*/
|
||||
static int
|
||||
pg_SSPI_continue(char *PQerrormsg, PGconn *conn)
|
||||
{
|
||||
SECURITY_STATUS r;
|
||||
CtxtHandle newContext;
|
||||
ULONG contextAttr;
|
||||
SecBufferDesc inbuf;
|
||||
SecBufferDesc outbuf;
|
||||
SecBuffer OutBuffers[1];
|
||||
SecBuffer InBuffers[1];
|
||||
|
||||
if (conn->sspictx != NULL)
|
||||
{
|
||||
/*
|
||||
* On runs other than the first we have some data to send. Put this
|
||||
* data in a SecBuffer type structure.
|
||||
*/
|
||||
inbuf.ulVersion = SECBUFFER_VERSION;
|
||||
inbuf.cBuffers = 1;
|
||||
inbuf.pBuffers = InBuffers;
|
||||
InBuffers[0].pvBuffer = conn->ginbuf.value;
|
||||
InBuffers[0].cbBuffer = conn->ginbuf.length;
|
||||
InBuffers[0].BufferType = SECBUFFER_TOKEN;
|
||||
}
|
||||
|
||||
OutBuffers[0].pvBuffer = NULL;
|
||||
OutBuffers[0].BufferType = SECBUFFER_TOKEN;
|
||||
OutBuffers[0].cbBuffer = 0;
|
||||
outbuf.cBuffers = 1;
|
||||
outbuf.pBuffers = OutBuffers;
|
||||
outbuf.ulVersion = SECBUFFER_VERSION;
|
||||
|
||||
r = InitializeSecurityContext(conn->sspicred,
|
||||
conn->sspictx,
|
||||
conn->sspitarget,
|
||||
ISC_REQ_ALLOCATE_MEMORY,
|
||||
0,
|
||||
SECURITY_NETWORK_DREP,
|
||||
(conn->sspictx == NULL)?NULL:&inbuf,
|
||||
0,
|
||||
&newContext,
|
||||
&outbuf,
|
||||
&contextAttr,
|
||||
NULL);
|
||||
|
||||
if (r != SEC_E_OK && r != SEC_I_CONTINUE_NEEDED)
|
||||
{
|
||||
pg_SSPI_error(libpq_gettext("SSPI continuation error"),
|
||||
PQerrormsg, PQERRORMSG_LENGTH, r);
|
||||
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
|
||||
if (conn->sspictx == NULL)
|
||||
{
|
||||
/* On first run, transfer retreived context handle */
|
||||
conn->sspictx = malloc(sizeof(CtxtHandle));
|
||||
if (conn->sspictx == NULL)
|
||||
{
|
||||
strncpy(PQerrormsg, libpq_gettext("out of memory\n"), PQERRORMSG_LENGTH);
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
memcpy(conn->sspictx, &newContext, sizeof(CtxtHandle));
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* On subsequent runs when we had data to send, free buffers that contained
|
||||
* this data.
|
||||
*/
|
||||
free(conn->ginbuf.value);
|
||||
conn->ginbuf.value = NULL;
|
||||
conn->ginbuf.length = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* If SSPI returned any data to be sent to the server (as it normally would),
|
||||
* send this data as a password packet.
|
||||
*/
|
||||
if (outbuf.cBuffers > 0)
|
||||
{
|
||||
if (outbuf.cBuffers != 1)
|
||||
{
|
||||
/*
|
||||
* This should never happen, at least not for Kerberos authentication. Keep check
|
||||
* in case it shows up with other authentication methods later.
|
||||
*/
|
||||
strncpy(PQerrormsg, "SSPI returned invalid number of output buffers\n", PQERRORMSG_LENGTH);
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
|
||||
if (pqPacketSend(conn, 'p',
|
||||
outbuf.pBuffers[0].pvBuffer, outbuf.pBuffers[0].cbBuffer))
|
||||
{
|
||||
FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
FreeContextBuffer(outbuf.pBuffers[0].pvBuffer);
|
||||
}
|
||||
|
||||
/* Cleanup is handled by the code in freePGconn() */
|
||||
return STATUS_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Send initial SSPI authentication token.
|
||||
* If use_negotiate is 0, use kerberos authentication package which is
|
||||
* compatible with Unix. If use_negotiate is 1, use the negotiate package
|
||||
* which supports both kerberos and NTLM, but is not compatible with Unix.
|
||||
*/
|
||||
static int
|
||||
pg_SSPI_startup(char *PQerrormsg, PGconn *conn, int use_negotiate)
|
||||
{
|
||||
SECURITY_STATUS r;
|
||||
TimeStamp expire;
|
||||
|
||||
conn->sspictx = NULL;
|
||||
|
||||
/*
|
||||
* Retreive credentials handle
|
||||
*/
|
||||
conn->sspicred = malloc(sizeof(CredHandle));
|
||||
if (conn->sspicred == NULL)
|
||||
{
|
||||
strncpy(PQerrormsg, libpq_gettext("out of memory\n"), PQERRORMSG_LENGTH);
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
|
||||
r = AcquireCredentialsHandle(NULL, use_negotiate?"negotiate":"kerberos", SECPKG_CRED_OUTBOUND, NULL, NULL, NULL, NULL, conn->sspicred, &expire);
|
||||
if (r != SEC_E_OK)
|
||||
{
|
||||
pg_SSPI_error("acquire credentials failed", PQerrormsg, PQERRORMSG_LENGTH, r);
|
||||
free(conn->sspicred);
|
||||
conn->sspicred = NULL;
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compute target principal name. SSPI has a different format from GSSAPI, but
|
||||
* not more complex. We can skip the @REALM part, because Windows will fill that
|
||||
* in for us automatically.
|
||||
*/
|
||||
if (conn->pghost == NULL)
|
||||
{
|
||||
strncpy(PQerrormsg, libpq_gettext("hostname must be specified\n"), PQERRORMSG_LENGTH);
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
conn->sspitarget = malloc(strlen(conn->krbsrvname)+strlen(conn->pghost)+2);
|
||||
if (!conn->sspitarget)
|
||||
{
|
||||
strncpy(PQerrormsg, libpq_gettext("out of memory\n"), PQERRORMSG_LENGTH);
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
sprintf(conn->sspitarget, "%s/%s", conn->krbsrvname, conn->pghost);
|
||||
|
||||
/*
|
||||
* Indicate that we're in SSPI authentication mode to make sure that
|
||||
* pg_SSPI_continue is called next time in the negotiation.
|
||||
*/
|
||||
conn->usesspi = 1;
|
||||
|
||||
return pg_SSPI_continue(PQerrormsg, conn);
|
||||
}
|
||||
#endif /* ENABLE_SSPI */
|
||||
|
||||
/*
|
||||
* Respond to AUTH_REQ_SCM_CREDS challenge.
|
||||
@ -671,27 +851,60 @@ pg_fe_sendauth(AuthRequest areq, PGconn *conn, const char *hostname,
|
||||
return STATUS_ERROR;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_GSS
|
||||
#if defined(ENABLE_GSS) || defined(ENABLE_SSPI)
|
||||
case AUTH_REQ_GSS:
|
||||
pglock_thread();
|
||||
if (pg_GSS_startup(PQerrormsg, conn) != STATUS_OK)
|
||||
{
|
||||
/* PQerrormsg already filled in. */
|
||||
int r;
|
||||
pglock_thread();
|
||||
/*
|
||||
* If we have both GSS and SSPI support compiled in, use SSPI
|
||||
* support by default. This is overridable by a connection string parameter.
|
||||
* Note that when using SSPI we still leave the negotiate parameter off,
|
||||
* since we want SSPI to use the GSSAPI kerberos protocol. For actual
|
||||
* SSPI negotiate protocol, we use AUTH_REQ_SSPI.
|
||||
*/
|
||||
#if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
|
||||
if (conn->gsslib && (pg_strcasecmp(conn->gsslib, "gssapi") == 0))
|
||||
r = pg_GSS_startup(PQerrormsg, conn);
|
||||
else
|
||||
r = pg_SSPI_startup(PQerrormsg, conn, 0);
|
||||
#elif defined(ENABLE_GSS) && !defined(ENABLE_SSPI)
|
||||
r = pg_GSS_startup(PQerrormsg, conn);
|
||||
#elif !defined(ENABLE_GSS) && defined(ENABLE_SSPI)
|
||||
r = pg_SSPI_startup(PQerrormsg, conn, 0);
|
||||
#endif
|
||||
if (r != STATUS_OK)
|
||||
{
|
||||
/* PQerrormsg already filled in. */
|
||||
pgunlock_thread();
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
pgunlock_thread();
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
pgunlock_thread();
|
||||
break;
|
||||
|
||||
case AUTH_REQ_GSS_CONT:
|
||||
pglock_thread();
|
||||
if (pg_GSS_continue(PQerrormsg, conn) != STATUS_OK)
|
||||
{
|
||||
/* PQerrormsg already filled in. */
|
||||
int r;
|
||||
pglock_thread();
|
||||
#if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
|
||||
if (conn->usesspi)
|
||||
r = pg_SSPI_continue(PQerrormsg, conn);
|
||||
else
|
||||
r = pg_GSS_continue(PQerrormsg, conn);
|
||||
#elif defined(ENABLE_GSS) && !defined(ENABLE_SSPI)
|
||||
r = pg_GSS_continue(PQerrormsg, conn);
|
||||
#elif !defined(ENABLE_GSS) && defined(ENABLE_SSPI)
|
||||
r = pg_SSPI_continue(PQerrormsg, conn);
|
||||
#endif
|
||||
if (r != STATUS_OK)
|
||||
{
|
||||
/* PQerrormsg already filled in. */
|
||||
pgunlock_thread();
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
pgunlock_thread();
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
pgunlock_thread();
|
||||
break;
|
||||
|
||||
#else
|
||||
@ -702,6 +915,30 @@ pg_fe_sendauth(AuthRequest areq, PGconn *conn, const char *hostname,
|
||||
return STATUS_ERROR;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_SSPI
|
||||
case AUTH_REQ_SSPI:
|
||||
/*
|
||||
* SSPI has it's own startup message so libpq can decide which
|
||||
* method to use. Indicate to pg_SSPI_startup that we want
|
||||
* SSPI negotiation instead of Kerberos.
|
||||
*/
|
||||
pglock_thread();
|
||||
if (pg_SSPI_startup(PQerrormsg, conn, 1) != STATUS_OK)
|
||||
{
|
||||
/* PQerrormsg already filled in. */
|
||||
pgunlock_thread();
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
pgunlock_thread();
|
||||
break;
|
||||
#else
|
||||
case AUTH_REQ_SSPI:
|
||||
snpritnf(PQerrormsg, PQERRORMSG_LENGTH,
|
||||
libpq_gettext("SSPI authentication not supported\n"));
|
||||
return STATUS_ERROR;
|
||||
#endif
|
||||
|
||||
|
||||
case AUTH_REQ_MD5:
|
||||
case AUTH_REQ_CRYPT:
|
||||
case AUTH_REQ_PASSWORD:
|
||||
|
@ -8,7 +8,7 @@
|
||||
*
|
||||
*
|
||||
* IDENTIFICATION
|
||||
* $PostgreSQL: pgsql/src/interfaces/libpq/fe-connect.c,v 1.349 2007/07/11 08:27:33 mha Exp $
|
||||
* $PostgreSQL: pgsql/src/interfaces/libpq/fe-connect.c,v 1.350 2007/07/23 10:16:54 mha Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@ -181,12 +181,18 @@ static const PQconninfoOption PQconninfoOptions[] = {
|
||||
{"sslmode", "PGSSLMODE", DefaultSSLMode, NULL,
|
||||
"SSL-Mode", "", 8}, /* sizeof("disable") == 8 */
|
||||
|
||||
#if defined(KRB5) || defined(ENABLE_GSS)
|
||||
#if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
|
||||
/* Kerberos and GSSAPI authentication support specifying the service name */
|
||||
{"krbsrvname", "PGKRBSRVNAME", PG_KRB_SRVNAM, NULL,
|
||||
"Kerberos-service-name", "", 20},
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
|
||||
/* GSSAPI and SSPI both enabled, give a way to override which is used by default */
|
||||
{"gsslib", "PGGSSLIB", NULL, NULL,
|
||||
"GSS-library", "", 7}, /* sizeof("gssapi") = 7 */
|
||||
#endif
|
||||
|
||||
/* Terminating entry --- MUST BE LAST */
|
||||
{NULL, NULL, NULL, NULL,
|
||||
NULL, NULL, 0}
|
||||
@ -412,10 +418,14 @@ connectOptions1(PGconn *conn, const char *conninfo)
|
||||
conn->sslmode = strdup("require");
|
||||
}
|
||||
#endif
|
||||
#if defined(KRB5) || defined(ENABLE_GSS)
|
||||
#if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
|
||||
tmp = conninfo_getval(connOptions, "krbsrvname");
|
||||
conn->krbsrvname = tmp ? strdup(tmp) : NULL;
|
||||
#endif
|
||||
#if defined(ENABLE_GSS) && defined(ENABLE_SSPI)
|
||||
tmp = conninfo_getval(connOptions, "gsslib");
|
||||
conn->gsslib = tmp ? strdup(tmp) : NULL;
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Free the option info - all is in conn now
|
||||
@ -1661,22 +1671,13 @@ keep_going: /* We will come back to here until there is
|
||||
return PGRES_POLLING_READING;
|
||||
}
|
||||
}
|
||||
#ifdef ENABLE_GSS
|
||||
#if defined(ENABLE_GSS) || defined(ENABLE_SSPI)
|
||||
/*
|
||||
* AUTH_REQ_GSS provides no input data
|
||||
* Just set the request flags
|
||||
*/
|
||||
if (areq == AUTH_REQ_GSS)
|
||||
conn->gflags = GSS_C_MUTUAL_FLAG;
|
||||
|
||||
/*
|
||||
* Read GSSAPI data packets
|
||||
* Continue GSSAPI/SSPI authentication
|
||||
*/
|
||||
if (areq == AUTH_REQ_GSS_CONT)
|
||||
{
|
||||
/* Continue GSSAPI authentication */
|
||||
int llen = msgLength - 4;
|
||||
|
||||
/*
|
||||
* We can be called repeatedly for the same buffer.
|
||||
* Avoid re-allocating the buffer in this case -
|
||||
@ -2002,7 +2003,7 @@ freePGconn(PGconn *conn)
|
||||
free(conn->pgpass);
|
||||
if (conn->sslmode)
|
||||
free(conn->sslmode);
|
||||
#if defined(KRB5) || defined(ENABLE_GSS)
|
||||
#if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
|
||||
if (conn->krbsrvname)
|
||||
free(conn->krbsrvname);
|
||||
#endif
|
||||
@ -2030,6 +2031,26 @@ freePGconn(PGconn *conn)
|
||||
if (conn->goutbuf.length)
|
||||
gss_release_buffer(&min_s, &conn->goutbuf);
|
||||
}
|
||||
#endif
|
||||
#ifdef ENABLE_SSPI
|
||||
{
|
||||
if (conn->ginbuf.length)
|
||||
free(conn->ginbuf.value);
|
||||
|
||||
if (conn->sspitarget)
|
||||
free(conn->sspitarget);
|
||||
|
||||
if (conn->sspicred)
|
||||
{
|
||||
FreeCredentialsHandle(conn->sspicred);
|
||||
free(conn->sspicred);
|
||||
}
|
||||
if (conn->sspictx)
|
||||
{
|
||||
DeleteSecurityContext(conn->sspictx);
|
||||
free(conn->sspictx);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
pstatus = conn->pstatus;
|
||||
while (pstatus != NULL)
|
||||
|
@ -12,7 +12,7 @@
|
||||
* Portions Copyright (c) 1996-2007, PostgreSQL Global Development Group
|
||||
* Portions Copyright (c) 1994, Regents of the University of California
|
||||
*
|
||||
* $PostgreSQL: pgsql/src/interfaces/libpq/libpq-int.h,v 1.123 2007/07/12 14:36:52 mha Exp $
|
||||
* $PostgreSQL: pgsql/src/interfaces/libpq/libpq-int.h,v 1.124 2007/07/23 10:16:54 mha Exp $
|
||||
*
|
||||
*-------------------------------------------------------------------------
|
||||
*/
|
||||
@ -52,6 +52,22 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_SSPI
|
||||
#define SECURITY_WIN32
|
||||
#include <security.h>
|
||||
#undef SECURITY_WIN32
|
||||
|
||||
#ifndef ENABLE_GSS
|
||||
/*
|
||||
* Define a fake structure compatible with GSSAPI on Unix.
|
||||
*/
|
||||
typedef struct {
|
||||
void *value;
|
||||
int length;
|
||||
} gss_buffer_desc;
|
||||
#endif
|
||||
#endif /* ENABLE_SSPI */
|
||||
|
||||
#ifdef USE_SSL
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/err.h>
|
||||
@ -276,7 +292,7 @@ struct pg_conn
|
||||
char *pguser; /* Postgres username and password, if any */
|
||||
char *pgpass;
|
||||
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
|
||||
#if defined(KRB5) || defined(ENABLE_GSS)
|
||||
#if defined(KRB5) || defined(ENABLE_GSS) || defined(ENABLE_SSPI)
|
||||
char *krbsrvname; /* Kerberos service name */
|
||||
#endif
|
||||
|
||||
@ -361,11 +377,23 @@ struct pg_conn
|
||||
#ifdef ENABLE_GSS
|
||||
gss_ctx_id_t gctx; /* GSS context */
|
||||
gss_name_t gtarg_nam; /* GSS target name */
|
||||
OM_uint32 gflags; /* GSS service request flags */
|
||||
gss_buffer_desc ginbuf; /* GSS input token */
|
||||
gss_buffer_desc goutbuf; /* GSS output token */
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_SSPI
|
||||
#ifndef ENABLE_GSS
|
||||
gss_buffer_desc ginbuf; /* GSS input token */
|
||||
#else
|
||||
char *gsslib; /* What GSS librart to use ("gssapi" or "sspi") */
|
||||
#endif
|
||||
CredHandle *sspicred; /* SSPI credentials handle */
|
||||
CtxtHandle *sspictx; /* SSPI context */
|
||||
char *sspitarget;/* SSPI target name */
|
||||
int usesspi; /* Indicate if SSPI is in use on the connection */
|
||||
#endif
|
||||
|
||||
|
||||
/* Buffer for current error message */
|
||||
PQExpBufferData errorMessage; /* expansible string */
|
||||
|
||||
@ -415,12 +443,6 @@ extern pgthreadlock_t pg_g_threadlock;
|
||||
#define pgunlock_thread() ((void) 0)
|
||||
#endif
|
||||
|
||||
/* === in fe-auth.c === */
|
||||
#ifdef ENABLE_GSS
|
||||
extern void pg_GSS_error(char *mprefix, char *msg, int msglen,
|
||||
OM_uint32 maj_stat, OM_uint32 min_stat);
|
||||
#endif
|
||||
|
||||
/* === in fe-exec.c === */
|
||||
|
||||
extern void pqSetResultError(PGresult *res, const char *msg);
|
||||
|
Reference in New Issue
Block a user