make git repository for git upload. quick code presented without warranty or support.
commit
87159c46c4
|
@ -0,0 +1,2 @@
|
||||||
|
pam_firstcomefirstserve.so: main.cpp
|
||||||
|
g++ -shared -fPIC -o $@ $^ -lsqlite3
|
|
@ -0,0 +1,249 @@
|
||||||
|
#include <security/pam_modules.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <string>
|
||||||
|
#include <sys/stat.h> // for mkdir
|
||||||
|
#include <unistd.h> // for chown
|
||||||
|
#include <errno.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
#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
|
|
@ -0,0 +1 @@
|
||||||
|
sudo proftpd -c $PWD/proftpd.conf -n -d10
|
|
@ -0,0 +1,8 @@
|
||||||
|
AuthPAM on
|
||||||
|
# configures which file in /etc/pam.d is used for proftpd access check
|
||||||
|
AuthPAMConfig ftp-upload
|
||||||
|
# ONLY authenticate with pam
|
||||||
|
AuthOrder mod_auth_pam.c
|
||||||
|
|
||||||
|
Trace auth:10 auth.pam:10
|
||||||
|
TraceLog /dev/tty
|
|
@ -0,0 +1 @@
|
||||||
|
sudo proftpd -c $PWD/proftpd.conf -n
|
|
@ -0,0 +1,31 @@
|
||||||
|
# ONLY authenticate with pam, but sql provides the user information lookup.
|
||||||
|
AuthOrder mod_auth_pam.c* mod_sql.c
|
||||||
|
|
||||||
|
AuthPAM on
|
||||||
|
# configures which file in /etc/pam.d is used for proftpd access check
|
||||||
|
AuthPAMConfig ftp-upload
|
||||||
|
|
||||||
|
# /etc/pam.d looks like this:
|
||||||
|
#auth required /home/user/projects/pam_firstcomefirstserve/pam_firstcomefirstserve.so /home/user/projects/pam_firstcomefirstserve/proftpd_test_2/auth.db /srv/testupload 65534 65534
|
||||||
|
#account required pam_permit.so
|
||||||
|
#password required pam_permit.so
|
||||||
|
#session required pam_permit.so
|
||||||
|
|
||||||
|
# pam_firstcomefirstserve.so manages the passwords, and creates home directories with the specified uid/gid.
|
||||||
|
|
||||||
|
# PAM doesn't look up user information, so SQL is configured for that.
|
||||||
|
|
||||||
|
SQLAuthenticate users
|
||||||
|
SQLBackend sqlite
|
||||||
|
#It can be any sqlite database since we don't use any data.
|
||||||
|
SQLConnectInfo /home/user/projects/pam_firstcomefirstserve/proftpd_test_2/auth.db
|
||||||
|
SQLEngine auth
|
||||||
|
SQLUserInfo custom:/get-user-by-name
|
||||||
|
# Even though we return password 'hunter2', login with this password fails because the * AuthOrder means that PAM auth has to succeed.
|
||||||
|
SQLNamedQuery get-user-by-name select "'%U','hunter2',65534,65534,'/srv/testupload/%U','/bin/sh'"
|
||||||
|
|
||||||
|
# Restrict users to their home directories. This is the only thing that stops users accessing each others' files.
|
||||||
|
DefaultRoot ~
|
||||||
|
|
||||||
|
#Trace auth:10 auth.pam:10
|
||||||
|
#TraceLog /dev/tty
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/sbin/sshd
|
|
@ -0,0 +1 @@
|
||||||
|
pam service name is name of sshd binary, so we provide a symlink. sshd must run as root to provide pam.
|
|
@ -0,0 +1,2 @@
|
||||||
|
sudo killall ftp-upload
|
||||||
|
sudo $PWD/ftp-upload -f sshd_config -e -D -d
|
|
@ -0,0 +1,19 @@
|
||||||
|
Port 5022
|
||||||
|
ListenAddress 127.0.0.1
|
||||||
|
ListenAddress ::1
|
||||||
|
|
||||||
|
HostKey /home/user/projects/pam_firstcomefirstserve/sshd_test/ssh_host_ed25519_key
|
||||||
|
# with PAM doesn't kbd-interactive follow the design better? we see if password can work anyway
|
||||||
|
PasswordAuthentication yes
|
||||||
|
PermitEmptyPasswords no
|
||||||
|
KbdInteractiveAuthentication no
|
||||||
|
AuthenticationMethods password
|
||||||
|
UsePAM yes
|
||||||
|
|
||||||
|
Subsystem sftp internal-sftp
|
||||||
|
#ForceCommand /usr/lib64/misc/sftp-server -d /%u
|
||||||
|
ChrootDirectory /srv/testupload
|
||||||
|
X11Forwarding no
|
||||||
|
AllowTcpForwarding no
|
||||||
|
AllowAgentForwarding no
|
||||||
|
ForceCommand internal-sftp -d /%u
|
|
@ -0,0 +1,25 @@
|
||||||
|
anonymous_enable=NO
|
||||||
|
local_enable=YES
|
||||||
|
write_enable=YES
|
||||||
|
|
||||||
|
# Activate logging of uploads/downloads.
|
||||||
|
xferlog_enable=YES
|
||||||
|
xferlog_file=/dev/stderr
|
||||||
|
|
||||||
|
# It is recommended that you define on your system a unique user which the
|
||||||
|
# ftp server can use as a totally isolated and unprivileged user.
|
||||||
|
#nopriv_user=ftpsecure
|
||||||
|
|
||||||
|
ftpd_banner=test ftp service for pam_firstcomefirstserve
|
||||||
|
|
||||||
|
# When "listen" directive is enabled, vsftpd runs in standalone mode and
|
||||||
|
# listens on IPv4 sockets. This directive cannot be used in conjunction
|
||||||
|
# with the listen_ipv6 directive.
|
||||||
|
listen=YES
|
||||||
|
#
|
||||||
|
# This directive enables listening on IPv6 sockets. To listen on IPv4 and IPv6
|
||||||
|
# sockets, you must run two copies of vsftpd with two configuration files.
|
||||||
|
# Make sure, that one of the listen options is commented !!
|
||||||
|
#listen_ipv6=YES
|
||||||
|
|
||||||
|
#pam_service_name=ftp-upload
|
Loading…
Reference in New Issue