/* 
 * FastCGI FTP Authentication Server -- Scott Dybiec
 * The Program
 */ 
#include "fcgi_stdio.h" 
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <stdarg.h>

/* This program relies on two pieces of external code: a Tcl Hash
 * table for in-memory storage of sessions and an FTP library for
 * access to ftp servers. The Tcl hash comes in the example code
 * for the FastCGI developers kit, while the FTP library, LIBFTP
 * Version 5.0 by Oleg Orel was taken from in comp.sources.unix
 */

#include "FtpLibrary.h"
#include "tcl.h"

/* Defaults overridden if initializing environment variables sent */
#define INACTIVITY_TIMEOUT 60  /* allowable time of inactivity in seconds */
#define SESSION_LIFETIME 180   /* maximum session lifetime in seconds */
#define REAPER_FREQ 10         /* call the reaper every 10 requests */
#define DEFAULT_FTP_HOST "localhost"

#define USERID_LEN 32
#define PASSWD_LEN 32

typedef struct SessionObj {
    char userid[USERID_LEN];
    char password[PASSWD_LEN];
    time_t last_access;
    time_t last_login;
} SessionObj;

/* Cached session access return codes */
typedef enum { GOOD_SESSION, EXPIRED, NOT_FOUND, BAD_PASSWORD } SessionState;

/* FTP login attempt status */
typedef enum { GOOD_LOGIN, BAD_LOGIN, NO_HOST_FOUND } LoginStatus; 

/* Status returned to HTTPd caller from this authenticator */
typedef enum { OK, AUTH_REQ, SERVER_ERROR } AuthStatus;

void          Initialize(void);
void          Authenticate(void);
void          Respond(void);
LoginStatus   TryFtpLogin(char *, char *);
Tcl_HashEntry *NewSession(char *, char *);
void          TouchSessionLastAccess(char *);
void          TouchSessionLastLogin(char *);
Tcl_HashEntry *GetSession(char *);
void          DeleteSession(char *);
SessionState  SessionStatus(char *, char *);
void          SendAuthStatus(AuthStatus, char *);
void          ReapExpiredSessions(void);
void          LogMsg(const char *, ...);
void          DisplaySessions(void);

void          getword(char *, char *, char stop);
void          unescape_url(char *);
void          plustospace(char *);

/* Global hash table and FTP host */
static Tcl_HashTable *sessionTablePtr;
static Tcl_HashTable sessionTable;
static char *ftpHost;

int requestCount;
int debugFlag;
int inactivityTimeout;
int sessionLifetime;
int reaperFrequency;


void main(void)
{
    Initialize();

    while(FCGI_Accept() >= 0) { 

        char *role;

        if (role = getenv("FCGI_ROLE")) {
            if (!strcmp(role, "AUTHORIZER"))
                Authenticate();
            else if (!strcmp(role, "RESPONDER"))
                Respond();
        }

    } /* while */
}

void Initialize(void)
{

    sessionTablePtr = &sessionTable;
    Tcl_InitHashTable(sessionTablePtr, TCL_STRING_KEYS);

    /* Take the initializing environment variables over the defaults */
    if (!(ftpHost = getenv("FTP_HOST"))) ftpHost = DEFAULT_FTP_HOST;

    if (!getenv("INACTIVITY_TIMEOUT"))
        inactivityTimeout = INACTIVITY_TIMEOUT;
    else
        inactivityTimeout = atoi(getenv("INACTIVITY_TIMEOUT"));

    if (!getenv("SESSION_LIFETIME"))
        sessionLifetime = SESSION_LIFETIME;
    else
        sessionLifetime = atoi(getenv("SESSION_LIFETIME"));

    if (!getenv("REAPER_FREQ"))
        reaperFrequency = REAPER_FREQ;
    else
        reaperFrequency = atoi(getenv("REAPER_FREQ"));

    debugFlag = 1;
    requestCount = 1;

}

void Authenticate(void)
{

    char *remoteUser, *remotePassword;
    SessionObj *session;
    SessionState sessionStatus;
    LoginStatus loginStatus;
    AuthStatus authStatus;

    authStatus = OK;
    remoteUser = getenv("REMOTE_USER");
    remotePassword = getenv("REMOTE_PASSWD");

    LogMsg("Authenticating user '%s' with password '%s'",
                                    remoteUser, remotePassword);

    sessionStatus = SessionStatus(remoteUser, remotePassword);

    switch (sessionStatus) {

      case GOOD_SESSION:
          LogMsg("Found good session, go ahead!");
          TouchSessionLastAccess(remoteUser);
          break;

      case BAD_PASSWORD:
      case EXPIRED:
          LogMsg("Found expired session");
          loginStatus = TryFtpLogin(remoteUser, remotePassword);
          switch (loginStatus) {
            case GOOD_LOGIN:
                TouchSessionLastAccess(remoteUser);
                TouchSessionLastLogin(remoteUser);
                authStatus = OK;
                break;
            case BAD_LOGIN:
                DeleteSession(remoteUser);
                authStatus = AUTH_REQ;
                break;
            case NO_HOST_FOUND:
                DeleteSession(remoteUser);
                authStatus = SERVER_ERROR;
                break;
          }
      break;

      case NOT_FOUND:
          LogMsg("No session found");
          loginStatus = TryFtpLogin(remoteUser, remotePassword);
          switch (loginStatus) {
            case GOOD_LOGIN:
                NewSession(remoteUser, remotePassword);
                authStatus = OK;
                break;
            case BAD_LOGIN:
                authStatus = AUTH_REQ;
                break;
            case NO_HOST_FOUND:
                authStatus = SERVER_ERROR;
                break;
          }
          break;
    }

    SendAuthStatus(authStatus, remoteUser);
    if (!(requestCount % reaperFrequency)) ReapExpiredSessions();
    requestCount++;
}

void Respond(void)
{
    char name[128];
    char value[128];
    char *qs;

    printf("Content-type: text/html%c%c",10,10);

    if(strcmp(getenv("REQUEST_METHOD"),"GET")) {
        printf("This server should be referenced with a METHOD of GET.\n");
        return;
    }

    qs = getenv("QUERY_STRING");
    if (qs != NULL) {
        getword(value, qs, '&');
        plustospace(value);
        unescape_url(value);
        getword(name, value, '=');
    }

    if (!strcasecmp(name, "INACTIVITY_TIMEOUT"))
        inactivityTimeout = atoi(value);
    else if (!strcasecmp(name, "SESSION_LIFETIME"))
        sessionLifetime = atoi(value);
    else if (!strcasecmp(name, "REAPER_FREQ"))
        reaperFrequency = atoi(value);
    else if (!strcasecmp(name, "DEBUG"))
        debugFlag ^= 1; 

    printf("<title>FastCGI FTP Authenticator Management</title>");
    printf("<H1>FastCGI FTP Authenticator Management</H1>");

    printf("<H3>Server Parameter Settings</H3>");
    printf("<ul>%c",10);
    printf("<li>Requests serviced = <b>%d hits</b><br>", requestCount);
    printf("<li>FTP authenticator hostname =  <b>%s</b><br>", ftpHost);
    printf("<li>Inactivity timeout period = <b>%d seconds</b><br>", 
                     inactivityTimeout);
    printf("<li>Maximum session lifetime = <b>%d seconds</b><br>", 
                     sessionLifetime);
    printf("<li>Reap expired sessions = <b>once every %d requests</b><br>", 
                     reaperFrequency);
    printf("<li>Debug mode = <b>%s</b>. ", ( debugFlag ? "ON" : "OFF" ));
    printf("<a href=\"authftp?debug\">[Toggle]</a><br>");
    printf("</ul>%c",10);

    printf("<H3>Cached Sessions</H3>");
    DisplaySessions();
}

LoginStatus TryFtpLogin(char *user, char *password)
{
    FTP ftp;
    FTP *ftp_session;
    FTP **ftp_session_ptr;
    STATUS status;

    ftp_session = &ftp;
    ftp_session_ptr = &ftp_session;

    LogMsg("Trying FTP login using host '%s' ...", ftpHost);

    status = FtpLogin(ftp_session_ptr, ftpHost,
                        user, password, NULL);

    switch (status) {

      case -5:
          LogMsg("FTP host could not be found");
          return NO_HOST_FOUND;
          break;

      case 230:
          LogMsg("We have a login!");
          FtpBye(ftp_session);
          return GOOD_LOGIN;
          break;

      default:
          LogMsg("Bad userid or password");
          FtpBye(ftp_session);
          return BAD_LOGIN;
    }
}

Tcl_HashEntry *NewSession(char *user, char *password) 
{
    Tcl_HashEntry *sessionEntry;
    int new;

    time_t t = time(NULL);
    SessionObj *session = malloc(sizeof(SessionObj));
    strcpy(session->userid, user);
    strcpy(session->password, password);
    session->last_access = t;
    session->last_login = t;
    sessionEntry = Tcl_CreateHashEntry(sessionTablePtr, user, &new);
    assert(new);
    Tcl_SetHashValue(sessionEntry, session);
    return sessionEntry;
}
         
void TouchSessionLastAccess(char *user) 
{
    Tcl_HashEntry *sessionEntry = Tcl_FindHashEntry(sessionTablePtr, user);
    time_t now = time(NULL);
    SessionObj *session;

    session = Tcl_GetHashValue(sessionEntry);
    session->last_access = now;
}
         
void TouchSessionLastLogin(char *user) 
{
    Tcl_HashEntry *sessionEntry = Tcl_FindHashEntry(sessionTablePtr, user);
    time_t now = time(NULL);
    SessionObj *session;

    session = Tcl_GetHashValue(sessionEntry);
    session->last_login = now;
}
         
Tcl_HashEntry *GetSession(char *user) 
{
    Tcl_HashEntry *sessionEntry = Tcl_FindHashEntry(sessionTablePtr, user);

    return sessionEntry;
}
         
void DeleteSession(char *user) 
{
    Tcl_HashEntry *sessionEntry = Tcl_FindHashEntry(sessionTablePtr, user);
    time_t now = time(NULL);
    SessionObj *session;

    session = Tcl_GetHashValue(sessionEntry);
    free(session);
    Tcl_DeleteHashEntry(sessionEntry);
}

SessionState SessionStatus(char *user, char *password) 
{
    Tcl_HashEntry *sessionEntry = Tcl_FindHashEntry(sessionTablePtr, user);
    time_t now = time(NULL);
    double sinceLastAccess;
    double sinceLastLogin;

    if (sessionEntry != NULL) {
        SessionObj *session;

        session = Tcl_GetHashValue(sessionEntry);
        sinceLastAccess = difftime(now, session->last_access);
        sinceLastLogin = difftime(now, session->last_login);

        if (sinceLastAccess > INACTIVITY_TIMEOUT || 
            sinceLastLogin > SESSION_LIFETIME) {
            return EXPIRED;
        } else if (strcmp(password, session->password)) {
            return BAD_PASSWORD;
        } else {
            return GOOD_SESSION;
        }
    } else {
        return NOT_FOUND;
    }
}

void SendAuthStatus(AuthStatus authStatus, char *user)
{
    char errstr[255];

        switch (authStatus) {

          case OK:
              /* Authentication worked */
              LogMsg("FCGI FTP Auth: authStatus is OK");
              printf("Status: 200 OK\r\n"
                     "Variable-AUTH_TYPE: FTP Basic\r\n"
                     "Variable-REMOTE_PASSWORD:\r\n"
                     "\r\n");
              break;

          case SERVER_ERROR:
              /* FTP host could not be found */
              LogMsg("FCGI FTP Auth: Bad FTP hostname");
              printf("Status: 500 Server Error\r\n"
                     "\r\n");
              break;

          case AUTH_REQ:
              /* Bad userid or password */
              LogMsg("FCGI FTP Auth: "
                        "Bad userid or password for user '%s'", user);
              printf("Status: 401 Unauthorized\r\n"
                     "WWW-Authenticate: Basic realm=\"Test\"\r\n" 
                     "\r\n");
        }
}

void ReapExpiredSessions(void) 
{
    Tcl_HashSearch search;
    Tcl_HashEntry *sessionEntry;
    time_t now = time(NULL);
    double sinceLastAccess;
    double sinceLastLogin;
    char *user;

    LogMsg("Reaping expired sessions ...");
    sessionEntry = Tcl_FirstHashEntry(sessionTablePtr, &search);
    for (sessionEntry = Tcl_FirstHashEntry(sessionTablePtr, &search);
          sessionEntry != NULL; sessionEntry = Tcl_NextHashEntry(&search)) {
        SessionObj *session;
        user = Tcl_GetHashKey(sessionTablePtr, sessionEntry);
        session = Tcl_GetHashValue(sessionEntry);
        sinceLastAccess = difftime(now, session->last_access);
        sinceLastLogin = difftime(now, session->last_login);

        if (sinceLastAccess > INACTIVITY_TIMEOUT ||
            sinceLastLogin > SESSION_LIFETIME) {
            free(session);
            Tcl_DeleteHashEntry(sessionEntry);
            LogMsg("Session reaped for user '%s'", user);
        }
    }
}

void DisplaySessions(void) 
{
    Tcl_HashSearch search;
    Tcl_HashEntry *sessionEntry;
    time_t now = time(NULL);
    double sinceLastAccess;
    double sinceLastLogin;
    char *user;

    printf("<table border>");
    printf("<tr>");
    printf("<th> User ID </th><th> Password </th>"
           "<th> Since Last Access </th><th> Since Last Login </th>");
    printf("</tr>");

    sessionEntry = Tcl_FirstHashEntry(sessionTablePtr, &search);
    for (sessionEntry = Tcl_FirstHashEntry(sessionTablePtr, &search);
          sessionEntry != NULL; sessionEntry = Tcl_NextHashEntry(&search)) {
        SessionObj *session;
        user = Tcl_GetHashKey(sessionTablePtr, sessionEntry);
        session = Tcl_GetHashValue(sessionEntry);
        sinceLastAccess = difftime(now, session->last_access);
        sinceLastLogin = difftime(now, session->last_login);
        printf("<tr>");
        printf("<td>%s</td><td>%s</td><td>%6.2f</td><td>%6.2f</td>", 
                  user, session->password, sinceLastAccess, sinceLastLogin);
        printf("</tr>");

    }
    printf("</table>");
}

/* Four routines taken from Apache support directory */
char x2c(char *what) {
    register char digit;

    digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
    digit *= 16;
    digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));
    return(digit);
}

void getword(char *word, char *line, char stop) {
    int x = 0,y;

    for(x=0;((line[x]) && (line[x] != stop));x++)
        word[x] = line[x];

    word[x] = '\0';
    if(line[x]) ++x;
    y=0;

    while(line[y++] = line[x++]);
}

void unescape_url(char *url) {
    register int x,y;

    for(x=0,y=0;url[y];++x,++y) {
        if((url[x] = url[y]) == '%') {
            url[x] = x2c(&url[y+1]);
            y+=2;
        }
    }
    url[x] = '\0';
}

void plustospace(char *str) {
    register int x;

    for(x=0;str[x];x++) if(str[x] == '+') str[x] = ' ';
}

/*
 * Should the Tcl hash package detect an unrecoverable error(!), halt.
 */
void panic(char *format,
        char *arg1, char *arg2, char *arg3, char *arg4,
        char *arg5, char *arg6, char *arg7, char *arg8)
{
    assert(0);
}

void LogMsg(const char *fmt, ...)
{
    va_list ap;
    char *buf[4096];
    
    if (debugFlag) {
        va_start(ap, fmt);
        fprintf(stderr, "\n\t-->");
        vfprintf(stderr, fmt, ap);
        fflush(stderr);
        va_end(ap);
    }
}
