/** * wsgi-admin, by Aaron Bloomfield, (c) 2014-2016 * * This is part of the SLP repository * (https://github.com/aaronbloomfield/slp), and is released under a * CC BY-SA license, like the rest of the repository. * * This program will help manage the registration and de-registration * of wsgi files. It needs two Ubuntu pacakges to work: sqlite3 and * libsqlite3-dev. * * This program will take, as a command-line parameter, a wsgi file * (such as Django's wsgi.py), and either add it to, or remove it * from, a sqlite db. The django.conf for apache is then * re-generated, checking each existing wsgi file to make sure they * are valid. * * Full directions for how to use it can be found in the * django-getting-started.md (and .html) files in the docs/ directory * in the SLP repo. * * Installation: * - Compile with 'make', put it somewhere (such as /usr/local/bin) * - You will need this to be in a group that can write to the two * conf files (in the directories declared in the #defines, below) * - Run 'chmod 4755 wsgi-admin': this will set the set-uid bit when * the program runs. This means the program will run as the user * who owns it, who (presumably) can edit those files. * - the django.conf file needs to be writable by the user who owns * the compiled file * - the DB file also needs to be writable by that user * * Note that the app name is no longer used (it was previously used as * part of the static directory), but has not been removed from this * program yet. * * The callback functions were adapted from * http://www.sqlite.org/quickstart.html * * * Apache2 Configuration * * Note that that multiple apache2 sites (such as http:// and * https://) can not *both* define the WSGIDaemonProcess. The site * that is brought up first (likely http://) must define it. For that * reason, that command is enclosed in an <IfModule> command. To * include it in the http:// site (likely 000-default.conf), have the * following stanza: * * * <IfDefine NoDaemonProcess> * UnDefine NoDaemonProcess * </IfDefine> * Include django.conf * * For the htts:// site (likely default-ssl.conf), have the following * stanza: * * Define NoDaemonProcess * Include django.conf * * * DB schema: * * id int auto_increment primary key * uid int * wsgi text * valid boolean * added datetime * removed datetime * app text * rootdir boolean default 0 * staticdir text * urluserid text * morepath text * * The line to create this in sqlite3: * create table wsgi(id integer primary key asc, uid int, wsgi text, valid boolean, added datetime, removed datetime, app text, rootdir boolean default 0, staticdir text, urluserid text); * * Note that the urluserid allows for the URL to be different than the * userid; if not set, it defaults to the userid. While this program * does pay attention to that value, it can not be set via this * program, and must be manually set in the django.db. */ #include <iostream> #include <sstream> #include <stdlib.h> #include <fstream> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <sqlite3.h> #include <stdio.h> #include <string.h> #include <pwd.h> using namespace std; // constants to change as per your system configuration #define DJANGO_CONF_FILE "/etc/apache2/django.conf" #define APACHE2_RELOAD_CMD "/usr/local/bin/reload-apache2" #define URL_PREFIX "/django" #define DATABASE_FILE "/etc/apache2/django.db" #define DEFAULT_APP_NAME "polls" // some global variables stringstream wsgifile, query; int count = 0, find_uid = 0, reg_root = 0, remove_id = 0, force_uid = 0; string find_filename = "", appname = string(DEFAULT_APP_NAME), wsgi_file_name, staticdir, morepath; bool check_file = true, compact_list = false, show_all = false; // for when they invoke it incorrectly... void printUsage(char *argv0, bool doexit = true) { cerr << "Usage: " << argv0 << " -register -file <wsgi_file> [-app <app_name>] [-root] [-path <virtual_env_path>]\n" << "\t" << argv0 << " -remove -id <num>\n" << "\t" << argv0 << " -list [-compact]\n" << "\t" << argv0 << " -regenrate" << endl; if ( doexit ) exit(0); } // prints an error message with the query, deallocates that error message, and exits void sqlite3_die(char *errmsg, string query) { cerr << "SQL error: " << errmsg << " on query: " << query << endl; sqlite3_free(errmsg); exit(0); } // prints the error message and exits void die(string s) { cerr << "Error: " << s << endl; exit(0); } // finds who owns a file int get_UID_for_file (const char* filename) { struct stat buf; int uid = stat(filename,&buf); return buf.st_uid; } // maps a UID to a Unix user name char* get_userid_by_uid (int uid) { struct passwd *p = getpwuid(uid); if ( p == NULL ) { cerr << "Unknown user with uid " << uid << endl; exit(0); } return p->pw_name; } // checks if a file exists; this function adapted from //http://stackoverflow.com/questions/12774207/fastest-way-to-check-if-a-file-exist-using-standard-c-c11-c inline bool file_exists (const std::string& name) { ifstream f(name.c_str()); if (f.good()) { f.close(); return true; } else { f.close(); return false; } } // checks if the passed file is a valid WSGI file (really if it's a Python script) bool is_valid_wsgi_file(const char *filename) { // get the file type stringstream cmd; cmd << "/usr/bin/file " << filename; FILE *fp = popen(cmd.str().c_str(),"r"); if ( fp == NULL ) die ("Failed to run 'file' command"); char result[1024]; fgets(result, sizeof(result)-1, fp); pclose(fp); // check that the output was correct string obtained(result); stringstream expected; expected << filename << ": Python script, ASCII text executable\n"; if ( obtained == expected.str() ) return true; expected.str(""); expected << filename << ": Python script, ASCII text executable, with CRLF line terminators\n"; return ( obtained == expected.str() ); } // how to handle the data returned when displaying the list of the DB entries static int list_callback(void *NotUsed, int argc, char **argv, char **azColName) { for( int i = 0; i < argc; i++ ) { if ( compact_list ) { cout << azColName[i] << " = " << (argv[i] ? argv[i] : "NULL") << "; "; } else { if ( strcmp(azColName[i],"id") ) cout << "\t"; cout << azColName[i] << " = " << (argv[i] ? argv[i] : "NULL"); if ( !strcmp(azColName[i],"uid") ) { int uid; sscanf(argv[i],"%d",&uid); cout << " (" << get_userid_by_uid(uid) << ")"; } cout << endl; } } cout << endl; return 0; } // gets the count(*) field and saves it in a global variable static int count_callback(void *NotUsed, int argc, char **argv, char **azColName) { if ( argc != 1 ) die("count_callback returned multiple columns!"); if ( strcmp(azColName[0],"count(*)") ) die("count_callback not provided a count(*) column"); sscanf(argv[0],"%d",&count); return 0; } // this callback allows searching for the 'wsgi' and 'uid' fields in the DB static int find_callback(void *NotUsed, int argc, char **argv, char **azColName) { if ( argc < 2 ) die("invalid result set to find_callback (1)"); if ( strcmp(azColName[2],"wsgi") ) die("invalid result set to find_callback (2)"); find_filename = string(argv[2]); if ( strcmp(azColName[1],"uid") ) die("invalid result set to find_callback (3)"); sscanf(argv[1],"%d",&find_uid); return 0; } // this regenerates the django.conf files static int regenerate_callback(void *NotUsed, int argc, char **argv, char **azColName) { int uid, rootdir; sscanf(argv[1],"%d",&uid); char *filename = argv[2]; if ( !file_exists(argv[2]) ) return 0; if ( !is_valid_wsgi_file(argv[2]) ) return 0; sscanf(argv[7], "%d", &rootdir); string userid(get_userid_by_uid(uid)); // userid of who registered this string fullpath(realpath(filename,NULL)); // full path of the wsgi.py file int pos = fullpath.rfind("/"); string up1 = fullpath.substr(0,pos); // the directory that the wsgi.py file is in string basename = fullpath.substr(pos+1); // typically 'wsgi.py', the actual file name string app(argv[6]); // the app name pos = up1.rfind("/"); string up2 = up1.substr(0,pos); // the directory *above* the wsgi.py file string staticdir; // the static directory, which is determined below stringstream foo; if ( (argv[8] == NULL) || (strlen(argv[8]) == 0) ) { // the assumption is that there is a static/ directory in the main project directory... foo << up2 << "/static"; staticdir = foo.str(); } else /// ... unless they have specified otherwise staticdir = string(argv[8]); string urluserid; if ( (argv[9] == NULL) || (strlen(argv[9]) == 0) ) // the assumption is that url userid is the same as their userid urluserid = userid; else /// ... unless they have specified otherwise urluserid = string(argv[9]); string path = up2; wsgifile << "Alias " << (rootdir?"":URL_PREFIX) << "/" << urluserid << "/static " << staticdir << "\n" << "<Directory " << staticdir << ">\n" << " Require all granted\n" << "</Directory>\n"; wsgifile << "WSGIScriptAlias " << (rootdir?"":URL_PREFIX) << "/" << urluserid << " " << fullpath << "\n"; wsgifile << "<IfDefine !NoDaemonProcess>\n" << " WSGIDaemonProcess " << urluserid << " user=" << urluserid << " python-path=" << path; if ( (argv[10] != NULL) && (strlen(argv[10]) != 0) ) wsgifile << " python-home=" << argv[10]; wsgifile << "\n</IfDefine>\n"; wsgifile << "<Location " << (rootdir?"":URL_PREFIX) << "/" << urluserid << ">\n" << " WSGIProcessGroup " << urluserid << "\n" << "</Location>\n" << "<Directory " << up1 << ">\n" << " <Files " << basename << ">\n" << " Require all granted\n" << " </Files>\n" << "</Directory>\n\n"; return 0; } // what does the main() say? int main(int argc, char **argv) { int uid = getuid(); // get user's uid setuid(0); // ensure we are running as root (requires SUID bit set as well) enum { MODE_NONE, MODE_REGISTER, MODE_REMOVE, MODE_REGENERATE, MODE_LIST } mode = MODE_NONE; char *filename = NULL; int ret; if ( argc == 1 ) printUsage(argv[0]); // parse command line parameters, get wsgi file and mode for ( int i = 1; i < argc; i++ ) { string param(argv[i]); if ( param == "-help" ) printUsage(argv[0]); else if ( param == "-register" ) mode = MODE_REGISTER; else if ( param == "-remove" ) mode = MODE_REMOVE; else if ( param == "-regenerate" ) mode = MODE_REGENERATE; else if ( param == "-compact" ) compact_list = true; else if ( param == "-root" ) { //if ( (uid != 0) && (uid != 1000) ) //die ("You are not allowed to use the -root flag"); reg_root = 1; } else if ( param == "-list" ) mode = MODE_LIST; else if ( param == "-all" ) { if ( (uid != 0) && (uid != 1000) ) die ("You are not allowed to use the -all flag"); show_all = true; } else if ( param == "-staticdir" ) { if ( argc == i+1 ) die ("Must supply a directory name to -staticdir"); staticdir = string(argv[++i]); } else if ( (param == "-path") || (param == "-morepath") ) { if ( argc == i+1 ) die ("Must supply a directory name to -path or -morepath"); morepath = string(argv[++i]); } else if ( param == "-nocheck" ) { if ( (uid != 0) && (uid != 1000) ) die ("You are not allowed to use the -nocheck flag"); check_file = false; } else if ( param == "-app" ) { if ( argc == i+1 ) die ("Must supply a app name to -app"); appname = string(argv[++i]); } else if ( param == "-uid" ) { if ( (uid != 0) && (uid != 1000) ) die ("You are not allowed to use the -uid flag"); int ret = sscanf(argv[++i], "%d", &force_uid); if ( ret != 1 ) die ("Invalid integer format to -uid"); } else if ( param == "-id" ) { if ( argc == i+1 ) die ("Must supply a numerical id to -id"); int ret = sscanf(argv[++i], "%d", &remove_id); if ( ret != 1 ) die ("Invalid integer format to -id"); } else if ( param == "-file" ) { if ( argc == i+1 ) die ("Must supply a file name to -file"); wsgi_file_name = string(argv[++i]); if ( !file_exists(wsgi_file_name) ) die ("File does not exist!"); } else printUsage(argv[0]); } // check the parameters if ( (wsgi_file_name == "") && (mode == MODE_REGISTER) ) die("Must supply a file name with -register"); if ( (remove_id == 0) && (mode == MODE_REMOVE) ) die("Must supply a id (via -id) with -remove"); // open the DB sqlite3 *db; char *errmsg = NULL; if ( !file_exists(DATABASE_FILE) ) die("database file does not exist"); ret = sqlite3_open(DATABASE_FILE,&db); if ( ret != SQLITE_OK ) die("Can't open the DB"); // register a new entry if ( mode == MODE_REGISTER ) { // sanity check the file name for ( int i = 0; i < wsgi_file_name.length(); i++ ) if ( (wsgi_file_name[i] == '\\') || (wsgi_file_name[i] == '"') || (wsgi_file_name[i] == '\'') || (wsgi_file_name[i] == ';') ) die ("Try choosing a file name that does *not* lend itself to SQL injection attacks"); // does the file exist? if ( !file_exists(wsgi_file_name) ) { cerr << "File '" << wsgi_file_name << "' does not exist!" << endl; exit(0); } // is it a Python file? if ( check_file && !is_valid_wsgi_file(wsgi_file_name.c_str()) ) die("Not a valid WSGI file"); // is it owned by the user executing this script? int fuid = get_UID_for_file (wsgi_file_name.c_str()); if ( force_uid != 0 ) fuid = force_uid; if ( (uid != 0) && (uid != 1000) && (fuid != uid) ) die ("You cannot register a file that you do not own"); // check for duplicate entries of that file query.str(""); query << "select count(*) from wsgi where wsgi=\"" << realpath(wsgi_file_name.c_str(),NULL) << "\" and valid=1"; count = 0; ret = sqlite3_exec(db, query.str().c_str(), count_callback, 0, &errmsg); if ( ret != SQLITE_OK ) sqlite3_die(errmsg,query.str()); if ( count > 0 ) die("This wsgi file has already been registered"); // check for existing entries by this UID query.str(""); query << "select count(*) from wsgi where uid=" << uid << " and valid=1"; count = 0; ret = sqlite3_exec(db, query.str().c_str(), count_callback, 0, &errmsg); if ( ret != SQLITE_OK ) sqlite3_die(errmsg,query.str()); if ( count > 0 ) die("You can only have one WSGI file registered, so you must first remove it via -remove (and you can find it via -list)"); // we are not using the app name anymore (see the comments at the // top of this file), so there is no need anymore to warn about // using the default app name... //if ( appname == string(DEFAULT_APP_NAME) ) //cout << "Using the default app name of '" << DEFAULT_APP_NAME << "'" << endl; // insert entry into DB query.str(""); query << "insert into wsgi values (null," << uid << ",\"" << realpath(wsgi_file_name.c_str(),NULL) << "\",1,datetime(),null,\"" << appname << "\"," << reg_root << ",\"" << staticdir << "\",\"\",\"" << morepath << "\")"; ret = sqlite3_exec(db, query.str().c_str(), NULL, NULL, &errmsg); if ( ret != SQLITE_OK ) sqlite3_die(errmsg,query.str()); cout << "entry registered as /" << (reg_root?"":"django/") << get_userid_by_uid(uid) << endl; } // list entries in the DB if ( mode == MODE_LIST ) { query.str(""); query << "select * from wsgi"; if ( !show_all ) query << " where valid=1"; if ( (uid != 0) && (uid != 1000) ) // uids 0 and 1000 see all... query << " and uid=" << uid; ret = sqlite3_exec(db, query.str().c_str(), list_callback, 0, &errmsg); if ( ret != SQLITE_OK ) sqlite3_die(errmsg,query.str()); } // remove an entry from the DB if ( mode == MODE_REMOVE ) { // create query, and pull that data from the DB query.str(""); query << "select * from wsgi where id=" << remove_id << " and valid=1"; find_filename = string(""); ret = sqlite3_exec(db, query.str().c_str(), find_callback, 0, &errmsg); if ( ret != SQLITE_OK ) sqlite3_die(errmsg,query.str()); if ( find_filename == "" ) die("no such entry exists"); // check if UID matches if ( (uid != 0) && (uid != 1000) ) if ( uid != find_uid ) die("you can't remove that entry"); // remove entry query.str(""); query << "update wsgi set valid=0, removed=datetime() where id=" << remove_id; ret = sqlite3_exec(db, query.str().c_str(), NULL, NULL, &errmsg); if ( ret != SQLITE_OK ) sqlite3_die(errmsg,query.str()); cout << "entry removed" << endl; } // all of these perform the generation query.str(""); query << "select * from wsgi where valid=1"; ret = sqlite3_exec(db, query.str().c_str(), regenerate_callback, 0, &errmsg); if ( ret != SQLITE_OK ) sqlite3_die(errmsg, query.str()); if ( mode != MODE_LIST ) { // write to django.conf ofstream fout(DJANGO_CONF_FILE); fout << wsgifile.str(); fout.close(); cout << "django.conf file regenerated" << endl; // reload apache cout << "reloading the web server..." << endl; if ( !file_exists(APACHE2_RELOAD_CMD) ) die("unable to find apache2 reload command"); stringstream cmd; cmd << APACHE2_RELOAD_CMD << " > /dev/null"; system(cmd.str().c_str()); } return 0; }