diff --git a/include/vlc_keystore.h b/include/vlc_keystore.h index 1cdf8e74aa..94780ee833 100644 --- a/include/vlc_keystore.h +++ b/include/vlc_keystore.h @@ -30,9 +30,10 @@ typedef struct vlc_keystore vlc_keystore; typedef struct vlc_keystore_entry vlc_keystore_entry; +typedef struct vlc_credential vlc_credential; /** - * @defgroup keystore Keystore API + * @defgroup keystore Keystore and credential API * @{ * @defgroup keystore_public Keystore public API * @{ @@ -144,6 +145,106 @@ vlc_keystore_remove(vlc_keystore *p_keystore, VLC_API void vlc_keystore_release_entries(vlc_keystore_entry *p_entries, unsigned int i_count); +/** + * @} + * @defgroup credential Credential API + * @{ + */ + +/** + * @note init with vlc_credential_init() + */ +struct vlc_credential +{ + /** url to store or to search */ + const vlc_url_t *p_url; + /** http realm or smb domain */ + const char *psz_realm; + /** http authtype */ + const char *psz_authtype; + /** valid only if vlc_credential_get() returned true */ + const char *psz_username; + /** valid only if vlc_credential_get() returned true */ + const char *psz_password; + + /* internal */ + enum { + GET_FROM_URL, + GET_FROM_OPTION, + GET_FROM_KEYSTORE, + GET_FROM_DIALOG, + } i_get_order; + + vlc_keystore *p_keystore; + vlc_keystore_entry *p_entries; + unsigned int i_entries_count; + + char *psz_split_realm; + char *psz_var_username; + char *psz_var_password; + + char *psz_dialog_username; + char *psz_dialog_password; + bool b_store; +}; + +/** + * Init a credential struct + * + * @note to be cleaned with vlc_credential_clean() + * + * @param psz_url url to store or to search + */ +VLC_API void +vlc_credential_init(vlc_credential *p_credential, const vlc_url_t *p_url); + +/** + * Clean a credential struct + */ +VLC_API void +vlc_credential_clean(vlc_credential *p_credential); + +/** + * Get a username/password couple + * + * This will search for a credential using url, VLC options, the vlc_keystore + * or by asking the user via dialog_Login(). This function can be called + * indefinitely, it will first return the user/password from the url (if any), + * then from VLC options (if any), then from the keystore (if any), and finally + * from the dialog (if any). This function will return true as long as the user + * fill the dialog texts and will return false when the user cancel it. + * + * @param p_parent the parent object (for var, keystore and dialog) + * @param psz_option_username VLC option name for the username + * @param psz_option_password VLC option name for the password + * @param psz_dialog_title dialog title, if NULL, this function won't use the + * keystore or the dialog + * @param psz_dialog_fmt dialog text using format + * + * @return true if vlc_credential.psz_username and vlc_credential.psz_password + * are valid, otherwise this function should not be called again. + */ + +VLC_API bool +vlc_credential_get(vlc_credential *p_credential, vlc_object_t *p_parent, + const char *psz_option_username, + const char *psz_option_password, + const char *psz_dialog_title, + const char *psz_dialog_fmt, ...) VLC_FORMAT(6, 7); +#define vlc_credential_get(a, b, c, d, e, f, ...) \ + vlc_credential_get(a, VLC_OBJECT(b), c, d, e, f, ##__VA_ARGS__) + +/** + * Store the last dialog credential returned by vlc_credential_get() + * + * This function will store the credential only if it comes from the dialog and + * if the vlc_keystore object is valid. + * + * @return true if credential was stored, false otherwise + */ +VLC_API bool +vlc_credential_store(vlc_credential *p_credential); + /** * @} * @defgroup keystore_implementation Implemented by keystore modules diff --git a/src/libvlccore.sym b/src/libvlccore.sym index e624fcbd41..e1d6aba491 100644 --- a/src/libvlccore.sym +++ b/src/libvlccore.sym @@ -519,6 +519,10 @@ vlc_cond_init_daytime vlc_cond_signal vlc_cond_timedwait vlc_cond_wait +vlc_credential_init +vlc_credential_clean +vlc_credential_get +vlc_credential_store vlc_sem_init vlc_sem_destroy vlc_sem_post diff --git a/src/misc/keystore.c b/src/misc/keystore.c index b89c679500..eb20214b0b 100644 --- a/src/misc/keystore.c +++ b/src/misc/keystore.c @@ -23,8 +23,10 @@ #endif #include +#include #include #include +#include #include #include @@ -120,5 +122,365 @@ vlc_keystore_release_entries(vlc_keystore_entry *p_entries, unsigned int i_count free(p_entry->ppsz_values[j]); free(p_entry->p_secret); } - free (p_entries); + free(p_entries); +} + +static vlc_keystore_entry * +find_closest_path(vlc_keystore_entry *p_entries, unsigned i_count, + const char *psz_path) +{ + vlc_keystore_entry *p_match_entry = NULL; + size_t i_last_pathlen = 0; + + /* Try to find the entry that has the closest path to psz_url */ + for (unsigned int i = 0; i < i_count; ++i) + { + vlc_keystore_entry *p_entry = &p_entries[i]; + const char *psz_entry_path = p_entry->ppsz_values[KEY_PATH]; + size_t i_entry_pathlen = strlen(psz_entry_path); + + if (strncasecmp(psz_path, psz_entry_path, i_entry_pathlen) == 0 + && i_entry_pathlen > i_last_pathlen) + { + i_last_pathlen = i_entry_pathlen; + p_match_entry = p_entry; + } + } + return p_match_entry; +} + +static bool +is_credential_valid(vlc_credential *p_credential) +{ + return p_credential->psz_username && *p_credential->psz_username != '\0' + && p_credential->psz_password; +} + +/* Default port for each protocol */ +static struct +{ + const char * psz_protocol; + unsigned int i_port; +} protocol_default_ports [] = { + { "rtsp", 80 }, + { "http", 80 }, + { "https", 443 }, + { "ftp", 21 }, + { "sftp", 22 }, + { "smb", 445 }, +}; + +/* Don't store a port if it's the default one */ +static bool +protocol_is_default_port(const vlc_url_t *p_url) +{ + for (unsigned int i = 0; i < sizeof(protocol_default_ports) + / sizeof(*protocol_default_ports); ++i) + { + if (p_url->i_port == protocol_default_ports[i].i_port + && strcasecmp(p_url->psz_protocol, + protocol_default_ports[i].psz_protocol) == 0) + return true; + } + return false; +} + +static bool +protocol_is_smb(const vlc_url_t *p_url) +{ + return strcasecmp(p_url->psz_protocol, "smb") == 0; +} + +static bool +protocol_store_path(const vlc_url_t *p_url) +{ + return p_url->psz_path + && (strncasecmp(p_url->psz_protocol, "http", 4) == 0 + || strcasecmp(p_url->psz_protocol, "rtsp") == 0 + || protocol_is_smb(p_url)); +} + +/* Split domain;user in userinfo */ +static void +smb_split_domain(vlc_credential *p_credential) +{ + char *psz_delim = strchr(p_credential->psz_username, ';'); + if (psz_delim) + { + size_t i_len = psz_delim - p_credential->psz_username; + if (i_len > 0) + { + p_credential->psz_split_realm = strndup(p_credential->psz_username, + i_len); + p_credential->psz_realm = p_credential->psz_split_realm; + } + p_credential->psz_username = psz_delim + 1; + } +} + +static void +credential_find_keystore(vlc_credential *p_credential) +{ + const vlc_url_t *p_url = p_credential->p_url; + + const char *ppsz_values[KEY_MAX] = { 0 }; + ppsz_values[KEY_PROTOCOL] = p_url->psz_protocol; + ppsz_values[KEY_USER] = p_credential->psz_username; + ppsz_values[KEY_SERVER] = p_url->psz_host; + /* don't try to match with the path */ + ppsz_values[KEY_REALM] = p_credential->psz_realm; + ppsz_values[KEY_AUTHTYPE] = p_credential->psz_authtype; + char psz_port[21]; + if (p_url->i_port > 0 && !protocol_is_default_port(p_url)) + { + sprintf(psz_port, "%u", p_url->i_port); + ppsz_values[KEY_PORT] = psz_port; + } + + if (p_credential->i_entries_count > 0) + { + vlc_keystore_release_entries(p_credential->p_entries, + p_credential->i_entries_count); + p_credential->i_entries_count = 0; + } + + p_credential->i_entries_count = vlc_keystore_find(p_credential->p_keystore, + ppsz_values, + &p_credential->p_entries); + + if (p_credential->i_entries_count > 0) + { + vlc_keystore_entry *p_entry; + if (protocol_store_path(p_url)) + p_entry = find_closest_path(p_credential->p_entries, + p_credential->i_entries_count, + p_url->psz_path); + else + p_entry = &p_credential->p_entries[0]; + + if (!p_entry || p_entry->p_secret[p_entry->i_secret_len - 1] != '\0') + { + vlc_keystore_release_entries(p_credential->p_entries, + p_credential->i_entries_count); + p_credential->i_entries_count = 0; + } + else + { + p_credential->psz_password = (const char *)p_entry->p_secret; + p_credential->psz_username = p_entry->ppsz_values[KEY_USER]; + } + } +} + +void +vlc_credential_init(vlc_credential *p_credential, const vlc_url_t *p_url) +{ + assert(p_credential); + + memset(p_credential, 0, sizeof(*p_credential)); + p_credential->i_get_order = GET_FROM_URL; + p_credential->p_url = p_url; +} + +void +vlc_credential_clean(vlc_credential *p_credential) +{ + if (p_credential->p_keystore) + { + if (p_credential->i_entries_count > 0) + vlc_keystore_release_entries(p_credential->p_entries, + p_credential->i_entries_count); + vlc_keystore_release(p_credential->p_keystore); + } + + free(p_credential->psz_split_realm); + free(p_credential->psz_var_username); + free(p_credential->psz_var_password); + free(p_credential->psz_dialog_username); + free(p_credential->psz_dialog_password); +} + +#undef vlc_credential_get +bool +vlc_credential_get(vlc_credential *p_credential, vlc_object_t *p_parent, + const char *psz_option_username, + const char *psz_option_password, + const char *psz_dialog_title, + const char *psz_dialog_fmt, ...) +{ + assert(p_credential && p_parent); + const vlc_url_t *p_url = p_credential->p_url; + + if (!p_url || !p_url->psz_protocol || !p_url->psz_host) + { + msg_Err(p_parent, "vlc_credential_get: invalid url"); + return false; + } + + /* Don't set username to NULL, we may want to use the last one set */ + p_credential->psz_password = NULL; + + while (!is_credential_valid(p_credential)) + { + /* First, fetch credential from URL (if any). + * Secondly, fetch credential from VLC Options (if any). + * Thirdly, fetch credential from keystore (if any) using user and realm + * previously set by the caller, the URL or by VLC Options. + * Finally, fetch credential from the dialog (if any). This last will be + * repeated until user cancel the dialog. */ + + switch (p_credential->i_get_order) + { + case GET_FROM_URL: + p_credential->psz_username = p_url->psz_username; + p_credential->psz_password = p_url->psz_password; + + if (p_credential->psz_password) + msg_Warn(p_parent, "Password in a URI is DEPRECATED"); + + if (p_url->psz_username && protocol_is_smb(p_url)) + smb_split_domain(p_credential); + p_credential->i_get_order++; + break; + + case GET_FROM_OPTION: + free(p_credential->psz_var_username); + free(p_credential->psz_var_password); + p_credential->psz_var_username = + p_credential->psz_var_password = NULL; + + if (psz_option_username) + p_credential->psz_var_username = + var_InheritString(p_parent, psz_option_username); + if (psz_option_password) + p_credential->psz_var_password = + var_InheritString(p_parent, psz_option_password); + + if (p_credential->psz_var_username) + p_credential->psz_username = p_credential->psz_var_username; + if (p_credential->psz_var_password) + p_credential->psz_password = p_credential->psz_var_password; + + p_credential->i_get_order++; + break; + + case GET_FROM_KEYSTORE: + if (!psz_dialog_title || !psz_dialog_fmt) + return false; + + if (p_credential->p_keystore == NULL) + p_credential->p_keystore = vlc_keystore_create(p_parent); + if (p_credential->p_keystore != NULL) + credential_find_keystore(p_credential); + + p_credential->i_get_order++; + break; + + default: + case GET_FROM_DIALOG: + if (!psz_dialog_title || !psz_dialog_fmt) + return false; + + free(p_credential->psz_dialog_username); + free(p_credential->psz_dialog_password); + p_credential->psz_dialog_username = + p_credential->psz_dialog_password = NULL; + + /* TODO: save previously saved username and print it in dialog */ + va_list ap; + char *psz_dialog_text; + va_start(ap, psz_dialog_fmt); + if (vasprintf(&psz_dialog_text, psz_dialog_fmt, ap) == -1) + { + va_end(ap); + return false; + } + va_end(ap); + dialog_Login(p_parent, &p_credential->psz_dialog_username, + &p_credential->psz_dialog_password, + psz_dialog_title, psz_dialog_text); + free(psz_dialog_text); + if (p_credential->psz_dialog_username + && p_credential->psz_dialog_password) + { + /* TODO: add a dialog option to know if the user want to store + * the credential */ + p_credential->b_store = true; + p_credential->psz_username = p_credential->psz_dialog_username; + p_credential->psz_password = p_credential->psz_dialog_password; + + if (protocol_is_smb(p_url)) + smb_split_domain(p_credential); + } + else + { + p_credential->psz_username = p_credential->psz_password = NULL; + return false; + } + break; + } + } + return is_credential_valid(p_credential); +} + +bool +vlc_credential_store(vlc_credential *p_credential) +{ + if (!p_credential->p_keystore || !p_credential->b_store) + return false; + + const vlc_url_t *p_url = p_credential->p_url; + + char *psz_path = NULL; + if (protocol_store_path(p_url) && (psz_path = strdup(p_url->psz_path))) + { + char *p_slash; + if (protocol_is_smb(p_url)) + { + /* Remove all characters after the first slash (store the share but + * not the path) */ + p_slash = strchr(psz_path + 1, '/'); + } + else + { + /* Remove all characters after the last slash (store the path + * without the filename) */ + p_slash = strrchr(psz_path + 1, '/'); + } + if (p_slash && psz_path != p_slash) + *p_slash = '\0'; + else + { + free(psz_path); + psz_path = NULL; + } + } + + const char *ppsz_values[KEY_MAX] = { 0 }; + ppsz_values[KEY_PROTOCOL] = p_url->psz_protocol; + ppsz_values[KEY_USER] = p_credential->psz_username; + ppsz_values[KEY_SERVER] = p_url->psz_host; + ppsz_values[KEY_PATH] = psz_path; + ppsz_values[KEY_REALM] = p_credential->psz_realm; + ppsz_values[KEY_AUTHTYPE] = p_credential->psz_authtype; + + char psz_port[21]; + if (p_url->i_port > 0 && !protocol_is_default_port(p_url)) + { + sprintf(psz_port, "%u", p_url->i_port); + ppsz_values[KEY_PORT] = psz_port; + } + + char *psz_label; + if (asprintf(&psz_label, "LibVLC password for %s://%s%s", + p_url->psz_protocol, p_url->psz_host, + psz_path ? psz_path : "") == -1) + return false; + + bool b_ret = vlc_keystore_store(p_credential->p_keystore, ppsz_values, + (const uint8_t *)p_credential->psz_password, + -1, psz_label) == VLC_SUCCESS; + free(psz_label); + free(psz_path); + return b_ret; }