diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 14:11:00 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 14:11:00 +0000 |
commit | af754e596a8dbb05ed8580c342e7fe02e08b28e0 (patch) | |
tree | b2f334c2b55ede42081aa6710a72da784547d8ea /src/modules/rlm_sql/drivers/rlm_sql_postgresql/rlm_sql_postgresql.c | |
parent | Initial commit. (diff) | |
download | freeradius-upstream/3.2.3+dfsg.tar.xz freeradius-upstream/3.2.3+dfsg.zip |
Adding upstream version 3.2.3+dfsg.upstream/3.2.3+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/modules/rlm_sql/drivers/rlm_sql_postgresql/rlm_sql_postgresql.c')
-rw-r--r-- | src/modules/rlm_sql/drivers/rlm_sql_postgresql/rlm_sql_postgresql.c | 610 |
1 files changed, 610 insertions, 0 deletions
diff --git a/src/modules/rlm_sql/drivers/rlm_sql_postgresql/rlm_sql_postgresql.c b/src/modules/rlm_sql/drivers/rlm_sql_postgresql/rlm_sql_postgresql.c new file mode 100644 index 0000000..c88d8b0 --- /dev/null +++ b/src/modules/rlm_sql/drivers/rlm_sql_postgresql/rlm_sql_postgresql.c @@ -0,0 +1,610 @@ +/* + * sql_postgresql.c Postgresql rlm_sql driver + * + * Version: $Id$ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Copyright 2000,2006 The FreeRADIUS server project + * Copyright 2000 Mike Machado <mike@innercite.com> + * Copyright 2000 Alan DeKok <aland@ox.org> + */ + +/* + * April 2001: + * + * Use blocking queries and delete unused functions. In + * rlm_sql_postgresql replace all functions that are not really used + * with the not_implemented function. + * + * Add a new field to the rlm_sql_postgres_conn_t struct to store the + * number of rows affected by a query because the sql module calls + * finish_query before it retrieves the number of affected rows from the + * driver + * + * Bernhard Herzog <bh@intevation.de> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#include <sys/stat.h> + +#include <libpq-fe.h> +#include <postgres_ext.h> + +#include "config.h" +#include "rlm_sql.h" +#include "sql_postgresql.h" + +#ifndef NAMEDATALEN +# define NAMEDATALEN 64 +#endif + +typedef struct rlm_sql_postgres_config { + char const *db_string; + bool send_application_name; + char const *application_name; +} rlm_sql_postgres_config_t; + +typedef struct rlm_sql_postgres_conn { + PGconn *db; + PGresult *result; + int cur_row; + int num_fields; + int affected_rows; + char **row; +} rlm_sql_postgres_conn_t; + +static const CONF_PARSER driver_config[] = { + { "send_application_name", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_sql_postgres_config_t, send_application_name), "no" }, + { "application_name", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_sql_postgres_config_t, application_name), NULL }, + CONF_PARSER_TERMINATOR +}; + +static int mod_instantiate(CONF_SECTION *conf, rlm_sql_config_t *config) +{ +#if defined(HAVE_OPENSSL_CRYPTO_H) && (defined(HAVE_PQINITOPENSSL) || defined(HAVE_PQINITSSL)) + static bool ssl_init = false; +#endif + + rlm_sql_postgres_config_t *driver; + char buffer[NAMEDATALEN]; + char const *application_name = NULL; + char *db_string; + +#if defined(HAVE_OPENSSL_CRYPTO_H) && (defined(HAVE_PQINITOPENSSL) || defined(HAVE_PQINITSSL)) + if (!ssl_init) { +# ifdef HAVE_PQINITOPENSSL + PQinitOpenSSL(0, 0); +# else + PQinitSSL(0); +# endif + ssl_init = true; + } +#endif + + MEM(driver = config->driver = talloc_zero(config, rlm_sql_postgres_config_t)); + if (cf_section_parse(conf, driver, driver_config) < 0) { + return -1; + } + + /* + * Allow the user to set their own, or disable it + */ + if (driver->send_application_name) { + if (driver->application_name && *driver->application_name) { + application_name = driver->application_name; + } else { + CONF_SECTION *cs; + char const *name; + + cs = cf_item_parent(cf_section_to_item(conf)); + + name = cf_section_name2(cs); + if (!name) name = cf_section_name1(cs); + + snprintf(buffer, sizeof(buffer), + "FreeRADIUS " RADIUSD_VERSION_STRING " - %s (%s)", main_config.name, name); + + application_name = buffer; + } + } + + /* + * Old style database name + * + * Append options if they were set in the config + */ + if (!strchr(config->sql_db, '=')) { + db_string = talloc_typed_asprintf(driver, "dbname='%s'", config->sql_db); + + if (config->sql_server[0] != '\0') { + db_string = talloc_asprintf_append(db_string, " host='%s'", config->sql_server); + } + + if (config->sql_port) { + db_string = talloc_asprintf_append(db_string, " port=%i", config->sql_port); + } + + if (config->sql_login[0] != '\0') { + db_string = talloc_asprintf_append(db_string, " user='%s'", config->sql_login); + } + + if (config->sql_password[0] != '\0') { + db_string = talloc_asprintf_append(db_string, " password='%s'", config->sql_password); + } + + if (config->query_timeout) { + db_string = talloc_asprintf_append(db_string, " connect_timeout=%d", config->query_timeout); + } + + if (driver->send_application_name) { + db_string = talloc_asprintf_append(db_string, " application_name='%s'", application_name); + } + + /* + * New style parameter string + * + * Only append options when not already present + */ + } else { + db_string = talloc_typed_strdup(driver, config->sql_db); + + if ((config->sql_server[0] != '\0') && !strstr(db_string, "host=")) { + db_string = talloc_asprintf_append(db_string, " host='%s'", config->sql_server); + } + + if (config->sql_port && !strstr(db_string, "port=")) { + db_string = talloc_asprintf_append(db_string, " port=%i", config->sql_port); + } + + if ((config->sql_login[0] != '\0') && !strstr(db_string, "user=")) { + db_string = talloc_asprintf_append(db_string, " user='%s'", config->sql_login); + } + + if ((config->sql_password[0] != '\0') && !strstr(db_string, "password=")) { + db_string = talloc_asprintf_append(db_string, " password='%s'", config->sql_password); + } + + if ((config->query_timeout) && !strstr(db_string, "connect_timeout=")) { + db_string = talloc_asprintf_append(db_string, " connect_timeout=%d", config->query_timeout); + } + + if (driver->send_application_name && !strstr(db_string, "application_name=")) { + db_string = talloc_asprintf_append(db_string, " application_name='%s'", application_name); + } + } + driver->db_string = db_string; + + return 0; +} + +/** Return the number of affected rows of the result as an int instead of the string that postgresql provides + * + */ +static int affected_rows(PGresult * result) +{ + return atoi(PQcmdTuples(result)); +} + +/** Free the row of the current result that's stored in the conn struct + * + */ +static void free_result_row(rlm_sql_postgres_conn_t *conn) +{ + TALLOC_FREE(conn->row); + conn->num_fields = 0; +} + +#if defined(PG_DIAG_SQLSTATE) && defined(PG_DIAG_MESSAGE_PRIMARY) +static sql_rcode_t sql_classify_error(PGresult const *result) +{ + int i; + + char *errorcode; + char *errormsg; + + /* + * Check the error code to see if we should reconnect or not + * Error Code table taken from: + * http://www.postgresql.org/docs/8.1/interactive/errcodes-appendix.html + */ + errorcode = PQresultErrorField(result, PG_DIAG_SQLSTATE); + errormsg = PQresultErrorField(result, PG_DIAG_MESSAGE_PRIMARY); + if (!errorcode) { + ERROR("rlm_sql_postgresql: Error occurred, but unable to retrieve error code"); + return RLM_SQL_ERROR; + } + + /* SUCCESSFUL COMPLETION */ + if (strcmp("00000", errorcode) == 0) { + return RLM_SQL_OK; + } + + /* WARNING */ + if (strcmp("01000", errorcode) == 0) { + WARN("%s", errormsg); + return RLM_SQL_OK; + } + + /* UNIQUE VIOLATION */ + if (strcmp("23505", errorcode) == 0) { + return RLM_SQL_ALT_QUERY; + } + + /* others */ + for (i = 0; errorcodes[i].errorcode != NULL; i++) { + if (strcmp(errorcodes[i].errorcode, errorcode) == 0) { + ERROR("rlm_sql_postgresql: %s: %s", errorcode, errorcodes[i].meaning); + + return (errorcodes[i].reconnect == true) ? + RLM_SQL_RECONNECT : + RLM_SQL_ERROR; + } + } + + ERROR("rlm_sql_postgresql: Can't classify: %s", errorcode); + return RLM_SQL_ERROR; +} +# else +static sql_rcode_t sql_classify_error(UNUSED PGresult const *result) +{ + ERROR("rlm_sql_postgresql: Error occurred, no more information available, rebuild with newer libpq"); + return RLM_SQL_ERROR; +} +#endif + +static int _sql_socket_destructor(rlm_sql_postgres_conn_t *conn) +{ + DEBUG2("rlm_sql_postgresql: Socket destructor called, closing socket"); + + if (!conn->db) return 0; + + /* PQfinish also frees the memory used by the PGconn structure */ + PQfinish(conn->db); + + return 0; +} + +static int CC_HINT(nonnull) sql_socket_init(rlm_sql_handle_t *handle, rlm_sql_config_t *config) +{ + rlm_sql_postgres_config_t *driver = config->driver; + rlm_sql_postgres_conn_t *conn; + + MEM(conn = handle->conn = talloc_zero(handle, rlm_sql_postgres_conn_t)); + talloc_set_destructor(conn, _sql_socket_destructor); + + DEBUG2("rlm_sql_postgresql: Connecting using parameters: %s", driver->db_string); + conn->db = PQconnectdb(driver->db_string); + if (!conn->db) { + ERROR("rlm_sql_postgresql: Connection failed: Out of memory"); + return -1; + } + if (PQstatus(conn->db) != CONNECTION_OK) { + ERROR("rlm_sql_postgresql: Connection failed: %s", PQerrorMessage(conn->db)); + PQfinish(conn->db); + conn->db = NULL; + return -1; + } + + DEBUG2("Connected to database '%s' on '%s' server version %i, protocol version %i, backend PID %i ", + PQdb(conn->db), PQhost(conn->db), PQserverVersion(conn->db), PQprotocolVersion(conn->db), + PQbackendPID(conn->db)); + + return 0; +} + +static CC_HINT(nonnull) sql_rcode_t sql_query(rlm_sql_handle_t *handle, UNUSED rlm_sql_config_t *config, + char const *query) +{ + rlm_sql_postgres_conn_t *conn = handle->conn; + struct timeval start; + int sockfd; + ExecStatusType status; + int numfields = 0; + PGresult *tmp_result; + + if (!conn->db) { + ERROR("rlm_sql_postgresql: Socket not connected"); + return RLM_SQL_RECONNECT; + } + + sockfd = PQsocket(conn->db); + if (sockfd < 0) { + ERROR("rlm_sql_postgresql: Unable to obtain socket: %s", PQerrorMessage(conn->db)); + return RLM_SQL_RECONNECT; + } + + if (!PQsendQuery(conn->db, query)) { + ERROR("rlm_sql_postgresql: Failed to send query: %s", PQerrorMessage(conn->db)); + return RLM_SQL_RECONNECT; + } + + /* + * We try to avoid blocking by waiting until the driver indicates that + * the result is ready or our timeout expires + */ + gettimeofday(&start, NULL); + while (PQisBusy(conn->db)) { + int r; + fd_set read_fd; + struct timeval when, elapsed, wake; + + FD_ZERO(&read_fd); + FD_SET(sockfd, &read_fd); + + if (config->query_timeout) { + gettimeofday(&when, NULL); + rad_tv_sub(&when, &start, &elapsed); + if (elapsed.tv_sec >= config->query_timeout) goto too_long; + + when.tv_sec = config->query_timeout; + when.tv_usec = 0; + rad_tv_sub(&when, &elapsed, &wake); + } + + r = select(sockfd + 1, &read_fd, NULL, NULL, config->query_timeout ? &wake : NULL); + if (r == 0) { + too_long: + ERROR("rlm_sql_postgresql: Socket read timeout after %d seconds", config->query_timeout); + return RLM_SQL_RECONNECT; + } + if (r < 0) { + if (errno == EINTR) continue; + ERROR("rlm_sql_postgresql: Failed in select: %s", fr_syserror(errno)); + return RLM_SQL_RECONNECT; + } + if (!PQconsumeInput(conn->db)) { + ERROR("rlm_sql_postgresql: Failed reading input: %s", PQerrorMessage(conn->db)); + return RLM_SQL_RECONNECT; + } + } + + /* + * Returns a PGresult pointer or possibly a null pointer. + * A non-null pointer will generally be returned except in + * out-of-memory conditions or serious errors such as inability + * to send the command to the server. If a null pointer is + * returned, it should be treated like a PGRES_FATAL_ERROR + * result. + */ + conn->result = PQgetResult(conn->db); + + /* Discard results for appended queries */ + while ((tmp_result = PQgetResult(conn->db)) != NULL) + PQclear(tmp_result); + + /* + * As this error COULD be a connection error OR an out-of-memory + * condition return value WILL be wrong SOME of the time + * regardless! Pick your poison... + */ + if (!conn->result) { + ERROR("rlm_sql_postgresql: Failed getting query result: %s", PQerrorMessage(conn->db)); + return RLM_SQL_RECONNECT; + } + + status = PQresultStatus(conn->result); + DEBUG("rlm_sql_postgresql: Status: %s", PQresStatus(status)); + + switch (status){ + /* + * Successful completion of a command returning no data. + */ + case PGRES_COMMAND_OK: + /* + * Affected_rows function only returns the number of affected rows of a command + * returning no data... + */ + conn->affected_rows = affected_rows(conn->result); + DEBUG("rlm_sql_postgresql: query affected rows = %i", conn->affected_rows); + return RLM_SQL_OK; + /* + * Successful completion of a command returning data (such as a SELECT or SHOW). + */ +#ifdef HAVE_PGRES_SINGLE_TUPLE + case PGRES_SINGLE_TUPLE: +#endif + case PGRES_TUPLES_OK: + conn->cur_row = 0; + conn->affected_rows = PQntuples(conn->result); + numfields = PQnfields(conn->result); /*Check row storing functions..*/ + DEBUG("rlm_sql_postgresql: query affected rows = %i , fields = %i", conn->affected_rows, numfields); + return RLM_SQL_OK; + +#ifdef HAVE_PGRES_COPY_BOTH + case PGRES_COPY_BOTH: +#endif + case PGRES_COPY_OUT: + case PGRES_COPY_IN: + DEBUG("rlm_sql_postgresql: Data transfer started"); + return RLM_SQL_OK; + + /* + * Weird.. this shouldn't happen. + */ + case PGRES_EMPTY_QUERY: + ERROR("rlm_sql_postgresql: Empty query"); + return RLM_SQL_QUERY_INVALID; + + /* + * The server's response was not understood. + */ + case PGRES_BAD_RESPONSE: + ERROR("rlm_sql_postgresql: Bad Response From Server"); + return RLM_SQL_RECONNECT; + + + case PGRES_NONFATAL_ERROR: + case PGRES_FATAL_ERROR: + return sql_classify_error(conn->result); + +#ifdef HAVE_PGRES_PIPELINE_SYNC + case PGRES_PIPELINE_SYNC: + case PGRES_PIPELINE_ABORTED: + ERROR("rlm_sql_postgresql: Pipeline flagged as aborted"); + return RLM_SQL_ERROR; +#endif + } + + return RLM_SQL_ERROR; +} + +static sql_rcode_t sql_select_query(rlm_sql_handle_t * handle, rlm_sql_config_t *config, char const *query) +{ + return sql_query(handle, config, query); +} + +static sql_rcode_t sql_fetch_row(rlm_sql_handle_t *handle, UNUSED rlm_sql_config_t *config) +{ + + int records, i, len; + rlm_sql_postgres_conn_t *conn = handle->conn; + + handle->row = NULL; + + if (conn->cur_row >= PQntuples(conn->result)) return RLM_SQL_NO_MORE_ROWS; + + free_result_row(conn); + + records = PQnfields(conn->result); + conn->num_fields = records; + + if ((PQntuples(conn->result) > 0) && (records > 0)) { + conn->row = talloc_zero_array(conn, char *, records + 1); + for (i = 0; i < records; i++) { + len = PQgetlength(conn->result, conn->cur_row, i); + conn->row[i] = talloc_array(conn->row, char, len + 1); + strlcpy(conn->row[i], PQgetvalue(conn->result, conn->cur_row, i), len + 1); + } + conn->cur_row++; + handle->row = conn->row; + } else { + return RLM_SQL_NO_MORE_ROWS; + } + + return RLM_SQL_OK; +} + +static int sql_num_fields(rlm_sql_handle_t * handle, UNUSED rlm_sql_config_t *config) +{ + rlm_sql_postgres_conn_t *conn = handle->conn; + + conn->affected_rows = PQntuples(conn->result); + if (conn->result) + return PQnfields(conn->result); + + return 0; +} + +static sql_rcode_t sql_free_result(rlm_sql_handle_t * handle, UNUSED rlm_sql_config_t *config) +{ + rlm_sql_postgres_conn_t *conn = handle->conn; + + if (conn->result != NULL) { + PQclear(conn->result); + conn->result = NULL; + } + + free_result_row(conn); + + return 0; +} + +/** Retrieves any errors associated with the connection handle + * + * @note Caller will free any memory allocated in ctx. + * + * @param ctx to allocate temporary error buffers in. + * @param out Array of sql_log_entrys to fill. + * @param outlen Length of out array. + * @param handle rlm_sql connection handle. + * @param config rlm_sql config. + * @return number of errors written to the sql_log_entry array. + */ +static size_t sql_error(TALLOC_CTX *ctx, sql_log_entry_t out[], size_t outlen, + rlm_sql_handle_t *handle, UNUSED rlm_sql_config_t *config) +{ + rlm_sql_postgres_conn_t *conn = handle->conn; + char const *p, *q; + size_t i = 0; + + rad_assert(outlen > 0); + + p = PQerrorMessage(conn->db); + while ((q = strchr(p, '\n'))) { + out[i].type = L_ERR; + out[i].msg = talloc_asprintf(ctx, "%.*s", (int) (q - p), p); + p = q + 1; + if (++i == outlen) return outlen; + } + if (*p != '\0') { + out[i].type = L_ERR; + out[i].msg = p; + i++; + } + + return i; +} + +static int sql_affected_rows(rlm_sql_handle_t * handle, UNUSED rlm_sql_config_t *config) +{ + rlm_sql_postgres_conn_t *conn = handle->conn; + + return conn->affected_rows; +} + +static size_t sql_escape_func(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, void *arg) +{ + size_t inlen, ret; + rlm_sql_handle_t *handle = talloc_get_type_abort(arg, rlm_sql_handle_t); + rlm_sql_postgres_conn_t *conn = handle->conn; + int err; + + /* Check for potential buffer overflow */ + inlen = strlen(in); + if ((inlen * 2 + 1) > outlen) return 0; + /* Prevent integer overflow */ + if ((inlen * 2 + 1) <= inlen) return 0; + + ret = PQescapeStringConn(conn->db, out, in, inlen, &err); + if (err) { + REDEBUG("Error escaping string \"%s\": %s", in, PQerrorMessage(conn->db)); + return 0; + } + + return ret; +} + +/* Exported to rlm_sql */ +extern rlm_sql_module_t rlm_sql_postgresql; +rlm_sql_module_t rlm_sql_postgresql = { + .name = "rlm_sql_postgresql", + .flags = RLM_SQL_RCODE_FLAGS_ALT_QUERY, + .mod_instantiate = mod_instantiate, + .sql_socket_init = sql_socket_init, + .sql_query = sql_query, + .sql_select_query = sql_select_query, + .sql_num_fields = sql_num_fields, + .sql_fetch_row = sql_fetch_row, + .sql_error = sql_error, + .sql_finish_query = sql_free_result, + .sql_finish_select_query = sql_free_result, + .sql_affected_rows = sql_affected_rows, + .sql_escape_func = sql_escape_func +}; |