/**
* 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;
}