From d642846c803872ae1d37e0ba1458f7ac27e91060 Mon Sep 17 00:00:00 2001 From: oddsock Date: Fri, 30 Apr 2004 14:36:07 +0000 Subject: [PATCH] added web based interface to htpasswd client authentication svn path=/icecast/trunk/icecast/; revision=6610 --- admin/Makefile.am | 2 +- admin/listclients.xsl | 2 +- admin/listmounts.xsl | 5 +- admin/stats.xsl | 5 +- conf/icecast.xml.in | 3 + doc/icecast2_config_file.html | 8 ++ src/admin.c | 85 +++++++++++++ src/auth.c | 227 ++++++++++++++++++++++++++++++++++ src/auth.h | 10 ++ src/cfgfile.c | 1 + src/source.c | 1 + web/Makefile.am | 1 + win32/icecast2.iss | 11 +- 13 files changed, 356 insertions(+), 5 deletions(-) diff --git a/admin/Makefile.am b/admin/Makefile.am index c7c2af43..3a298c24 100644 --- a/admin/Makefile.am +++ b/admin/Makefile.am @@ -4,5 +4,5 @@ AUTOMAKE_OPTIONS = foreign admindir = $(pkgdatadir)/admin dist_admin_DATA = listclients.xsl listmounts.xsl moveclients.xsl response.xsl \ - stats.xsl + stats.xsl manageauth.xsl diff --git a/admin/listclients.xsl b/admin/listclients.xsl index fbc434fa..1a57dfa9 100755 --- a/admin/listclients.xsl +++ b/admin/listclients.xsl @@ -51,7 +51,7 @@ - + () seconds kill diff --git a/admin/listmounts.xsl b/admin/listmounts.xsl index f6bf81dc..82784eeb 100755 --- a/admin/listmounts.xsl +++ b/admin/listmounts.xsl @@ -31,13 +31,16 @@

-()

+() + +
Show Listeners | Move Listeners | Kill Source + | Manage Authentication


diff --git a/admin/stats.xsl b/admin/stats.xsl index e56b5828..04b9a17c 100755 --- a/admin/stats.xsl +++ b/admin/stats.xsl @@ -58,13 +58,16 @@

-()

+() + +
List Clients | Move MountPoints | Kill Source + | Manage Authentication


diff --git a/conf/icecast.xml.in b/conf/icecast.xml.in index 63f05bb6..481c7ff2 100644 --- a/conf/icecast.xml.in +++ b/conf/icecast.xml.in @@ -84,6 +84,9 @@ 1 /tmp/dump-example1.ogg /example2.ogg + + --> diff --git a/doc/icecast2_config_file.html b/doc/icecast2_config_file.html index 4c52fefa..b5a9ffbf 100755 --- a/doc/icecast2_config_file.html +++ b/doc/icecast2_config_file.html @@ -271,6 +271,10 @@ If you are relaying a Shoutcast stream, you need to specify this indicator to al <max-listeners>1<max-listeners> <dump-file>/tmp/dump-example1.ogg<dump-file> <fallback-mount>example2.ogg<fallback-mount> + <authentication type="htpasswd"> + <option name="filename" value="myauth"/> + </authentication> + <mount>

This section contains settings which apply only to a specific mountpoint. Within this section you can reserve a specific mountpoint and set a source username/password for that mountpoint (not yet implemented) as well as specify individual settings which will apply only to the supplied mountpoint. @@ -299,6 +303,10 @@ An optional value which will set the filename which will be a dump of the stream

This specifies a mountpoint that is used in the case of a source disconnect. If listeners are connected to the mount specified by the <mount-name> config value, then if the source is disconnected; all currently connected clients will be moved to the fallback-mount.
+

authentication

+
+This specifies that the named mount point will require listener authentication. Currently, we only support a file-based authentication scheme (type=htpasswd). Users and encrypted password are placed in this file (separated by a :) and all requests for this mountpoint will require that a user and password be supplied for authentication purposes. These values are passed in via normal HTTP Basic Authentication means (i.e. http://user:password@stream:port/mountpoint.ogg). Users and Passwords are maintained via the web admin interface. A mountpoint configured with an authenticator will display a red key next to the mount point name on the admin screens. +



diff --git a/src/admin.c b/src/admin.c index 049a3661..448c728d 100644 --- a/src/admin.c +++ b/src/admin.c @@ -37,6 +37,7 @@ #include "format_mp3.h" #include "logging.h" +#include "auth.h" #ifdef _WIN32 #define snprintf _snprintf #endif @@ -50,10 +51,12 @@ #define COMMAND_METADATA_UPDATE 2 #define COMMAND_RAW_SHOW_LISTENERS 3 #define COMMAND_RAW_MOVE_CLIENTS 4 +#define COMMAND_RAW_MANAGEAUTH 5 #define COMMAND_TRANSFORMED_FALLBACK 50 #define COMMAND_TRANSFORMED_SHOW_LISTENERS 53 #define COMMAND_TRANSFORMED_MOVE_CLIENTS 54 +#define COMMAND_TRANSFORMED_MANAGEAUTH 55 /* Global commands */ #define COMMAND_RAW_LIST_MOUNTS 101 @@ -89,6 +92,8 @@ #define KILLSOURCE_RAW_REQUEST "killsource" #define KILLSOURCE_TRANSFORMED_REQUEST "killsource.xsl" #define ADMIN_XSL_RESPONSE "response.xsl" +#define MANAGEAUTH_RAW_REQUEST "manageauth" +#define MANAGEAUTH_TRANSFORMED_REQUEST "manageauth.xsl" #define DEFAULT_RAW_REQUEST "" #define DEFAULT_TRANSFORMED_REQUEST "" @@ -133,6 +138,10 @@ int admin_get_command(char *command) return COMMAND_RAW_KILL_SOURCE; else if(!strcmp(command, KILLSOURCE_TRANSFORMED_REQUEST)) return COMMAND_TRANSFORMED_KILL_SOURCE; + else if(!strcmp(command, MANAGEAUTH_RAW_REQUEST)) + return COMMAND_RAW_MANAGEAUTH; + else if(!strcmp(command, MANAGEAUTH_TRANSFORMED_REQUEST)) + return COMMAND_TRANSFORMED_MANAGEAUTH; else if(!strcmp(command, DEFAULT_TRANSFORMED_REQUEST)) return COMMAND_TRANSFORMED_STATS; else if(!strcmp(command, DEFAULT_RAW_REQUEST)) @@ -151,6 +160,8 @@ static void command_stats(client_t *client, int response); static void command_list_mounts(client_t *client, int response); static void command_kill_client(client_t *client, source_t *source, int response); +static void command_manageauth(client_t *client, source_t *source, + int response); static void command_kill_source(client_t *client, source_t *source, int response); static void admin_handle_mount_request(client_t *client, source_t *source, @@ -195,6 +206,10 @@ xmlDocPtr admin_build_sourcelist(char *current_source) xmlNewChild(srcnode, NULL, "Connected", buf); xmlNewChild(srcnode, NULL, "Format", source->format->format_description); + if (source->authenticator) { + xmlNewChild(srcnode, NULL, "authenticator", + source->authenticator->type); + } } node = avl_get_next(node); } @@ -402,6 +417,12 @@ static void admin_handle_mount_request(client_t *client, source_t *source, case COMMAND_TRANSFORMED_KILL_SOURCE: command_kill_source(client, source, TRANSFORMED); break; + case COMMAND_TRANSFORMED_MANAGEAUTH: + command_manageauth(client, source, TRANSFORMED); + break; + case COMMAND_RAW_MANAGEAUTH: + command_manageauth(client, source, RAW); + break; default: WARN0("Mount request not recognised"); client_send_400(client, "Mount request unknown"); @@ -547,6 +568,9 @@ static void command_show_listeners(client_t *client, source_t *source, memset(buf, '\000', sizeof(buf)); snprintf(buf, sizeof(buf)-1, "%lu", current->con->id); xmlNewChild(listenernode, NULL, "ID", buf); + if (current->username) { + xmlNewChild(listenernode, NULL, "username", current->username); + } client_node = avl_get_next(client_node); } @@ -557,6 +581,67 @@ static void command_show_listeners(client_t *client, source_t *source, client_destroy(client); } +static void command_manageauth(client_t *client, source_t *source, + int response) +{ + xmlDocPtr doc; + xmlNodePtr node, srcnode, msgnode; + char *action = NULL; + char *username = NULL; + char *password = NULL; + char *message = NULL; + int ret = AUTH_OK; + + if((COMMAND_OPTIONAL(client, "action", action))) { + if (!strcmp(action, "add")) { + COMMAND_REQUIRE(client, "username", username); + COMMAND_REQUIRE(client, "password", password); + ret = auth_adduser(source, username, password); + if (ret == AUTH_FAILED) { + message = strdup("User add failed - check the icecast error log"); + } + if (ret == AUTH_USERADDED) { + message = strdup("User added"); + } + if (ret == AUTH_USEREXISTS) { + message = strdup("User already exists - not added"); + } + } + if (!strcmp(action, "delete")) { + COMMAND_REQUIRE(client, "username", username); + ret = auth_deleteuser(source, username); + if (ret == AUTH_FAILED) { + message = strdup("User delete failed - check the icecast error log"); + } + if (ret == AUTH_USERDELETED) { + message = strdup("User deleted"); + } + } + } + + doc = xmlNewDoc("1.0"); + node = xmlNewDocNode(doc, NULL, "icestats", NULL); + srcnode = xmlNewChild(node, NULL, "source", NULL); + xmlSetProp(srcnode, "mount", source->mount); + + if (message) { + msgnode = xmlNewChild(node, NULL, "iceresponse", NULL); + xmlNewChild(msgnode, NULL, "message", message); + } + + xmlDocSetRootElement(doc, node); + + auth_get_userlist(source, srcnode); + + admin_send_response(doc, client, response, + MANAGEAUTH_TRANSFORMED_REQUEST); + if (message) { + free(message); + } + xmlFreeDoc(doc); + client_destroy(client); +} + static void command_kill_source(client_t *client, source_t *source, int response) { diff --git a/src/auth.c b/src/auth.c index e807b6ad..0f789ded 100644 --- a/src/auth.c +++ b/src/auth.c @@ -33,6 +33,7 @@ #include "logging.h" #define CATMODULE "auth" + auth_result auth_check_client(source_t *source, client_t *client) { auth_t *authenticator = source->authenticator; @@ -90,6 +91,7 @@ auth_t *auth_get_authenticator(char *type, config_options_t *options) auth_t *auth = NULL; if(!strcmp(type, "htpasswd")) { auth = auth_get_htpasswd_auth(options); + auth->type = strdup(type); } else { ERROR1("Unrecognised authenticator type: \"%s\"", type); @@ -104,12 +106,15 @@ auth_t *auth_get_authenticator(char *type, config_options_t *options) typedef struct { char *filename; + rwlock_t file_rwlock; } htpasswd_auth_state; static void htpasswd_clear(auth_t *self) { htpasswd_auth_state *state = self->state; free(state->filename); + thread_rwlock_destroy(&state->file_rwlock); free(state); + free(self->type); free(self); } @@ -152,9 +157,11 @@ static auth_result htpasswd_auth(auth_t *auth, char *username, char *password) char line[MAX_LINE_LEN]; char *sep; + thread_rwlock_rlock(&state->file_rwlock); if(passwdfile == NULL) { WARN2("Failed to open authentication database \"%s\": %s", state->filename, strerror(errno)); + thread_rwlock_unlock(&state->file_rwlock); return AUTH_FAILED; } @@ -176,6 +183,7 @@ static auth_result htpasswd_auth(auth_t *auth, char *username, char *password) if(!strcmp(hash, hashed_password)) { fclose(passwdfile); free(hashed_password); + thread_rwlock_unlock(&state->file_rwlock); return AUTH_OK; } free(hashed_password); @@ -186,6 +194,7 @@ static auth_result htpasswd_auth(auth_t *auth, char *username, char *password) fclose(passwdfile); + thread_rwlock_unlock(&state->file_rwlock); return AUTH_FAILED; } @@ -216,6 +225,224 @@ static auth_t *auth_get_htpasswd_auth(config_options_t *options) DEBUG1("Configured htpasswd authentication using password file %s", state->filename); + thread_rwlock_create(&state->file_rwlock); + return authenticator; } +int auth_htpasswd_existing_user(auth_t *auth, char *username) +{ + FILE *passwdfile; + htpasswd_auth_state *state; + int ret = AUTH_OK; + char line[MAX_LINE_LEN]; + char *sep; + + state = auth->state; + passwdfile = fopen(state->filename, "rb"); + + if(passwdfile == NULL) { + WARN2("Failed to open authentication database \"%s\": %s", + state->filename, strerror(errno)); + return AUTH_FAILED; + } + while(get_line(passwdfile, line, MAX_LINE_LEN)) { + if(!line[0] || line[0] == '#') + continue; + sep = strchr(line, ':'); + if(sep == NULL) { + DEBUG0("No seperator in line"); + continue; + } + *sep = 0; + if (!strcmp(username, line)) { + /* We found the user, break out of the loop */ + ret = AUTH_USEREXISTS; + break; + } + } + + fclose(passwdfile); + return ret; + +} +int auth_htpasswd_adduser(auth_t *auth, char *username, char *password) +{ + FILE *passwdfile; + char *hashed_password = NULL; + htpasswd_auth_state *state; + + if (auth_htpasswd_existing_user(auth, username) == AUTH_USEREXISTS) { + return AUTH_USEREXISTS; + } + state = auth->state; + passwdfile = fopen(state->filename, "ab"); + + if(passwdfile == NULL) { + WARN2("Failed to open authentication database \"%s\": %s", + 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); + return AUTH_USERADDED; +} + +int auth_adduser(source_t *source, char *username, char *password) +{ + int ret = 0; + htpasswd_auth_state *state; + + if (source->authenticator) { + if (!strcmp(source->authenticator->type, "htpasswd")) { + state = source->authenticator->state; + thread_rwlock_wlock(&state->file_rwlock); + ret = auth_htpasswd_adduser(source->authenticator, username, password); + thread_rwlock_unlock(&state->file_rwlock); + } + } + return ret; +} + +int auth_htpasswd_deleteuser(auth_t *auth, 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; + + state = auth->state; + passwdfile = fopen(state->filename, "rb"); + + if(passwdfile == NULL) { + WARN2("Failed to open authentication database \"%s\": %s", + state->filename, strerror(errno)); + return AUTH_FAILED; + } + tmpfile_len = strlen(state->filename) + 6; + tmpfile = calloc(1, tmpfile_len); + sprintf(tmpfile, ".%s.tmp", state->filename); + + tmp_passwdfile = fopen(tmpfile, "wb"); + + if(tmp_passwdfile == NULL) { + WARN2("Failed to open temporary authentication database \"%s\": %s", + tmpfile, strerror(errno)); + fclose(passwdfile); + free(tmpfile); + return AUTH_FAILED; + } + + + while(get_line(passwdfile, line, MAX_LINE_LEN)) { + if(!line[0] || line[0] == '#') + continue; + + sep = strchr(line, ':'); + if(sep == NULL) { + DEBUG0("No seperator in line"); + 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 */ + if (rename(tmpfile, state->filename) != 0) { + ERROR3("Problem moving temp authentication file to original \"%s\" - \"%s\": %s", + tmpfile, state->filename, strerror(errno)); + } + + free(tmpfile); + + return AUTH_USERDELETED; +} +int auth_deleteuser(source_t *source, char *username) +{ + htpasswd_auth_state *state; + + int ret = 0; + if (source->authenticator) { + if (!strcmp(source->authenticator->type, "htpasswd")) { + state = source->authenticator->state; + thread_rwlock_wlock(&state->file_rwlock); + ret = auth_htpasswd_deleteuser(source->authenticator, username); + thread_rwlock_unlock(&state->file_rwlock); + } + } + return ret; +} + +int auth_get_htpasswd_userlist(auth_t *auth, xmlNodePtr srcnode) +{ + htpasswd_auth_state *state; + FILE *passwdfile; + char line[MAX_LINE_LEN]; + char *sep; + char *passwd; + xmlNodePtr newnode; + + state = auth->state; + + passwdfile = fopen(state->filename, "rb"); + + if(passwdfile == NULL) { + WARN2("Failed to open authentication database \"%s\": %s", + state->filename, strerror(errno)); + return AUTH_FAILED; + } + + while(get_line(passwdfile, line, MAX_LINE_LEN)) { + if(!line[0] || line[0] == '#') + continue; + + sep = strchr(line, ':'); + if(sep == NULL) { + DEBUG0("No seperator in line"); + continue; + } + + *sep = 0; + newnode = xmlNewChild(srcnode, NULL, "User", NULL); + xmlNewChild(newnode, NULL, "username", line); + passwd = sep+1; + xmlNewChild(newnode, NULL, "password", passwd); + } + + fclose(passwdfile); + return AUTH_OK; +} + +int auth_get_userlist(source_t *source, xmlNodePtr srcnode) +{ + int ret = 0; + htpasswd_auth_state *state; + + if (source->authenticator) { + if (!strcmp(source->authenticator->type, "htpasswd")) { + state = source->authenticator->state; + thread_rwlock_rlock(&state->file_rwlock); + ret = auth_get_htpasswd_userlist(source->authenticator, srcnode); + thread_rwlock_unlock(&state->file_rwlock); + } + } + return ret; +} diff --git a/src/auth.h b/src/auth.h index 17f4f5a2..eebb3fbc 100644 --- a/src/auth.h +++ b/src/auth.h @@ -16,11 +16,17 @@ #include "source.h" #include "client.h" #include "config.h" +#include +#include +#include typedef enum { AUTH_OK, AUTH_FAILED, + AUTH_USERADDED, + AUTH_USEREXISTS, + AUTH_USERDELETED, } auth_result; typedef struct auth_tag @@ -30,12 +36,16 @@ typedef struct auth_tag char *username, char *password); void (*free)(struct auth_tag *self); void *state; + void *type; } auth_t; auth_result auth_check_client(source_t *source, client_t *client); auth_t *auth_get_authenticator(char *type, config_options_t *options); void *auth_clear(auth_t *authenticator); +int auth_get_userlist(source_t *source, xmlNodePtr srcnode); +int auth_adduser(source_t *source, char *username, char *password); +int auth_deleteuser(source_t *source, char *username); #endif diff --git a/src/cfgfile.c b/src/cfgfile.c index 7ef1753b..0e0f2c30 100644 --- a/src/cfgfile.c +++ b/src/cfgfile.c @@ -551,6 +551,7 @@ static void _parse_mount(xmlDocPtr doc, xmlNodePtr node, option = option->next; continue; } + opt->next = NULL; if(last_option) last_option->next = opt; diff --git a/src/source.c b/src/source.c index 5ef6c18b..c041cbfd 100644 --- a/src/source.c +++ b/src/source.c @@ -978,6 +978,7 @@ void source_apply_mount (source_t *source, mount_proxy *mountinfo) { source->authenticator = auth_get_authenticator( mountinfo->auth_type, mountinfo->auth_options); + stats_event(source->mount, "authenticator", mountinfo->auth_type); } if (mountinfo->dumpfile) { diff --git a/web/Makefile.am b/web/Makefile.am index cdb7ed5f..4ecf9714 100644 --- a/web/Makefile.am +++ b/web/Makefile.am @@ -10,4 +10,5 @@ dist_web_DATA = status.xsl \ corner_topleft.jpg \ corner_topright.jpg \ icecast.png \ + key.gif \ style.css diff --git a/win32/icecast2.iss b/win32/icecast2.iss index 4dfa7015..6ba2226f 100644 --- a/win32/icecast2.iss +++ b/win32/icecast2.iss @@ -32,13 +32,22 @@ Name: "{app}\logs" Source: "Release\Icecast2.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "Release\icecast2console.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "..\doc\icecast2.chm"; DestDir: "{app}\doc"; Flags: ignoreversion -Source: "..\web\status.xsl"; DestDir: "{app}\web"; Flags: ignoreversion +Source: "..\web\corner_bottomleft.jpg"; DestDir: "{app}\web"; Flags: ignoreversion +Source: "..\web\corner_bottomright.jpg"; DestDir: "{app}\web"; Flags: ignoreversion +Source: "..\web\corner_topleft.jpg"; DestDir: "{app}\web"; Flags: ignoreversion +Source: "..\web\corner_topright.jpg"; DestDir: "{app}\web"; Flags: ignoreversion +Source: "..\web\icecast.png"; DestDir: "{app}\web"; Flags: ignoreversion +Source: "..\web\key.gif"; DestDir: "{app}\web"; Flags: ignoreversion Source: "..\web\status2.xsl"; DestDir: "{app}\web"; Flags: ignoreversion +Source: "..\web\status.xsl"; DestDir: "{app}\web"; Flags: ignoreversion +Source: "..\web\style.css"; DestDir: "{app}\web"; Flags: ignoreversion + Source: "..\admin\listclients.xsl"; DestDir: "{app}\admin"; Flags: ignoreversion Source: "..\admin\listmounts.xsl"; DestDir: "{app}\admin"; Flags: ignoreversion Source: "..\admin\moveclients.xsl"; DestDir: "{app}\admin"; Flags: ignoreversion Source: "..\admin\response.xsl"; DestDir: "{app}\admin"; Flags: ignoreversion Source: "..\admin\stats.xsl"; DestDir: "{app}\admin"; Flags: ignoreversion +Source: "..\admin\manageauth.xsl"; DestDir: "{app}\admin"; Flags: ignoreversion Source: "..\..\pthreads\pthreadVSE.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "..\conf\icecast.xml"; DestDir: "{app}"; Flags: ignoreversion Source: "..\..\iconv\lib\iconv.dll"; DestDir: "{app}"; Flags: ignoreversion -- GitLab