auth_htpasswd.c 11.1 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
394
        xmlNewTextChild(newnode, NULL, XMLSTR("username"), XMLSTR(user->name));
        xmlNewTextChild(newnode, NULL, XMLSTR("password"), XMLSTR(user->pass));
Marvin Scholz's avatar
Marvin Scholz committed
395
        node = avl_get_next(node);
396
    }
Marvin Scholz's avatar
Marvin Scholz committed
397
    thread_rwlock_unlock(&state->file_rwlock);
398
399
400

    return AUTH_OK;
}