Le backend d’une application web doit très souvent stocker et lire des données dans une base de données. Souvent cette base de données fonctionne avec SQL, et parfois il s’agit de MySQL.
Je vais présenter ci-dessous les 7 fonctions que j’utilise en langage C pour accéder à une base MySQL située sur la même machine que le script C de backend.
J’ai mis ces fonctions dans un fichier climb.h et climb.c, dans lesquels il y a des fonctions relatives à beaucoup d’autres sujets, mais aussi les fonctions relatives au dialogue avec MySQL.
La librairie CLIMB (C LIbrary for Modern Backends)
CLIMB est donc la librairie qui regroupe toutes les fonctions utiles perso que j’utilise.
Voici les fonctions utiles, déclarées dans climb.h :
// MySQL functions
#define _MYSQL
typedef struct CLIMB_MYSQL_RESULT_STRUCT CLIMB_MYSQL_RESULT_STRUCT;
typedef CLIMB_MYSQL_RESULT_STRUCT *CLIMB_MYSQL_DBRESULT;
#define CLIMB_MYSQL_DBHANDLER MYSQL *
#define CLIMB_MYSQL_DBSTMT MYSQL_STMT *
CLIMB_MYSQL_DBHANDLER climb_mysql_opendatabase(char *databasename, char *host, char *login, char *password);
int climb_mysql_ntuples(CLIMB_MYSQL_DBRESULT result);
void climb_mysql_clear_result(CLIMB_MYSQL_DBRESULT result);
void climb_mysql_closedatabase(CLIMB_MYSQL_DBHANDLER handler);
void climb_mysql_getvalue(CLIMB_MYSQL_DBRESULT result, int row, int col, char *value, int max);
CLIMB_MYSQL_DBRESULT climb_mysql_query_prepare(CLIMB_MYSQL_DBHANDLER handler, char *query);
CLIMB_MYSQL_DBRESULT climb_mysql_do_query(int nparams, ...);
climb_mysql_opendatabase : ouvre une connexion avec la base MySQL dont le nom est databasename, située sur la machine dont le nom DNS est host (donc localhost pour nous), dont l’utilisateur propriétaire s’appelle login, et son mot de passe password. Si elle réussit, cette fonction ramène un handler qui permet de poursuivre le dialogue.
climb_mysql_query_prepare : prépare une requête query (préparée donc) pour la base de données ouverte (handler).
climb_mysql_do_query : envoie une quantité de nparams paramètres à la requête préparée par climb_mysql_query_prepare, et exécute cette requête SQL. Cette fonction ramène un résultat result.
climb_mysql_ntuples : compte le nombre de lignes (enregistrements) résultats de l’exécution de la requête.
climb_mysql_getvalue : récupère dans la variable value (de taille max) l’enregistrement de ligne row et de colonne col de la requête dont le résultat a été mis dans result.
climb_mysql_clear_result : ferme la requête courante pour permettre de passer ensuite une nouvelle requête.
climb_mysql_closedatabase : ferme la connexion à la base ouverte (handler).
Voici le code source ce ces fonctions dans climb.c :
static MYSQL_STMT *climb_prepared_stmt = NULL;
/* Résultat matérialisé d'une requête préparée */
typedef struct {
int nrows;
int ncols;
char ***data;
} CLIMB_MYSQL_RESULTSET;
static void climb_mysql_rs_free_internal(CLIMB_MYSQL_RESULTSET *rs);
/* Structure interne derrière CLIMB_MYSQL_DBRESULT */
struct CLIMB_MYSQL_RESULT_STRUCT {
int type; /* 0 = classique (MYSQL_RES), 1 = préparé (CLIMB_MYSQL_RESULTSET) */
MYSQL_RES *mysql_res; /* mode classique */
CLIMB_MYSQL_RESULTSET *rs; /* mode préparé */
MYSQL_STMT *stmt; /* statement à libérer avec le résultat */
};
CLIMB_MYSQL_DBHANDLER climb_mysql_opendatabase(char *databasename, char *host, char *login, char *password)
{
MYSQL *handler;
handler=(MYSQL *)malloc(sizeof(MYSQL));
mysql_init(handler);
return(mysql_real_connect(handler, host, login, password, databasename, 0, NULL, 0));
}
int climb_mysql_ntuples(CLIMB_MYSQL_DBRESULT result)
{
if (result == NULL) return 0;
if (result->type == 0)
return((int)mysql_num_rows(result->mysql_res));
else
return result->rs->nrows;
}
void climb_mysql_clear_result(CLIMB_MYSQL_DBRESULT result)
{
if (result == NULL) return;
if (result->type == 0) {
if (result->mysql_res) mysql_free_result(result->mysql_res);
} else {
if (result->rs) climb_mysql_rs_free_internal(result->rs);
if (result->stmt) mysql_stmt_close(result->stmt);
}
free(result);
}
void climb_mysql_closedatabase(CLIMB_MYSQL_DBHANDLER handler)
{
mysql_close(handler);
free(handler);
}
void climb_mysql_getvalue(CLIMB_MYSQL_DBRESULT result, int row, int col, char *value, int max)
{
MYSQL_ROW rrow;
if (result == NULL) { value[0]=0; return; }
if (result->type == 0) {
mysql_data_seek(result->mysql_res, row);
rrow=mysql_fetch_row(result->mysql_res);
strncpy(value, rrow[col], max-1);
value[max-1]=0;
} else {
if (row < 0 || row >= result->rs->nrows || col < 0 || col >= result->rs->ncols) {
value[0]=0; return;
}
if (result->rs->data[row][col] == NULL) { value[0]=0; return; }
strncpy(value, result->rs->data[row][col], max-1);
value[max-1]=0;
}
}
/* ============================================================
* Libération interne d'un CLIMB_MYSQL_RESULTSET
* ============================================================ */
static void climb_mysql_rs_free_internal(CLIMB_MYSQL_RESULTSET *rs)
{
int i, j;
if (rs == NULL) return;
for (i = 0; i < rs->nrows; i++) {
for (j = 0; j < rs->ncols; j++)
if (rs->data[i][j]) free(rs->data[i][j]);
free(rs->data[i]);
}
free(rs->data);
free(rs);
}
/* ============================================================
* Requêtes préparées : climb_mysql_query_prepare + climb_mysql_do_query
* ============================================================ */
CLIMB_MYSQL_DBRESULT climb_mysql_query_prepare(CLIMB_MYSQL_DBHANDLER handler, char *query)
{
if (climb_prepared_stmt) {
mysql_stmt_close(climb_prepared_stmt);
climb_prepared_stmt = NULL;
}
climb_prepared_stmt = mysql_stmt_init(handler);
if (climb_prepared_stmt == NULL) return NULL;
if (mysql_stmt_prepare(climb_prepared_stmt, query, strlen(query)) != 0) {
mysql_stmt_close(climb_prepared_stmt);
climb_prepared_stmt = NULL;
return NULL;
}
/* Retourne un marqueur non-NULL pour signaler le succès */
return (CLIMB_MYSQL_DBRESULT)climb_prepared_stmt;
}
CLIMB_MYSQL_DBRESULT climb_mysql_do_query(int nparams, ...)
{
va_list ap;
int i;
MYSQL_BIND *bind;
unsigned long *lengths;
char **params;
CLIMB_MYSQL_RESULT_STRUCT *r;
MYSQL_RES *meta;
unsigned int ncols;
my_ulonglong nrows;
char **buffers;
bool *is_null;
if (climb_prepared_stmt == NULL) return NULL;
/* Récupérer les paramètres */
params = (char **)malloc(sizeof(char *) * (nparams > 0 ? nparams : 1));
va_start(ap, nparams);
for (i = 0; i < nparams; i++)
params[i] = va_arg(ap, char *);
va_end(ap);
/* Lier les paramètres */
bind = (MYSQL_BIND *)malloc(sizeof(MYSQL_BIND) * (nparams > 0 ? nparams : 1));
lengths = (unsigned long *)malloc(sizeof(unsigned long) * (nparams > 0 ? nparams : 1));
memset(bind, 0, sizeof(MYSQL_BIND) * (nparams > 0 ? nparams : 1));
for (i = 0; i < nparams; i++) {
lengths[i] = strlen(params[i]);
bind[i].buffer_type = MYSQL_TYPE_STRING;
bind[i].buffer = params[i];
bind[i].buffer_length = lengths[i];
bind[i].length = &lengths[i];
}
if (nparams > 0 && mysql_stmt_bind_param(climb_prepared_stmt, bind) != 0) {
free(bind); free(lengths); free(params);
mysql_stmt_close(climb_prepared_stmt);
climb_prepared_stmt = NULL;
return NULL;
}
if (mysql_stmt_execute(climb_prepared_stmt) != 0) {
free(bind); free(lengths); free(params);
mysql_stmt_close(climb_prepared_stmt);
climb_prepared_stmt = NULL;
return NULL;
}
free(bind); free(lengths); free(params);
/* Récupérer le résultat s'il y en a un */
meta = mysql_stmt_result_metadata(climb_prepared_stmt);
r = (CLIMB_MYSQL_RESULT_STRUCT *)malloc(sizeof(CLIMB_MYSQL_RESULT_STRUCT));
r->type = 1;
r->mysql_res = NULL;
r->stmt = climb_prepared_stmt;
climb_prepared_stmt = NULL;
if (meta == NULL) {
/* INSERT/UPDATE/DELETE : pas de résultat à matérialiser */
r->rs = (CLIMB_MYSQL_RESULTSET *)malloc(sizeof(CLIMB_MYSQL_RESULTSET));
r->rs->nrows = 0;
r->rs->ncols = 0;
r->rs->data = NULL;
return r;
}
ncols = mysql_num_fields(meta);
mysql_stmt_store_result(r->stmt);
nrows = mysql_stmt_num_rows(r->stmt);
CLIMB_MYSQL_RESULTSET *rs = (CLIMB_MYSQL_RESULTSET *)malloc(sizeof(CLIMB_MYSQL_RESULTSET));
rs->nrows = (int)nrows;
rs->ncols = (int)ncols;
rs->data = (char ***)malloc(sizeof(char **) * (nrows > 0 ? nrows : 1));
for (i = 0; i < (int)nrows; i++)
rs->data[i] = (char **)malloc(sizeof(char *) * ncols);
buffers = (char **)malloc(sizeof(char *) * ncols);
lengths = (unsigned long *)malloc(sizeof(unsigned long) * ncols);
is_null = (bool *)malloc(sizeof(bool) * ncols);
MYSQL_BIND *rbind = (MYSQL_BIND *)malloc(sizeof(MYSQL_BIND) * ncols);
memset(rbind, 0, sizeof(MYSQL_BIND) * ncols);
for (i = 0; i < (int)ncols; i++) {
buffers[i] = (char *)malloc(20000);
rbind[i].buffer_type = MYSQL_TYPE_STRING;
rbind[i].buffer = buffers[i];
rbind[i].buffer_length = 20000;
rbind[i].length = &lengths[i];
rbind[i].is_null = &is_null[i];
}
mysql_stmt_bind_result(r->stmt, rbind);
for (i = 0; i < (int)nrows; i++) {
int ret = mysql_stmt_fetch(r->stmt);
int j;
if (ret != 0 && ret != MYSQL_DATA_TRUNCATED) break;
for (j = 0; j < (int)ncols; j++) {
if (is_null[j]) {
rs->data[i][j] = NULL;
} else {
rs->data[i][j] = (char *)malloc(lengths[j] + 1);
memcpy(rs->data[i][j], buffers[j], lengths[j]);
rs->data[i][j][lengths[j]] = 0;
}
}
}
for (i = 0; i < (int)ncols; i++) free(buffers[i]);
free(buffers); free(lengths); free(is_null); free(rbind);
mysql_free_result(meta);
r->rs = rs;
return r;
}
Requêtes SQL directes vs requêtes SQL préparées
La logique voudrait qu’on utilise ce genre de librairie selon 4 étapes :
- Ouverture de la connexion à la base
- Envoi d’une requête à la base
- Exploitation du résultat de la requête
- Fermeture de la connexion à la base
Cette méthode directe est simple et opérationnelle. Mais elle a un gros défaut : elle n’est pas sûre vis à vis de la cybersécurité. En effet, elle n’empêche pas l’utilisateur d’envoyer des paramètres dans lesquels il met subrepticement des instructions SQL. C’est ce qu’on appelle une injection SQL.
Ce n’est donc pas la bonne méthode. La bonne méthode consiste à séparer la requête de ses paramètres : on prépare une requête sans paramètres (on remplace les paramètres par le caractère ?), et ensuite on exécute la requête en lui passant les paramètres après avoir préparé la requête.
Ce mécanisme de requêtes préparées est un mécanisme offert par MySQL : il garantit que jamais un paramètre passé ne pourra être considéré comme une requête, ce qui empêche de facto toute tentative d’injection SQL.
Il y a cependant un bémol : la préparation des requêtes ne peut pas être faite une fois pour toute dans la base de données, elle est éphémère à chaque connexion. Elle doit donc être faite à chaque connexion.
Les fonctions présentées dans climb utilisent les requêtes préparées.
Exemple d’utilisation
Comme on peut le voir ci-dessous, l’utilisation de ces fonctions est très facile :
int main()
{
CLIMB_MYSQL_DBHANDLER handler;
CLIMB_MYSQL_DBRESULT result;
char query[1000],resultat[500];
if((handler=climb_mysql_opendatabase("ma_base","localhost","mon_utilisateur","mon_mot_de_passe"))==NULL) affiche_page_d_erreur_et_exit();
climb_mysql_query_prepare(handler,"select * from param where admin_name=? and admin_password_hash=?"); // on remplace les paramètres par des ? dans la requête SQL qu'on prépare
result=climb_mysql_do_query(2,"nom","password_hash"); // 2 paramètres. nom remplacera le premier ? dans la requête, password_hash remplacera le deuxième ? dans la requête. Puis la requête est exécutée
if(climb_mysql_ntuples(result)>0) // Si la requête ramène des résultats
{
climb_mysql_getvalue(result,0,0,resultat,500); // On récupère l'enregistrement ligne 0, colonne 0 dans la variable resultat
}
climb_mysql_clear_result(result); // On clos la requête s'il y a besoin de préparer une autre requête
climb_mysql_closedatabase(handler); // Fermeture de la connexion à la base MySQL
}
La librairie climb complète (on verra par la suite les autres fonctions) est téléchargeable ici.
