auth_htpasswd.c 11.9 KB
Newer Older
1 2 3 4 5
/* Icecast
 *
 * This program is distributed under the GNU General Public License, version 2.
 * A copy of this license is included with this source.
 *
6
 * Copyright 2000-2004, Jack Moffitt <jack@xiph.org,
7 8 9 10
 *                      Michael Smith <msmith@xiph.org>,
 *                      oddsock <oddsock@xiph.org>,
 *                      Karl Heyes <karl@xiph.org>
 *                      and others (see AUTHORS for details).
Philipp Schafft's avatar
Philipp Schafft committed
11
 * Copyright 2012-2014, Philipp "ph3-der-loewe" Schafft <lion@lion.leolix.org>,
12 13
 */

14
/**
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
 * Client authentication functions
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "auth.h"
#include "source.h"
#include "client.h"
#include "cfgfile.h"
33
#include "common/httpp/httpp.h"
34 35 36 37
#include "md5.h"

#include "logging.h"
#define CATMODULE "auth_htpasswd"
38 39 40 41

#ifdef WIN32
#define snprintf _snprintf
#endif
42 43 44 45 46 47

static auth_result htpasswd_adduser (auth_t *auth, const char *username, const char *password);
static auth_result htpasswd_deleteuser(auth_t *auth, const char *username);
static auth_result htpasswd_userlist(auth_t *auth, xmlNodePtr srcnode);
static int _free_user (void *key);

Marvin Scholz's avatar
Marvin Scholz committed
48
typedef struct {
49 50 51 52 53 54 55 56 57 58 59
    char *name;
    char *pass;
} htpasswd_user;

typedef struct {
    char *filename;
    rwlock_t file_rwlock;
    avl_tree *users;
    time_t mtime;
} htpasswd_auth_state;

Marvin Scholz's avatar
Marvin Scholz committed
60 61
static void htpasswd_clear(auth_t *self)
{
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
    htpasswd_auth_state *state = self->state;
    free(state->filename);
    if (state->users)
        avl_tree_free (state->users, _free_user);
    thread_rwlock_destroy(&state->file_rwlock);
    free(state);
}


/* md5 hash */
static char *get_hash(const char *data, int len)
{
    struct MD5Context context;
    unsigned char digest[16];

    MD5Init(&context);

79
    MD5Update(&context, (const unsigned char *)data, len);
80 81 82 83 84 85 86

    MD5Final(digest, &context);

    return util_bin_to_hex(digest, 16);
}


Marvin Scholz's avatar
Marvin Scholz committed
87
static int compare_users(void *arg, void *a, void *b)
88 89 90 91
{
    htpasswd_user *user1 = (htpasswd_user *)a;
    htpasswd_user *user2 = (htpasswd_user *)b;

92 93
    (void)arg;

94 95 96 97
    return strcmp (user1->name, user2->name);
}


Marvin Scholz's avatar
Marvin Scholz committed
98
static int _free_user(void *key)
99 100 101 102 103 104 105 106 107
{
    htpasswd_user *user = (htpasswd_user *)key;

    free (user->name); /* ->pass is part of same buffer */
    free (user);
    return 1;
}


Marvin Scholz's avatar
Marvin Scholz committed
108
static void htpasswd_recheckfile(htpasswd_auth_state *htpasswd)
109 110 111 112 113 114 115 116
{
    FILE *passwdfile;
    avl_tree *new_users;
    int num = 0;
    struct stat file_stat;
    char *sep;
    char line [MAX_LINE_LEN];

117 118
    if (htpasswd->filename == NULL)
        return;
Marvin Scholz's avatar
Marvin Scholz committed
119
    if (stat (htpasswd->filename, &file_stat) < 0) {
120
        ICECAST_LOG_WARN("failed to check status of %s", htpasswd->filename);
121 122 123 124 125 126 127

        /* Create a dummy users tree for things to use later */
        thread_rwlock_wlock (&htpasswd->file_rwlock);
        if(!htpasswd->users)
            htpasswd->users = avl_tree_new(compare_users, NULL);
        thread_rwlock_unlock (&htpasswd->file_rwlock);

128 129
        return;
    }
130

Marvin Scholz's avatar
Marvin Scholz committed
131
    if (file_stat.st_mtime == htpasswd->mtime) {
132 133 134
        /* common case, no update to file */
        return;
    }
135
    ICECAST_LOG_INFO("re-reading htpasswd file \"%s\"", htpasswd->filename);
136
    passwdfile = fopen (htpasswd->filename, "rb");
Marvin Scholz's avatar
Marvin Scholz committed
137
    if (passwdfile == NULL) {
138
        ICECAST_LOG_WARN("Failed to open authentication database \"%s\": %s",
139 140 141 142 143 144 145
                htpasswd->filename, strerror(errno));
        return;
    }
    htpasswd->mtime = file_stat.st_mtime;

    new_users = avl_tree_new (compare_users, NULL);

Marvin Scholz's avatar
Marvin Scholz committed
146
    while (get_line(passwdfile, line, MAX_LINE_LEN)) {
147 148 149 150
        int len;
        htpasswd_user *entry;

        num++;
151
        if (!line[0] || line[0] == '#')
152 153 154
            continue;

        sep = strrchr (line, ':');
Marvin Scholz's avatar
Marvin Scholz committed
155
        if (sep == NULL) {
156
            ICECAST_LOG_WARN("No separator on line %d (%s)", num, htpasswd->filename);
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
            continue;
        }
        entry = calloc (1, sizeof (htpasswd_user));
        len = strlen (line) + 1;
        entry->name = malloc (len);
        *sep = 0;
        memcpy (entry->name, line, len);
        entry->pass = entry->name + (sep-line) + 1;
        avl_insert (new_users, entry);
    }
    fclose (passwdfile);

    thread_rwlock_wlock (&htpasswd->file_rwlock);
    if (htpasswd->users)
        avl_tree_free (htpasswd->users, _free_user);
    htpasswd->users = new_users;
    thread_rwlock_unlock (&htpasswd->file_rwlock);
}


static auth_result htpasswd_auth (auth_client *auth_user)
{
    auth_t *auth = auth_user->client->auth;
    htpasswd_auth_state *htpasswd = auth->state;
    client_t *client = auth_user->client;
    htpasswd_user entry;
    void *result;

Philipp Schafft's avatar
Philipp Schafft committed
185 186
    if (!client->username || !client->password)
        return AUTH_NOMATCH;
187

Philipp Schafft's avatar
Philipp Schafft committed
188
    if (!htpasswd->filename) {
189
        ICECAST_LOG_ERROR("No filename given in options for authenticator.");
Philipp Schafft's avatar
Philipp Schafft committed
190
        return AUTH_NOMATCH;
191
    }
192 193
    htpasswd_recheckfile (htpasswd);

194
    if (htpasswd->users == NULL) {
195 196 197 198
        ICECAST_LOG_ERROR("No user list.");
        return AUTH_NOMATCH;
    }

199 200
    thread_rwlock_rlock (&htpasswd->file_rwlock);
    entry.name = client->username;
Marvin Scholz's avatar
Marvin Scholz committed
201
    if (avl_get_by_key (htpasswd->users, &entry, &result) == 0) {
202 203 204 205 206
        htpasswd_user *found = result;
        char *hashed_pw;

        thread_rwlock_unlock (&htpasswd->file_rwlock);
        hashed_pw = get_hash (client->password, strlen (client->password));
Marvin Scholz's avatar
Marvin Scholz committed
207
        if (strcmp (found->pass, hashed_pw) == 0) {
208 209 210 211
            free (hashed_pw);
            return AUTH_OK;
        }
        free (hashed_pw);
212
        ICECAST_LOG_DEBUG("incorrect password for client with username: %s", client->username);
213 214
        return AUTH_FAILED;
    }
215
    ICECAST_LOG_DEBUG("no such username: %s", client->username);
216
    thread_rwlock_unlock (&htpasswd->file_rwlock);
Philipp Schafft's avatar
Philipp Schafft committed
217
    return AUTH_NOMATCH;
218 219 220 221 222 223
}

int  auth_get_htpasswd_auth (auth_t *authenticator, config_options_t *options)
{
    htpasswd_auth_state *state;

Philipp Schafft's avatar
Philipp Schafft committed
224
    authenticator->authenticate_client = htpasswd_auth;
225 226 227 228
    authenticator->free = htpasswd_clear;
    authenticator->adduser = htpasswd_adduser;
    authenticator->deleteuser = htpasswd_deleteuser;
    authenticator->listuser = htpasswd_userlist;
229
    authenticator->immediate = 1;
230 231 232 233

    state = calloc(1, sizeof(htpasswd_auth_state));

    while(options) {
Marvin Scholz's avatar
Marvin Scholz committed
234
        if(!strcmp(options->name, "filename")) {
235
            free (state->filename);
236
            state->filename = strdup(options->value);
237
        }
238 239 240
        options = options->next;
    }

Marvin Scholz's avatar
Marvin Scholz committed
241
    if (state->filename) {
242
        ICECAST_LOG_INFO("Configured htpasswd authentication using password file \"%s\"",
243
                state->filename);
Marvin Scholz's avatar
Marvin Scholz committed
244
    } else {
245
        ICECAST_LOG_ERROR("No filename given in options for authenticator.");
Marvin Scholz's avatar
Marvin Scholz committed
246
    }
247 248 249 250

    authenticator->state = state;

    thread_rwlock_create(&state->file_rwlock);
251
    htpasswd_recheckfile(state);
252 253 254 255 256 257 258 259 260 261 262 263 264

    return 0;
}


static auth_result htpasswd_adduser (auth_t *auth, const char *username, const char *password)
{
    FILE *passwdfile;
    char *hashed_password = NULL;
    htpasswd_auth_state *state = auth->state;
    htpasswd_user entry;
    void *result;

265
    if (state->filename == NULL) {
266 267 268 269
        ICECAST_LOG_ERROR("No filename given in options for authenticator.");
        return AUTH_FAILED;
    }

270 271
    htpasswd_recheckfile (state);

272
    if (state->users == NULL) {
273 274 275 276
        ICECAST_LOG_ERROR("No user list.");
        return AUTH_FAILED;
    }

277 278 279
    thread_rwlock_wlock (&state->file_rwlock);

    entry.name = (char*)username;
Marvin Scholz's avatar
Marvin Scholz committed
280
    if (avl_get_by_key (state->users, &entry, &result) == 0) {
281 282 283 284 285 286
        thread_rwlock_unlock (&state->file_rwlock);
        return AUTH_USEREXISTS;
    }

    passwdfile = fopen(state->filename, "ab");

Marvin Scholz's avatar
Marvin Scholz committed
287
    if (passwdfile == NULL) {
288
        thread_rwlock_unlock (&state->file_rwlock);
289
        ICECAST_LOG_WARN("Failed to open authentication database \"%s\": %s",
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
                state->filename, strerror(errno));
        return AUTH_FAILED;
    }

    hashed_password = get_hash(password, strlen(password));
    if (hashed_password) {
        fprintf(passwdfile, "%s:%s\n", username, hashed_password);
        free(hashed_password);
    }

    fclose(passwdfile);
    thread_rwlock_unlock (&state->file_rwlock);

    return AUTH_USERADDED;
}


static auth_result htpasswd_deleteuser(auth_t *auth, const char *username)
{
    FILE *passwdfile;
    FILE *tmp_passwdfile;
    htpasswd_auth_state *state;
    char line[MAX_LINE_LEN];
    char *sep;
    char *tmpfile = NULL;
    int tmpfile_len = 0;
    struct stat file_info;

    state = auth->state;
319 320 321 322 323 324

    if (!state->filename) {
        ICECAST_LOG_ERROR("No filename given in options for authenticator.");
        return AUTH_FAILED;
    }

325
    if (state->users == NULL) {
326 327 328 329
        ICECAST_LOG_ERROR("No user list.");
        return AUTH_FAILED;
    }

330 331 332 333
    thread_rwlock_wlock (&state->file_rwlock);
    passwdfile = fopen(state->filename, "rb");

    if(passwdfile == NULL) {
334
        ICECAST_LOG_WARN("Failed to open authentication database \"%s\": %s",
335 336 337 338 339 340 341
                state->filename, strerror(errno));
        thread_rwlock_unlock (&state->file_rwlock);
        return AUTH_FAILED;
    }
    tmpfile_len = strlen(state->filename) + 6;
    tmpfile = calloc(1, tmpfile_len);
    snprintf (tmpfile, tmpfile_len, "%s.tmp", state->filename);
Marvin Scholz's avatar
Marvin Scholz committed
342
    if (stat (tmpfile, &file_info) == 0) {
343
        ICECAST_LOG_WARN("temp file \"%s\" exists, rejecting operation", tmpfile);
344 345 346 347 348 349 350 351 352
        free (tmpfile);
        fclose (passwdfile);
        thread_rwlock_unlock (&state->file_rwlock);
        return AUTH_FAILED;
    }

    tmp_passwdfile = fopen(tmpfile, "wb");

    if(tmp_passwdfile == NULL) {
353
        ICECAST_LOG_WARN("Failed to open temporary authentication database \"%s\": %s",
354 355 356 357 358 359 360 361 362 363 364 365 366 367
                tmpfile, strerror(errno));
        fclose(passwdfile);
        free(tmpfile);
        thread_rwlock_unlock (&state->file_rwlock);
        return AUTH_FAILED;
    }


    while(get_line(passwdfile, line, MAX_LINE_LEN)) {
        if(!line[0] || line[0] == '#')
            continue;

        sep = strchr(line, ':');
        if(sep == NULL) {
368
            ICECAST_LOG_DEBUG("No separator in line");
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
            continue;
        }

        *sep = 0;
        if (strcmp(username, line)) {
            /* We did not match on the user, so copy it to the temp file */
            /* and put the : back in */
            *sep = ':';
            fprintf(tmp_passwdfile, "%s\n", line);
        }
    }

    fclose(tmp_passwdfile);
    fclose(passwdfile);

    /* Now move the contents of the tmp file to the original */
    /* Windows won't let us rename a file if the destination file
       exists...so, lets remove the original first */
    if (remove(state->filename) != 0) {
388
        ICECAST_LOG_ERROR("Problem moving temp authentication file to original \"%s\" - \"%s\": %s",
389
                tmpfile, state->filename, strerror(errno));
Marvin Scholz's avatar
Marvin Scholz committed
390
    } else {
391
        if (rename(tmpfile, state->filename) != 0) {
392
            ICECAST_LOG_ERROR("Problem moving temp authentication file to original \"%s\" - \"%s\": %s",
393 394 395 396
                    tmpfile, state->filename, strerror(errno));
        }
    }
    free(tmpfile);
397 398
    thread_rwlock_unlock(&state->file_rwlock);
    htpasswd_recheckfile(state);
399 400 401 402 403 404 405 406 407 408 409 410 411

    return AUTH_USERDELETED;
}


static auth_result htpasswd_userlist(auth_t *auth, xmlNodePtr srcnode)
{
    htpasswd_auth_state *state;
    xmlNodePtr newnode;
    avl_node *node;

    state = auth->state;

412 413 414 415 416
    if (!state->filename) {
        ICECAST_LOG_ERROR("No filename given in options for authenticator.");
        return AUTH_FAILED;
    }

Marvin Scholz's avatar
Marvin Scholz committed
417
    htpasswd_recheckfile(state);
418

419
    if (state->users == NULL) {
420 421 422 423
        ICECAST_LOG_ERROR("No user list.");
        return AUTH_FAILED;
    }

Marvin Scholz's avatar
Marvin Scholz committed
424 425 426
    thread_rwlock_rlock(&state->file_rwlock);
    node = avl_get_first(state->users);
    while (node) {
427
        htpasswd_user *user = (htpasswd_user *)node->key;
Marvin Scholz's avatar
Marvin Scholz committed
428
        newnode = xmlNewChild(srcnode, NULL, XMLSTR("user"), NULL);
429
        xmlNewTextChild(newnode, NULL, XMLSTR("username"), XMLSTR(user->name));
Marvin Scholz's avatar
Marvin Scholz committed
430
        node = avl_get_next(node);
431
    }
Marvin Scholz's avatar
Marvin Scholz committed
432
    thread_rwlock_unlock(&state->file_rwlock);
433 434 435

    return AUTH_OK;
}