Posts
Search
Contact
Cookies
About
RSS

mod_cscript test app

Added 25 Oct 2022, 8:33 p.m. edited 18 Jun 2023, 1:12 a.m.

In order to drive development of my C script module, I previously decided to create a little demo application and it has certainly improved the general state of the whole project. I found several bugs covering session handling and SQL prepared statements and I'm really happy with progress to date.

In the unlikely event someone intrepid adventurer discovers my module, I thought it worth describing how it works from the point of view of the script itself, as at the very least it gives a good indication on how the module should be used.

I've organised some common functions in the file examples/testapp/util.h, possibly one of the most important functions is checklogin() this must be called first thing by every script in the application, with the exception of the login and logout scripts

    if (!isSSL()) {
        // redirect to SSL - not just a nice to have...
        sprintf(str,"https://%s%s",getHost(),getURI());
        redirect(escapeHtmlStr(str));
        return 0;
    }

This checks to see if the script was accessed via SSL and if not, redirects to the same script but via SSL. Note the comment, SSL if fundamental to securing a login, these days a certificate can be had for free and can even automagically refresh itself via a simple cron script...

Once we are connected via SSL, the current session is checked for a "uid" variable which signifies a valid login, if its not present it redirects to the login script where all the real work happens. It should go without saying at this point you should have set up session handling with mod_session_dbd not cookies! There is a sample Apache config in the projects README.md that covers this.

If the request to the login script is a GET request, then a simple user / password form is presented to the user, one additional step (and the meat of the script) happens if its a POST request

        sprintf(str, "server=%s;uid=%s;pwd=%s;dbname=%s;",
                        getENV("DB_HOST"),
                        getENV("DB_USER"),
                        getENV("DB_PASS"),
                        getENV("DB_NAME"));

        void* drv = initDB("mysql");
        
        if (drv) {
            hdl = openDB(drv, str);
            if (hdl) {

First a connection to the database is made, its good practice to store credentials for the database somewhere other than your scripts, so here they are stored in the Apache config using the SetEnv directive, if you place them in the directory section then you could have multiple apps with their own separate database credentials...

                setNameDB(drv, hdl, "test" );
                
                char* sqls="select * from users where username = %s";
                void* stm = prepareDB(drv, hdl, sqls);
                
                char* arg[2] = { 0 };
                arg[0] = (char*)getPost("user");
                
                void* res = NULL;
                if (stm) {
                    res = pselectDB(drv,hdl,stm,1,arg);
                }
                // if the entered user name is in the DB
                // get its UID and password reference hash
                if (res){
                    int nr = getNumRowsDB(drv, res);
                    if (nr==1) {
                        void* row = getRowDB(drv, res, 1);
                        if (row) {
                            char* val = getFieldDB(drv, row, 0);
                            strcpy(uid,val);
                            val = getFieldDB(drv, row, 1);
                            strcpy(uname,val);
                            val = getFieldDB(drv, row, 2);
                            strcpy(refhash,val);
                        }
                    }
                }
                closeDB(drv, hdl);

Here a temporary prepared statement is used (but you could construct an SQL statement from user input - providing you used escapeDB to avoid SQL injection). Although the database is mentioned in the connection string its still important to call setNameDB before preparing a statement.

Arguments are passed via a null terminated array of char pointers, it should be noted that the "number of parameters" parameter is ignored (its for backwards compatibility reasons in APR util) and this parameter will soon be removed.

Its important to close and opened database connection before a request closes, not doing so can cause a seg fault with subsequent requests that I haven't got to the bottom of yet. In future I may well maintain an APR table of open database connections and automatically close them after the current script has executed...

Assuming the user name entered is in the database a result set will be returned, from this we take the user name and password hash and its only now writing this that I realise taking the username is superfluous...

                if (strlen(passin)) {
                    // check if the two hashes are equivalent
                    if (strlen(refhash)) {
                        char* tmp = dohash(passin, refhash);
                        if (strcmp(tmp, refhash)==0) {
                            // set the sessions uid so checklogin
                            // knows the login is valid
                            setSessionVal(sess, "uid", uid);
                            setSessionVal(sess, "uname", uname);
                            saveSession(sess);
                            // allow the user to access the app
                            redirectRel("index.c");
                            return;
                        } 
                    }
                }

Assuming that a pass phrase has been entered and a reference hash for the user was found we can then use the do hash function to check that there is a match. (passing NULL for the reference hash would create a hash of the entered pass phrase - useful for example when the user wants to change their password). If the pass phrase matches the stored reference hash then both the uid (database key field of the user record) and the user name is stored in the session. Finally we are ready to allow the user to progress to the applications index script (where they originally started from)

The only script in the application demonstrates a simple paginated query, I won't go into too much detail here other than to say that the forms for the page numbers should be protected with a CSRF token (in the form and checked against the session) to mitigate against Cross Site Request Forgeries and although I haven't escaped the page number, it should be noticed its converted immediately into an integer and even if you enter a number outside the page range you simply get no result set...

There's still a whole host of TODO's on my list, but I think you can see that already this simple module is becoming quite capable.

Enjoy!