make git repository for git upload. quick code presented without warranty or support.

master
immibis 2025-01-13 18:19:50 +01:00
commit 87159c46c4
11 changed files with 340 additions and 0 deletions

2
Makefile Normal file
View File

@ -0,0 +1,2 @@
pam_firstcomefirstserve.so: main.cpp
g++ -shared -fPIC -o $@ $^ -lsqlite3

249
main.cpp Normal file
View File

@ -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

1
proftpd_test/command Normal file
View File

@ -0,0 +1 @@
sudo proftpd -c $PWD/proftpd.conf -n -d10

View File

@ -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

1
proftpd_test_2/command Normal file
View File

@ -0,0 +1 @@
sudo proftpd -c $PWD/proftpd.conf -n

View File

@ -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

1
sshd_test/ftp-upload Symbolic link
View File

@ -0,0 +1 @@
/usr/sbin/sshd

View File

@ -0,0 +1 @@
pam service name is name of sshd binary, so we provide a symlink. sshd must run as root to provide pam.

2
sshd_test/sshd_command Normal file
View File

@ -0,0 +1,2 @@
sudo killall ftp-upload
sudo $PWD/ftp-upload -f sshd_config -e -D -d

19
sshd_test/sshd_config Normal file
View File

@ -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

25
vsftpd_test/vsftpd.conf Normal file
View File

@ -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