auth_htpasswd.c 11 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"
Marvin Scholz's avatar
Marvin Scholz committed
33
#include "common/httpp/httpp.h"
34 35 36 37
#include "md5.h"

#include "logging.h"
#define CATMODULE "auth_htpasswd"
Philipp Schafft's avatar
Philipp Schafft committed
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 194 195
    htpasswd_recheckfile (htpasswd);

    thread_rwlock_rlock (&htpasswd->file_rwlock);
    entry.name = client->username;
Marvin Scholz's avatar
Marvin Scholz committed
196
    if (avl_get_by_key (htpasswd->users, &entry, &result) == 0) {
197 198 199 200 201
        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
202
        if (strcmp (found->pass, hashed_pw) == 0) {
203 204 205 206
            free (hashed_pw);
            return AUTH_OK;
        }
        free (hashed_pw);
207
        ICECAST_LOG_DEBUG("incorrect password for client with username: %s", client->username);
208 209
        return AUTH_FAILED;
    }
210
    ICECAST_LOG_DEBUG("no such username: %s", client->username);
211
    thread_rwlock_unlock (&htpasswd->file_rwlock);
Philipp Schafft's avatar
Philipp Schafft committed
212
    return AUTH_NOMATCH;
213 214 215 216 217 218
}

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

Philipp Schafft's avatar
Philipp Schafft committed
219
    authenticator->authenticate_client = htpasswd_auth;
220 221 222 223
    authenticator->free = htpasswd_clear;
    authenticator->adduser = htpasswd_adduser;
    authenticator->deleteuser = htpasswd_deleteuser;
    authenticator->listuser = htpasswd_userlist;
224
    authenticator->immediate = 1;
225 226 227 228

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

    while(options) {
Marvin Scholz's avatar
Marvin Scholz committed
229
        if(!strcmp(options->name, "filename")) {
230
            free (state->filename);
231
            state->filename = strdup(options->value);
232
        }
233 234 235
        options = options->next;
    }

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

    authenticator->state = state;

    thread_rwlock_create(&state->file_rwlock);
246
    htpasswd_recheckfile(state);
247 248 249 250 251 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;

    htpasswd_recheckfile (state);

    thread_rwlock_wlock (&state->file_rwlock);

    entry.name = (char*)username;
Marvin Scholz's avatar
Marvin Scholz committed
265
    if (avl_get_by_key (state->users, &entry, &result) == 0) {
266 267 268 269 270 271
        thread_rwlock_unlock (&state->file_rwlock);
        return AUTH_USEREXISTS;
    }

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

Marvin Scholz's avatar
Marvin Scholz committed
272
    if (passwdfile == NULL) {
273
        thread_rwlock_unlock (&state->file_rwlock);
274
        ICECAST_LOG_WARN("Failed to open authentication database \"%s\": %s",
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
                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;
    thread_rwlock_wlock (&state->file_rwlock);
    passwdfile = fopen(state->filename, "rb");

    if(passwdfile == NULL) {
308
        ICECAST_LOG_WARN("Failed to open authentication database \"%s\": %s",
309 310 311 312 313 314 315
                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
316
    if (stat (tmpfile, &file_info) == 0) {
317
        ICECAST_LOG_WARN("temp file \"%s\" exists, rejecting operation", tmpfile);
318 319 320 321 322 323 324 325 326
        free (tmpfile);
        fclose (passwdfile);
        thread_rwlock_unlock (&state->file_rwlock);
        return AUTH_FAILED;
    }

    tmp_passwdfile = fopen(tmpfile, "wb");

    if(tmp_passwdfile == NULL) {
327
        ICECAST_LOG_WARN("Failed to open temporary authentication database \"%s\": %s",
328 329 330 331 332 333 334 335 336 337 338 339 340 341
                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) {
342
            ICECAST_LOG_DEBUG("No separator in line");
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
            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) {
362
        ICECAST_LOG_ERROR("Problem moving temp authentication file to original \"%s\" - \"%s\": %s",
363
                tmpfile, state->filename, strerror(errno));
Marvin Scholz's avatar
Marvin Scholz committed
364
    } else {
365
        if (rename(tmpfile, state->filename) != 0) {
366
            ICECAST_LOG_ERROR("Problem moving temp authentication file to original \"%s\" - \"%s\": %s",
367 368 369 370
                    tmpfile, state->filename, strerror(errno));
        }
    }
    free(tmpfile);
371 372
    thread_rwlock_unlock(&state->file_rwlock);
    htpasswd_recheckfile(state);
373 374 375 376 377 378 379 380 381 382 383 384 385

    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;

Marvin Scholz's avatar
Marvin Scholz committed
386
    htpasswd_recheckfile(state);
387

Marvin Scholz's avatar
Marvin Scholz committed
388 389 390
    thread_rwlock_rlock(&state->file_rwlock);
    node = avl_get_first(state->users);
    while (node) {
391
        htpasswd_user *user = (htpasswd_user *)node->key;
Marvin Scholz's avatar
Marvin Scholz committed
392
        newnode = xmlNewChild(srcnode, NULL, XMLSTR("user"), NULL);
393
        xmlNewTextChild(newnode, NULL, XMLSTR("username"), XMLSTR(user->name));
Marvin Scholz's avatar
Marvin Scholz committed
394
        node = avl_get_next(node);
395
    }
Marvin Scholz's avatar
Marvin Scholz committed
396
    thread_rwlock_unlock(&state->file_rwlock);
397 398 399

    return AUTH_OK;
}