#include "internal.h" // Stringify binary data. Output buffer must be twice as big as input, // because each byte takes 2 bytes in string representation static void bin2str(char *to, const unsigned char *p, size_t len) { static const char *hex = "0123456789abcdef"; for (; len--; p++) { *to++ = hex[p[0] >> 4]; *to++ = hex[p[0] & 0x0f]; } *to = '\0'; } // Return stringified MD5 hash for list of strings. Buffer must be 33 bytes. char *mg_md5(char buf[33], ...) { unsigned char hash[16]; const char *p; va_list ap; MD5_CTX ctx; MD5Init(&ctx); va_start(ap, buf); while ((p = va_arg(ap, const char *)) != NULL) { MD5Update(&ctx, (const unsigned char *) p, (unsigned) strlen(p)); } va_end(ap); MD5Final(hash, &ctx); bin2str(buf, hash, sizeof(hash)); return buf; } // Check the user's password, return 1 if OK static int check_password(const char *method, const char *ha1, const char *uri, const char *nonce, const char *nc, const char *cnonce, const char *qop, const char *response) { char ha2[32 + 1], expected_response[32 + 1]; // Some of the parameters may be NULL if (method == NULL || nonce == NULL || nc == NULL || cnonce == NULL || qop == NULL || response == NULL) { return 0; } // NOTE(lsm): due to a bug in MSIE, we do not compare the URI // TODO(lsm): check for authentication timeout if (// strcmp(dig->uri, c->ouri) != 0 || strlen(response) != 32 // || now - strtoul(dig->nonce, NULL, 10) > 3600 ) { return 0; } mg_md5(ha2, method, ":", uri, NULL); mg_md5(expected_response, ha1, ":", nonce, ":", nc, ":", cnonce, ":", qop, ":", ha2, NULL); return mg_strcasecmp(response, expected_response) == 0; } // Use the global passwords file, if specified by auth_gpass option, // or search for .htpasswd in the requested directory. static FILE *open_auth_file(struct mg_connection *conn, const char *path) { char name[PATH_MAX]; const char *p, *e, *gpass = conn->ctx->config[GLOBAL_PASSWORDS_FILE]; struct file file = STRUCT_FILE_INITIALIZER; FILE *fp = NULL; if (gpass != NULL) { // Use global passwords file fp = mg_fopen(gpass, "r"); // Important: using local struct file to test path for is_directory flag. // If filep is used, mg_stat() makes it appear as if auth file was opened. } else if (mg_stat(path, &file) && file.is_directory) { mg_snprintf(name, sizeof(name), "%s%c%s", path, '/', PASSWORDS_FILE_NAME); fp = mg_fopen(name, "r"); } else { // Try to find .htpasswd in requested directory. for (p = path, e = p + strlen(p) - 1; e > p; e--) if (e[0] == '/') break; mg_snprintf(name, sizeof(name), "%.*s%c%s", (int) (e - p), p, '/', PASSWORDS_FILE_NAME); fp = mg_fopen(name, "r"); } return fp; } // Parsed Authorization header struct ah { char *user, *uri, *cnonce, *response, *qop, *nc, *nonce; }; // Return 1 on success. Always initializes the ah structure. static int parse_auth_header(struct mg_connection *conn, char *buf, size_t buf_size, struct ah *ah) { char *name, *value, *s; const char *auth_header; (void) memset(ah, 0, sizeof(*ah)); if ((auth_header = mg_get_header(conn, "Authorization")) == NULL || mg_strncasecmp(auth_header, "Digest ", 7) != 0) { return 0; } // Make modifiable copy of the auth header (void) mg_strlcpy(buf, auth_header + 7, buf_size); s = buf; // Parse authorization header for (;;) { // Gobble initial spaces while (isspace(* (unsigned char *) s)) { s++; } name = skip_quoted(&s, "=", " ", 0); // Value is either quote-delimited, or ends at first comma or space. if (s[0] == '\"') { s++; value = skip_quoted(&s, "\"", " ", '\\'); if (s[0] == ',') { s++; } } else { value = skip_quoted(&s, ", ", " ", 0); // IE uses commas, FF uses spaces } if (*name == '\0') { break; } if (!strcmp(name, "username")) { ah->user = value; } else if (!strcmp(name, "cnonce")) { ah->cnonce = value; } else if (!strcmp(name, "response")) { ah->response = value; } else if (!strcmp(name, "uri")) { ah->uri = value; } else if (!strcmp(name, "qop")) { ah->qop = value; } else if (!strcmp(name, "nc")) { ah->nc = value; } else if (!strcmp(name, "nonce")) { ah->nonce = value; } } // CGI needs it as REMOTE_USER if (ah->user != NULL) { conn->request_info.remote_user = mg_strdup(ah->user); } else { return 0; } return 1; } // Authorize against the opened passwords file. Return 1 if authorized. static int authorize(struct mg_connection *conn, FILE *fp) { struct ah ah; char line[256], f_user[256], ha1[256], f_domain[256], buf[MG_BUF_LEN]; if (!parse_auth_header(conn, buf, sizeof(buf), &ah)) { return 0; } // Loop over passwords file while (fgets(line, sizeof(line), fp) != NULL) { if (sscanf(line, "%[^:]:%[^:]:%s", f_user, f_domain, ha1) != 3) { continue; } if (!strcmp(ah.user, f_user) && !strcmp(conn->ctx->config[AUTHENTICATION_DOMAIN], f_domain)) return check_password(conn->request_info.request_method, ha1, ah.uri, ah.nonce, ah.nc, ah.cnonce, ah.qop, ah.response); } return 0; } // Return 1 if request is authorised, 0 otherwise. static int check_authorization(struct mg_connection *conn, const char *path) { char fname[PATH_MAX]; struct vec uri_vec, filename_vec; const char *list; FILE *fp = NULL; int authorized = 1; list = conn->ctx->config[PROTECT_URI]; while ((list = next_option(list, &uri_vec, &filename_vec)) != NULL) { if (!memcmp(conn->request_info.uri, uri_vec.ptr, uri_vec.len)) { mg_snprintf(fname, sizeof(fname), "%.*s", (int) filename_vec.len, filename_vec.ptr); fp = mg_fopen(fname, "r"); break; } } if (fp == NULL) { fp = open_auth_file(conn, path); } if (fp != NULL) { authorized = authorize(conn, fp); fclose(fp); } return authorized; } static void send_authorization_request(struct mg_connection *conn) { conn->status_code = 401; mg_printf(conn, "HTTP/1.1 401 Unauthorized\r\n" "Content-Length: 0\r\n" "WWW-Authenticate: Digest qop=\"auth\", " "realm=\"%s\", nonce=\"%lu\"\r\n\r\n", conn->ctx->config[AUTHENTICATION_DOMAIN], (unsigned long) time(NULL)); } static int is_authorized_for_put(struct mg_connection *conn) { const char *passfile = conn->ctx->config[PUT_DELETE_PASSWORDS_FILE]; FILE *fp; int ret = 0; if (passfile != NULL && (fp = mg_fopen(passfile, "r")) != NULL) { ret = authorize(conn, fp); fclose(fp); } return ret; } int mg_modify_passwords_file(const char *fname, const char *domain, const char *user, const char *pass) { int found; char line[512], u[512], d[512], ha1[33], tmp[PATH_MAX]; FILE *fp, *fp2; found = 0; fp = fp2 = NULL; // Regard empty password as no password - remove user record. if (pass != NULL && pass[0] == '\0') { pass = NULL; } (void) snprintf(tmp, sizeof(tmp), "%s.tmp", fname); // Create the file if does not exist if ((fp = fopen(fname, "a+")) != NULL) { fclose(fp); } // Open the given file and temporary file if ((fp = fopen(fname, "r")) == NULL) { return 0; } else if ((fp2 = fopen(tmp, "w+")) == NULL) { fclose(fp); return 0; } // Copy the stuff to temporary file while (fgets(line, sizeof(line), fp) != NULL) { if (sscanf(line, "%[^:]:%[^:]:%*s", u, d) != 2) { continue; } if (!strcmp(u, user) && !strcmp(d, domain)) { found++; if (pass != NULL) { mg_md5(ha1, user, ":", domain, ":", pass, NULL); fprintf(fp2, "%s:%s:%s\n", user, domain, ha1); } } else { fprintf(fp2, "%s", line); } } // If new user, just add it if (!found && pass != NULL) { mg_md5(ha1, user, ":", domain, ":", pass, NULL); fprintf(fp2, "%s:%s:%s\n", user, domain, ha1); } // Close files fclose(fp); fclose(fp2); // Put the temp file in place of real file remove(fname); rename(tmp, fname); return 1; }