From 581a2c00bcdcecdb023e7764a18a02430d114b56 Mon Sep 17 00:00:00 2001 From: Jaroslav Kysela Date: Mon, 1 Jun 2015 18:25:30 +0200 Subject: [PATCH] Separate passwords from ACL --- docs/html/config_access.html | 6 +- src/access.c | 403 +++++++++++++++++++++--------- src/access.h | 30 ++- src/api/api_access.c | 40 +++ src/config.c | 27 ++ src/webui/static/app/acleditor.js | 40 +++ src/webui/static/app/tvheadend.js | 1 + 7 files changed, 423 insertions(+), 124 deletions(-) diff --git a/docs/html/config_access.html b/docs/html/config_access.html index fbe9e9dba..2b163bae7 100644 --- a/docs/html/config_access.html +++ b/docs/html/config_access.html @@ -8,7 +8,7 @@ wide open.

When Tvheadend verifies access is scan through all the enabled access control entries. The permission flags, streaming profiles, DVR config profiles, channel tags are combined -for all matching access entries. An access entry is said to match if the username / password +for all matching access entries. An access entry is said to match if the username matches and the IP source address of the requesting peer is within the prefix.

@@ -47,10 +47,6 @@ The columns have the following functions:

Login name to be used. If no username is required, this entry should contain a single asterisk (*). -
Password -
- Login password to be used. If username is '*' (unused), the password should be the same (i.e. match any username/password combination, or no username/password required). -
Network prefix
IPv4 prefix for matching based on source IP address. diff --git a/src/access.c b/src/access.c index 96b1eb626..efa601c04 100644 --- a/src/access.c +++ b/src/access.c @@ -42,12 +42,22 @@ struct access_entry_queue access_entries; struct access_ticket_queue access_tickets; +struct passwd_entry_queue passwd_entries; const char *superuser_username; const char *superuser_password; int access_noacl; +static int passwd_verify(const char *username, const char *passwd); +static int passwd_verify2(const char *username, const char *passwd, + const char *username2, const char *passwd2); +static int passwd_verify_digest(const char *username, const uint8_t *digest, + const uint8_t *challenge); +static int passwd_verify_digest2(const char *username, const uint8_t *digest, + const uint8_t *challenge, + const char *username2, const char *passwd2); + /** * */ @@ -306,17 +316,18 @@ access_verify(const char *username, const char *password, { uint32_t bits = 0; access_entry_t *ae; - int match = 0; + int match = 0, nouser = username == NULL || username[0] == '\0'; if (access_noacl) return 0; - if(username != NULL && superuser_username != NULL && - password != NULL && superuser_password != NULL && - !strcmp(username, superuser_username) && - !strcmp(password, superuser_password)) + if (!passwd_verify2(username, password, + superuser_username, superuser_password)) return 0; + if (passwd_verify(username, password)) + username = NULL; + TAILQ_FOREACH(ae, &access_entries, ae_link) { if(!ae->ae_enabled) @@ -324,12 +335,8 @@ access_verify(const char *username, const char *password, if(ae->ae_username[0] != '*') { /* acl entry requires username to match */ - if(username == NULL || password == NULL) + if(username == NULL || strcmp(username, ae->ae_username)) continue; /* Didn't get one */ - - if(strcmp(ae->ae_username, username) || - strcmp(ae->ae_password, password)) - continue; /* username/password mismatch */ } if(!netmask_verify(ae, src)) @@ -342,10 +349,8 @@ access_verify(const char *username, const char *password, } /* Username was not matched - no access */ - if (!match) { - if (username && *username != '\0') - bits = 0; - } + if (!match && !nouser) + bits = 0; return (mask & ACCESS_OR) ? ((mask & bits) ? 0 : -1) : @@ -485,11 +490,13 @@ access_get(const char *username, const char *password, struct sockaddr *src) { access_t *a = calloc(1, sizeof(*a)); access_entry_t *ae; + int nouser = username == NULL || username[0] == '\0'; - if (username && username[0]) { + if (!passwd_verify(username, password)) { a->aa_username = strdup(username); a->aa_representative = strdup(username); } else { + username = NULL; a->aa_representative = malloc(50); tcp_get_str_from_ip((struct sockaddr*)src, a->aa_representative, 50); } @@ -499,10 +506,8 @@ access_get(const char *username, const char *password, struct sockaddr *src) return a; } - if(username != NULL && superuser_username != NULL && - password != NULL && superuser_password != NULL && - !strcmp(username, superuser_username) && - !strcmp(password, superuser_password)) { + if(!passwd_verify2(username, password, + superuser_username, superuser_password)) { a->aa_rights = ACCESS_FULL; return a; } @@ -514,12 +519,8 @@ access_get(const char *username, const char *password, struct sockaddr *src) if(ae->ae_username[0] != '*') { /* acl entry requires username to match */ - if(username == NULL || password == NULL) + if(username == NULL || strcmp(username, ae->ae_username)) continue; /* Didn't get one */ - - if(strcmp(ae->ae_username, username) || - strcmp(ae->ae_password, password)) - continue; /* username/password mismatch */ } if(!netmask_verify(ae, src)) @@ -535,7 +536,7 @@ access_get(const char *username, const char *password, struct sockaddr *src) if (!a->aa_match) { free(a->aa_username); a->aa_username = NULL; - if (username && *username != '\0') + if (!nouser) a->aa_rights = 0; } @@ -553,15 +554,15 @@ access_get_hashed(const char *username, const uint8_t digest[20], { access_t *a = calloc(1, sizeof(*a)); access_entry_t *ae; - SHA_CTX shactx; - uint8_t d[20]; + int nouser = username == NULL || username[0] == '\0'; - if (username && username[0]) { + if (!passwd_verify_digest(username, digest, challenge)) { a->aa_username = strdup(username); a->aa_representative = strdup(username); } else { a->aa_representative = malloc(50); tcp_get_str_from_ip((struct sockaddr*)src, a->aa_representative, 50); + username = NULL; } if(access_noacl) { @@ -569,18 +570,10 @@ access_get_hashed(const char *username, const uint8_t digest[20], return a; } - if(username && superuser_username != NULL && superuser_password != NULL) { - - SHA1_Init(&shactx); - SHA1_Update(&shactx, (const uint8_t *)superuser_password, - strlen(superuser_password)); - SHA1_Update(&shactx, challenge, 32); - SHA1_Final(d, &shactx); - - if(!strcmp(superuser_username, username) && !memcmp(d, digest, 20)) { - a->aa_rights = ACCESS_FULL; - return a; - } + if(!passwd_verify_digest2(username, digest, challenge, + superuser_username, superuser_password)) { + a->aa_rights = ACCESS_FULL; + return a; } TAILQ_FOREACH(ae, &access_entries, ae_link) { @@ -596,15 +589,6 @@ access_get_hashed(const char *username, const uint8_t digest[20], if (!username) continue; - SHA1_Init(&shactx); - SHA1_Update(&shactx, (const uint8_t *)ae->ae_password, - strlen(ae->ae_password)); - SHA1_Update(&shactx, challenge, 32); - SHA1_Final(d, &shactx); - - if(strcmp(ae->ae_username, username) || memcmp(d, digest, 20)) - continue; - a->aa_match = 1; } @@ -615,7 +599,7 @@ access_get_hashed(const char *username, const uint8_t digest[20], if (!a->aa_match) { free(a->aa_username); a->aa_username = NULL; - if (username && *username != '\0') + if (!nouser) a->aa_rights = 0; } @@ -874,13 +858,11 @@ access_entry_update_rights(access_entry_t *ae) */ static void access_entry_reindex(void); -static int access_entry_class_password_set(void *o, const void *v); access_entry_t * access_entry_create(const char *uuid, htsmsg_t *conf) { access_entry_t *ae, *ae2; - const char *s; lock_assert(&global_lock); @@ -902,9 +884,6 @@ access_entry_create(const char *uuid, htsmsg_t *conf) ae->ae_all_dvr = 1; ae->ae_failed_dvr = 1; idnode_load(&ae->ae_id, conf); - /* note password has PO_NOSAVE, thus it must be set manually */ - if ((s = htsmsg_get_str(conf, "password")) != NULL) - access_entry_class_password_set(ae, s); access_entry_update_rights(ae); TAILQ_FOREACH(ae2, &access_entries, ae_link) if (ae->ae_index < ae2->ae_index) @@ -922,8 +901,6 @@ access_entry_create(const char *uuid, htsmsg_t *conf) ae->ae_username = strdup("*"); if (ae->ae_comment == NULL) ae->ae_comment = strdup("New entry"); - if (ae->ae_password == NULL) - access_entry_class_password_set(ae, "*"); if (TAILQ_FIRST(&ae->ae_ipmasks) == NULL) access_set_prefix_default(ae); @@ -955,8 +932,6 @@ access_entry_destroy(access_entry_t *ae) } free(ae->ae_username); - free(ae->ae_password); - free(ae->ae_password2); free(ae->ae_comment); free(ae); } @@ -1125,47 +1100,6 @@ access_entry_class_prefix_get(void *o) return &ret; } -static int -access_entry_class_password_set(void *o, const void *v) -{ - access_entry_t *ae = (access_entry_t *)o; - char buf[256], result[300]; - - if (strcmp(v ?: "", ae->ae_password ?: "")) { - snprintf(buf, sizeof(buf), "TVHeadend-Hide-%s", (const char *)v ?: ""); - base64_encode(result, sizeof(result), (uint8_t *)buf, strlen(buf)); - free(ae->ae_password2); - ae->ae_password2 = strdup(result); - free(ae->ae_password); - ae->ae_password = strdup((const char *)v ?: ""); - return 1; - } - return 0; -} - -static int -access_entry_class_password2_set(void *o, const void *v) -{ - access_entry_t *ae = (access_entry_t *)o; - char result[300]; - int l; - - if (strcmp(v ?: "", ae->ae_password2 ?: "")) { - if (v && ((const char *)v)[0] != '\0') { - l = base64_decode((uint8_t *)result, v, sizeof(result)-1); - if (l < 0) - l = 0; - result[l] = '\0'; - free(ae->ae_password); - ae->ae_password = strdup(result + 15); - free(ae->ae_password2); - ae->ae_password2 = strdup((const char *)v); - return 1; - } - } - return 0; -} - static int access_entry_chtag_set(void *o, const void *v) { @@ -1289,22 +1223,6 @@ const idclass_t access_entry_class = { .name = "Username", .off = offsetof(access_entry_t, ae_username), }, - { - .type = PT_STR, - .id = "password", - .name = "Password", - .off = offsetof(access_entry_t, ae_password), - .opts = PO_PASSWORD | PO_NOSAVE, - .set = access_entry_class_password_set, - }, - { - .type = PT_STR, - .id = "password2", - .name = "Password2", - .off = offsetof(access_entry_t, ae_password2), - .opts = PO_PASSWORD | PO_HIDDEN | PO_ADVANCED | PO_WRONCE, - .set = access_entry_class_password2_set, - }, { .type = PT_STR, .id = "prefix", @@ -1427,6 +1345,244 @@ const idclass_t access_entry_class = { } }; +/* + * Password table + */ + +static int passwd_entry_class_password_set(void *o, const void *v); + +static int +passwd_verify_digest2(const char *username, const uint8_t *digest, + const uint8_t *challenge, + const char *username2, const char *passwd2) +{ + SHA_CTX shactx; + uint8_t d[20]; + + if (username == NULL || username[0] == '\0' || + username2 == NULL || username2[0] == '\0' || + passwd2 == NULL || passwd2[0] == '\0') + return -1; + + if (strcmp(username, username2)) + return -1; + + SHA1_Init(&shactx); + SHA1_Update(&shactx, (const uint8_t *)passwd2, strlen(passwd2)); + SHA1_Update(&shactx, challenge, 32); + SHA1_Final(d, &shactx); + + return memcmp(d, digest, 20) ? -1 : 0; +} + +static int +passwd_verify_digest(const char *username, const uint8_t *digest, + const uint8_t *challenge) +{ + passwd_entry_t *pw; + + TAILQ_FOREACH(pw, &passwd_entries, pw_link) + if (pw->pw_enabled && + !passwd_verify_digest2(username, digest, challenge, + pw->pw_username, pw->pw_password)) + return 0; + return -1; +} + +static int +passwd_verify2(const char *username, const char *passwd, + const char *username2, const char *passwd2) +{ + if (username == NULL || username[0] == '\0' || + username2 == NULL || username2[0] == '\0' || + passwd == NULL || passwd2 == NULL) + return -1; + + if (strcmp(username, username2)) + return -1; + + return strcmp(passwd, passwd2) ? -1 : 0; +} + +static int +passwd_verify(const char *username, const char *passwd) +{ + passwd_entry_t *pw; + + TAILQ_FOREACH(pw, &passwd_entries, pw_link) + if (pw->pw_enabled && + !passwd_verify2(username, passwd, + pw->pw_username, pw->pw_password)) + return 0; + return -1; +} + +passwd_entry_t * +passwd_entry_create(const char *uuid, htsmsg_t *conf) +{ + passwd_entry_t *pw; + const char *s; + + lock_assert(&global_lock); + + pw = calloc(1, sizeof(passwd_entry_t)); + + if (idnode_insert(&pw->pw_id, uuid, &passwd_entry_class, 0)) { + if (uuid) + tvherror("access", "invalid uuid '%s'", uuid); + free(pw); + return NULL; + } + + if (conf) { + pw->pw_enabled = 1; + idnode_load(&pw->pw_id, conf); + /* note password has PO_NOSAVE, thus it must be set manually */ + if ((s = htsmsg_get_str(conf, "password")) != NULL) + passwd_entry_class_password_set(pw, s); + } + + TAILQ_INSERT_TAIL(&passwd_entries, pw, pw_link); + + return pw; +} + +static void +passwd_entry_destroy(passwd_entry_t *pw) +{ + if (pw == NULL) + return; + TAILQ_REMOVE(&passwd_entries, pw, pw_link); + idnode_unlink(&pw->pw_id); + free(pw->pw_username); + free(pw->pw_password); + free(pw->pw_password2); + free(pw->pw_comment); + free(pw); +} + +void +passwd_entry_save(passwd_entry_t *pw) +{ + htsmsg_t *c = htsmsg_create_map(); + idnode_save(&pw->pw_id, c); + hts_settings_save(c, "passwd/%s", idnode_uuid_as_str(&pw->pw_id)); + htsmsg_destroy(c); +} + +static void +passwd_entry_class_save(idnode_t *self) +{ + passwd_entry_save((passwd_entry_t *)self); +} + +static void +passwd_entry_class_delete(idnode_t *self) +{ + passwd_entry_t *pw = (passwd_entry_t *)self; + + hts_settings_remove("passwd/%s", idnode_uuid_as_str(&pw->pw_id)); + passwd_entry_destroy(pw); +} + +static const char * +passwd_entry_class_get_title (idnode_t *self) +{ + passwd_entry_t *pw = (passwd_entry_t *)self; + + if (pw->pw_comment && pw->pw_comment[0] != '\0') + return pw->pw_comment; + return pw->pw_username ?: ""; +} + +static int +passwd_entry_class_password_set(void *o, const void *v) +{ + passwd_entry_t *pw = (passwd_entry_t *)o; + char buf[256], result[300]; + + if (strcmp(v ?: "", pw->pw_password ?: "")) { + snprintf(buf, sizeof(buf), "TVHeadend-Hide-%s", (const char *)v ?: ""); + base64_encode(result, sizeof(result), (uint8_t *)buf, strlen(buf)); + free(pw->pw_password2); + pw->pw_password2 = strdup(result); + free(pw->pw_password); + pw->pw_password = strdup((const char *)v ?: ""); + return 1; + } + return 0; +} + +static int +passwd_entry_class_password2_set(void *o, const void *v) +{ + passwd_entry_t *pw = (passwd_entry_t *)o; + char result[300]; + int l; + + if (strcmp(v ?: "", pw->pw_password2 ?: "")) { + if (v && ((const char *)v)[0] != '\0') { + l = base64_decode((uint8_t *)result, v, sizeof(result)-1); + if (l < 0) + l = 0; + result[l] = '\0'; + free(pw->pw_password); + pw->pw_password = strdup(result + 15); + free(pw->pw_password2); + pw->pw_password2 = strdup((const char *)v); + return 1; + } + } + return 0; +} + +const idclass_t passwd_entry_class = { + .ic_class = "passwd", + .ic_caption = "Passwords", + .ic_event = "passwd", + .ic_perm_def = ACCESS_ADMIN, + .ic_save = passwd_entry_class_save, + .ic_get_title = passwd_entry_class_get_title, + .ic_delete = passwd_entry_class_delete, + .ic_properties = (const property_t[]){ + { + .type = PT_BOOL, + .id = "enabled", + .name = "Enabled", + .off = offsetof(passwd_entry_t, pw_enabled), + }, + { + .type = PT_STR, + .id = "username", + .name = "Username", + .off = offsetof(passwd_entry_t, pw_username), + }, + { + .type = PT_STR, + .id = "password", + .name = "Password", + .off = offsetof(passwd_entry_t, pw_password), + .opts = PO_PASSWORD | PO_NOSAVE, + .set = passwd_entry_class_password_set, + }, + { + .type = PT_STR, + .id = "password2", + .name = "Password2", + .off = offsetof(passwd_entry_t, pw_password2), + .opts = PO_PASSWORD | PO_HIDDEN | PO_ADVANCED | PO_WRONCE, + .set = passwd_entry_class_password2_set, + }, + { + .type = PT_STR, + .id = "comment", + .name = "Comment", + .off = offsetof(passwd_entry_t, pw_comment), + }, + {} + } +}; + /** * */ @@ -1453,8 +1609,18 @@ access_init(int createdefault, int noacl) TAILQ_INIT(&access_entries); TAILQ_INIT(&access_tickets); + TAILQ_INIT(&passwd_entries); + + /* Load passwd entries */ + if ((c = hts_settings_load("passwd")) != NULL) { + HTSMSG_FOREACH(f, c) { + if (!(m = htsmsg_field_get_map(f))) continue; + (void)passwd_entry_create(f->hmf_name, m); + } + htsmsg_destroy(c); + } - /* Load */ + /* Load ACL entries */ if ((c = hts_settings_load("accesscontrol")) != NULL) { HTSMSG_FOREACH(f, c) { if (!(m = htsmsg_field_get_map(f))) continue; @@ -1513,12 +1679,15 @@ access_done(void) { access_entry_t *ae; access_ticket_t *at; + passwd_entry_t *pw; pthread_mutex_lock(&global_lock); while ((ae = TAILQ_FIRST(&access_entries)) != NULL) access_entry_destroy(ae); while ((at = TAILQ_FIRST(&access_tickets)) != NULL) access_ticket_destroy(at); + while ((pw = TAILQ_FIRST(&passwd_entries)) != NULL) + passwd_entry_destroy(pw); free((void *)superuser_username); superuser_username = NULL; free((void *)superuser_password); diff --git a/src/access.h b/src/access.h index 42321db57..c1e7f0b32 100644 --- a/src/access.h +++ b/src/access.h @@ -26,6 +26,26 @@ struct profile; struct dvr_config; struct channel_tag; +TAILQ_HEAD(passwd_entry_queue, passwd_entry); + +extern struct passwd_entry_queue passwd_entries; + +typedef struct passwd_entry { + idnode_t pw_id; + + TAILQ_ENTRY(passwd_entry) pw_link; + + char *pw_username; + char *pw_password; + char *pw_password2; + + int pw_enabled; + + char *pw_comment; +} passwd_entry_t; + +extern const idclass_t passwd_entry_class; + typedef struct access_ipmask { TAILQ_ENTRY(access_ipmask) ai_link; @@ -48,8 +68,6 @@ typedef struct access_entry { TAILQ_ENTRY(access_entry) ae_link; char *ae_username; - char *ae_password; - char *ae_password2; char *ae_comment; int ae_index; @@ -220,6 +238,14 @@ access_destroy_by_dvr_config(struct dvr_config *cfg, int delconf); void access_destroy_by_channel_tag(struct channel_tag *ct, int delconf); +/** + * + */ +passwd_entry_t * +passwd_entry_create(const char *uuid, htsmsg_t *conf); +void +passwd_entry_save(passwd_entry_t *pw); + /** * */ diff --git a/src/api/api_access.c b/src/api/api_access.c index 63f98398f..cc5456973 100644 --- a/src/api/api_access.c +++ b/src/api/api_access.c @@ -21,6 +21,42 @@ #include "access.h" #include "api.h" +/* + * + */ + +static void +api_passwd_entry_grid + ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) +{ + passwd_entry_t *pw; + + TAILQ_FOREACH(pw, &passwd_entries, pw_link) + idnode_set_add(ins, (idnode_t*)pw, &conf->filter); +} + +static int +api_passwd_entry_create + ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp ) +{ + htsmsg_t *conf; + passwd_entry_t *pw; + + if (!(conf = htsmsg_get_map(args, "conf"))) + return EINVAL; + + pthread_mutex_lock(&global_lock); + if ((pw = passwd_entry_create(NULL, conf)) != NULL) + passwd_entry_save(pw); + pthread_mutex_unlock(&global_lock); + + return 0; +} + +/* + * + */ + static void api_access_entry_grid ( access_t *perm, idnode_set_t *ins, api_idnode_grid_conf_t *conf, htsmsg_t *args ) @@ -52,6 +88,10 @@ api_access_entry_create void api_access_init ( void ) { static api_hook_t ah[] = { + { "passwd/entry/class", ACCESS_ADMIN, api_idnode_class, (void*)&passwd_entry_class }, + { "passwd/entry/grid", ACCESS_ADMIN, api_idnode_grid, api_passwd_entry_grid }, + { "passwd/entry/create", ACCESS_ADMIN, api_passwd_entry_create, NULL }, + { "access/entry/class", ACCESS_ADMIN, api_idnode_class, (void*)&access_entry_class }, { "access/entry/grid", ACCESS_ADMIN, api_idnode_grid, api_access_entry_grid }, { "access/entry/create", ACCESS_ADMIN, api_access_entry_create, NULL }, diff --git a/src/config.c b/src/config.c index 84156b833..0de63576a 100644 --- a/src/config.c +++ b/src/config.c @@ -1210,6 +1210,32 @@ config_migrate_v18 ( void ) } } +static void +config_migrate_v19 ( void ) +{ + htsmsg_t *c, *e, *m; + htsmsg_field_t *f; + const char *username, *passwd; + tvh_uuid_t u; + + if ((c = hts_settings_load("accesscontrol")) != NULL) { + HTSMSG_FOREACH(f, c) { + if (!(e = htsmsg_field_get_map(f))) continue; + if ((username = htsmsg_get_str(e, "username")) == NULL) + continue; + if ((passwd = htsmsg_get_str(e, "password2")) == NULL) + continue; + m = htsmsg_create_map(); + htsmsg_add_str(m, "username", username); + htsmsg_add_str(m, "password2", passwd); + uuid_init_hex(&u, NULL); + hts_settings_save(m, "passwd/%s", u.hex); + htsmsg_delete_field(e, "password2"); + hts_settings_save(e, "accesscontrol/%s", f->hmf_name); + } + } +} + /* * Perform backup */ @@ -1324,6 +1350,7 @@ static const config_migrate_t config_migrate_table[] = { config_migrate_v16, config_migrate_v17, config_migrate_v18, + config_migrate_v19, }; /* diff --git a/src/webui/static/app/acleditor.js b/src/webui/static/app/acleditor.js index 0ec941d80..e287e3f4a 100644 --- a/src/webui/static/app/acleditor.js +++ b/src/webui/static/app/acleditor.js @@ -61,3 +61,43 @@ tvheadend.acleditor = function(panel, index) } }); }; + +/* + * Password Control + */ + +tvheadend.passwdeditor = function(panel, index) +{ + var list = 'enabled,username,password,comment'; + + tvheadend.idnode_grid(panel, { + url: 'api/passwd/entry', + titleS: 'Password', + titleP: 'Passwords', + iconCls: 'key', + columns: { + enabled: { width: 120 }, + username: { width: 250 }, + password: { width: 250 } + }, + tabIndex: index, + edit: { + params: { + list: list + } + }, + add: { + url: 'api/passwd/entry', + params: { + list: list + }, + create: { } + }, + del: true, + move: true, + list: list, + help: function() { + new tvheadend.help('Password Control Entries', 'config_passwd.html'); + } + }); +}; diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index 9efe8d08b..0f2c6dca8 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -345,6 +345,7 @@ function accessUpdate(o) { tvheadend.miscconf(cp); tvheadend.acleditor(cp); + tvheadend.passwdeditor(cp); /* DVB inputs, networks, muxes, services */ var dvbin = new Ext.TabPanel({ -- 2.47.2