#include #include #include #include #include #include #include // for mkdir #include // for chown #include #include #include #define MYNAME "pam_firstcomefirstserve" struct database { sqlite3 *sql = nullptr; ~database() { if(sql) { if(SQLITE_OK != sqlite3_close(sql)) { fprintf(stderr, MYNAME ": sqlite3_close failed: %s\n", sqlite3_errmsg(sql)); // still continue, but with leaking memory } sql = nullptr; } } bool commit(); database() {} database(database const&) = delete; database& operator=(database const&) = delete; }; // error handling designed so that if an error occurs, all operations are ignored and then you check once at the end. struct statement { sqlite3_stmt *stmt = nullptr; sqlite3 *db; bool ok = true; statement(database& db, const char *command) { this->db = db.sql; if(SQLITE_OK != sqlite3_prepare_v2(db.sql, command, -1, &stmt, nullptr)) { fprintf(stderr, MYNAME ": %s: %s\n", command, sqlite3_errmsg(db.sql)); ok = false; } } // sqlite3_step returns an error, finished, or a row data. bool step() { // returns false if statement is finished (possibly with error), or true if we paused because another row is available to read from the results if(!stmt || !ok) return false; int result = sqlite3_step(stmt); if(result == SQLITE_ROW) return true; if(result != SQLITE_DONE) { fprintf(stderr, MYNAME ": %s\n", sqlite3_errmsg(db)); ok = false; } return false; } void bind(int paramNum, std::string const& string) { // string must outlive statement object is destroyed (that's what SQLITE_STATIC means) if(!stmt || !ok) return; if(SQLITE_OK != sqlite3_bind_text(stmt, paramNum, string.data(), string.size(), SQLITE_STATIC)) { fprintf(stderr, MYNAME ": sqlite3_bind_text: %s\n", sqlite3_errmsg(db)); ok = false; } } // undefined results (including crash) if row data is accessed any time except after step returns true, or if type doesn't match // note: parameters start at 1, but columns start at 0 std::string getColumnText(int columnNum) { const char *data = (const char *)sqlite3_column_text(stmt, columnNum); int length = sqlite3_column_bytes(stmt, columnNum); return std::string(data, data+length); } ~statement() { if(stmt) { if(sqlite3_finalize(stmt) != SQLITE_OK) { fprintf(stderr, MYNAME ": sqlite3_finalize: %s\n", sqlite3_errmsg(db)); // leak memory, but continue anyway } stmt = nullptr; } } statement(statement const&) = delete; statement& operator=(statement const&) = delete; }; bool database::commit() { statement st(*this, "commit"); while(st.step()) {} // should complete in one step return st.ok; } int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct pam_conv *conv; if(PAM_SUCCESS != pam_get_item(pamh, PAM_CONV, (const void**)&conv)) { fprintf(stderr, MYNAME ": auth: pam_get_item(PAM_CONV) failed (programming error in application)\n"); return PAM_AUTHINFO_UNAVAIL; } const char *username_; if(PAM_SUCCESS != pam_get_item(pamh, PAM_USER, (const void**)&username_)) { fprintf(stderr, MYNAME ": auth: pam_get_item(PAM_USER) failed (programming error in application)\n"); return PAM_AUTHINFO_UNAVAIL; } std::string username = username_; // Since we create directories based on usernames, we have to enforce some basic restrictions // so we don't create /srv/upload/../../etc/passwd or something. // Usernames are not empty, don't start with "." and contain alphanumeric characters, '_', '-' and '.'. for(char c : username) { if(!isalnum(c) && c != '_' && c != '.' && c != '-') { fprintf(stderr, MYNAME ": invalid username\n"); return PAM_AUTH_ERR; } } if(username.size() == 0 || username[0] == '.') { fprintf(stderr, MYNAME ": invalid username\n"); return PAM_AUTH_ERR; } // argv does not include the module name: argv[0] is the first argument, and argc is 0 if there are no arguments. if(argc != 4) { fprintf(stderr, MYNAME ": auth: wrong number of arguments. arguments must be database path, parent of user home directories, home dir UID and home dir GID\n"); return PAM_AUTHINFO_UNAVAIL; } int home_dir_uid = atoi(argv[2]), home_dir_gid = atoi(argv[3]); if(!home_dir_uid || !home_dir_gid) { // conveniently catches non-numbers and root fprintf(stderr, MYNAME ": auth: please check home uid/gid (args 3/4)\n"); return PAM_AUTHINFO_UNAVAIL; } std::string password; { struct pam_message msg = {PAM_PROMPT_ECHO_OFF, "Password: "}; const struct pam_message *msgs = {&msg}; struct pam_response *responses = NULL; if(PAM_SUCCESS != conv->conv(1, &msgs, &responses, conv->appdata_ptr)) { fprintf(stderr, MYNAME ": auth: password prompt failed\n"); // do not free responses in this case? "The application should not set *resp" return PAM_AUTHINFO_UNAVAIL; } password = responses[0].resp; free(responses[0].resp); free(responses); }; database db; if(SQLITE_OK != sqlite3_open(argv[0], &db.sql)) { fprintf(stderr, MYNAME ": auth: sqlite3_open failed\n"); return PAM_AUTHINFO_UNAVAIL; } if(SQLITE_OK != sqlite3_busy_timeout(db.sql, 10000)) { // milliseconds. Call should always succeed? fprintf(stderr, MYNAME ": auth: sqlite3_busy_timeout: %s\n", sqlite3_errmsg(db.sql)); } // Don't make any database changes until after reading the password, because SQLite may take a lock { statement st(db, "begin transaction"); while(st.step()) {} if(!st.ok) { fprintf(stderr, MYNAME ": auth: failed to begin database transaction\n"); return PAM_AUTHINFO_UNAVAIL; } } { statement st(db, "create table if not exists users (username varchar not null primary key, password varchar not null)"); while(st.step()) {} // should finish in one step, but anyway // no st.ok check - if an error occurred, it was already printed, and we continue anyway - // if the error is still present, we'll get the same error when accessing the data, anyway. } auto make_home_dir_and_subdirs = [&]() { auto make_home_dir = [&](std::string const& subdir) { std::string createpath = std::string(argv[1])+"/"+username; if(subdir != "") createpath += "/" + subdir; if(mkdir(createpath.c_str(), 0700) && errno != EEXIST) { fprintf(stderr, "mkdir %s: %s\n", createpath.c_str(), strerror(errno)); } if(chown(createpath.c_str(), home_dir_uid, home_dir_gid)) { fprintf(stderr, "chown %s: %s\n", createpath.c_str(), strerror(errno)); } }; make_home_dir(""); std::ifstream in("/srv/ftp-subdirs"); std::string subdir; while(std::getline(in, subdir)) { if(subdir != "") make_home_dir(subdir); } }; { statement st(db, "select password from users where username = ?1"); st.bind(1, username); if(st.step()) { // Found the username. Does their password match? std::string right_pw = st.getColumnText(0); if(right_pw == password) { make_home_dir_and_subdirs(); return PAM_SUCCESS; } else { return PAM_AUTH_ERR; } } else if(st.ok) { // New user! (Query succeeded, but didn't find any row) statement st2(db, "insert into users (username, password) values (?1, ?2)"); st2.bind(1, username); st2.bind(2, password); while(st2.step()) {} if(st2.ok && db.commit()) { make_home_dir_and_subdirs(); return PAM_SUCCESS; } } } return PAM_AUTHINFO_UNAVAIL; } int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { return PAM_SUCCESS; } #if 0 int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { } int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { } int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { } #endif