/*
 * mod_auth_imap: authentication via IMAP server
 * Version: 2.0 - BETA release.
 *
 * brillat-apachemodule@mainsheet.org (Ben Brillat), 
 *   based on mod_auth_pop by Milos Prodanovic <awl@verat.net>
 *
 * Jian Zhen <jlz@zhen.org>
 *   Modified version 1.1 by Ben to work with Apache 2
 *
 * Follows the IMAP v4rev1 RFC - RFC 2060
 * http://www.ietf.org/rfc/rfc2060.txt
 */

#include "apr_lib.h"

#define APR_WANT_STRFUNC
#include "apr_want.h"
#include "apr_strings.h"
#include "apr_dbm.h"
#include "apr_md5.h"

#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_protocol.h"
#include "http_request.h"   /* for ap_hook_(check_user_id | auth_checker)*/


#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <netdb.h>
#include <syslog.h>
 
#define _OK 1

int Sock;


/*******************************************************************************
 * tcp_gets
 * 	reads a line from the network (up to \n or EOF)
 *     Always returns 0
 *******************************************************************************/
int tcp_gets(int s, char *res,int len) {
    char c;
    int cur=0,rc;
 
    memset(res,0,len+1);
  
    while( (rc = read(s, &c, 1))!=EOF) {
        if(cur<len)
        res[cur]=c;
        cur++;
        if(c=='\n') break;     
    }

    return 0;
}

/*******************************************************************************
 * clean_up
 * 	closes the socket
 *******************************************************************************/
void clean_up(int s) { 
    close(s);
}

/*******************************************************************************
 * tcp_puts
 * 	write a line to the network
 *     performs a bzero (via memset) on the memory to be used...
 *******************************************************************************/
int tcp_puts(int s, char *sttr) {
    int rr,len;
    char line[512];

    memset(line,0,512);
    len=strlen(sttr);
    if(len>510) len=510;
    strncpy(line,sttr,len);
    rr=write(s,line,strlen(line));
    return rr;
}


/*******************************************************************************
 * imap_tcp_open
 * 	connects to the remote server
 * 	returns a socket
 *******************************************************************************/
int imap_tcp_open(request_rec *r, char *hostname, int port) {
    struct hostent *HOST;
    struct sockaddr_in sai;
    int s;

    if ((HOST = gethostbyname(hostname)) == (struct hostent *)NULL) {
        ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: Hostname unknown.");
        return !_OK;
    }

    memset((char *)&sai, '\0', sizeof(sai));
    memcpy((char *)&sai.sin_addr, (char *)HOST -> h_addr, HOST -> h_length);
    sai.sin_family = HOST -> h_addrtype;
    sai.sin_port = htons(port);
 
    if ((s = socket(HOST -> h_addrtype, SOCK_STREAM, 0)) == -1) {
        ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: socket problem");
        clean_up(s);
        return !_OK;
    }
 
    if(connect(s, (struct sockaddr *)&sai, sizeof(sai)) == -1) {
        ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: connect() problem");
        clean_up(s);
 	return !_OK;
    }

    return s;
}
 

/******************************************************************
 *imap_do_rfc2060
 * Check the U/P against the selected IMAP server
 *   according to IMAP v4rev1 RFC
 * Return: 1 or 0
 ******************************************************************/
int imap_do_rfc2060(request_rec *r, char *host, char *username, char *pass, 
                    char *cport, int logflag) {
    char result[512],buf[512];
    int ret=0;
    int port;

    port=atoi(cport);

    // Verify that the username and password are of a reasonable length (<100)
    if( strlen(username)>100 || strlen(pass)>100 ) {
         ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"username/password too long for mod_auth_imap");
         printf("Ouch - u/p too long!\n");
         return !_OK;
    }

    Sock = imap_tcp_open(r, host, port);

    if (!Sock) return !_OK;

    //Just eat the initial response from the server, up to 500 chars.
    tcp_gets(Sock,result,500);

    //Send a request for CAPABILITY
    memset(buf,0,500);
    sprintf(buf,"A001 CAPABILITY\r\n");
    tcp_puts(Sock,buf);

    //get the capability line...
    tcp_gets(Sock,result,500);

    //get the "A001 OK CAPABILITY completed" line..
    tcp_gets(Sock,result,500);

    //skip lines that start with "*"
    if (strncmp(result,"* ",2 == 0)) {
	tcp_gets(Sock,result,500);
    }

    if (strncmp(result,"A001 OK CAPABILITY",18) != 0 &&
        // Cyrus returns "A001 OK Completed"
        strncmp(result,"A001 OK Completed",17) != 0) {
        ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: Server does not support imap CAPABILITY.");
        ret=!_OK;
        clean_up(Sock);
        return ret;  //BAIL!
    }

    //Log In w/given Username & Password
    memset(buf,0,500);
    sprintf(buf,"A002 LOGIN %s \"%s\"\r\n", username, pass);
    tcp_puts(Sock,buf);
    tcp_gets(Sock,result,500);

    if (strncmp(result,"A002 OK",7) == 0) {
        if (logflag) {
            ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: Verified login for user %s.", username);
        }

        ret=_OK;
    } else if (strncmp(result,"A002 NO",7) == 0) {
        if (logflag) {
            ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: Login failed for user %s.", username);
            ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: Server said: %s", result);
        }

        ret=!_OK;
    } else {
        //it must have told us BYE and disconnected
        if (logflag) {
            ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: Premature server disconnect for user %s.", username);
            ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: Server said: %s", result);
        }

        ret=!_OK;
        clean_up(Sock);
        return ret;  //BAIL!
    }

    //Log out...
    memset(buf,0,500);
    sprintf(buf,"A003 LOGOUT\r\n");
    tcp_puts(Sock,buf);

    //read the BYE line
    tcp_gets(Sock,result,500);

    //read the OK LOGOUT
    tcp_gets(Sock,result,500);

    if (strncmp(result,"A003 OK",7) == 0) {
        if (logflag) {
            ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: OK logout for %s.", username);
        }
		//don't change the return here - we still need to know if
		//the user/pass was good from the LOGIN command!
    } else {
        if (logflag) {
            ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: Error in logout for %s.", username);
            ap_log_rerror(APLOG_MARK,APLOG_WARNING|APLOG_NOERRNO,0,r,"mod_auth_imap: Server said: %s", result);
        }

        ret=!_OK;
    }

    //close the connection
    clean_up(Sock);

    return ret;
}

 
/******************************************************************
* auth_config_struct 
* The struct with all data passed from .htaccess or httpd.conf
 ******************************************************************/
typedef struct auth_config_struct {
    char *imap_server;
    char *imap_port;
    int   imap_log;
    int   imap_authoritative;
    int   imap_enabled;
} imap_config_rec;


/******************************************************************
 * Defaults for the .htaccess options
 ******************************************************************/
static void *create_imap_dir_config (apr_pool_t *p, char *d)
{
    imap_config_rec *sec =
	(imap_config_rec *) apr_palloc (p, sizeof(imap_config_rec));
    sec->imap_server = "127.0.0.1";     /* localhost is server by default */
    sec->imap_port = "143"; 	/* port is 143 by default */
    sec->imap_log = 0; /* Logging is off by default */
    sec->imap_authoritative = 1; /* keep the fortress secure by default */
    sec->imap_enabled = 0; /* disable this authentication by defualt */
    return sec;
}


/*******************************************************************************
 * .htaccess and httpd.conf configuration options
 *******************************************************************************/
static command_rec imap_cmds[] = {
    AP_INIT_TAKE1("Auth_IMAP_Server", ap_set_string_slot,
     (void*) APR_XtOffsetOf(imap_config_rec, imap_server), OR_AUTHCFG, 
     "IMAP server . " ),

    AP_INIT_TAKE1("Auth_IMAP_Port", ap_set_string_slot,
     (void*) APR_XtOffsetOf(imap_config_rec, imap_port), OR_AUTHCFG, 
     "IMAP port . " ),

    AP_INIT_FLAG("Auth_IMAP_Log", ap_set_flag_slot,
     (void*) APR_XtOffsetOf(imap_config_rec, imap_log), OR_AUTHCFG, 
     "Set to 'yes' to enable some logging in the Apache ErrorLog" ),

    AP_INIT_FLAG("Auth_IMAP_Authoritative", ap_set_flag_slot,
     (void*) APR_XtOffsetOf(imap_config_rec, imap_authoritative), OR_AUTHCFG, 
     "Set to 'no' to allow access control to be passed along to lower modules if the UserID is not known to this module" ),

    AP_INIT_FLAG("Auth_IMAP_Enabled", ap_set_flag_slot, 
     (void*) APR_XtOffsetOf(imap_config_rec, imap_enabled), OR_AUTHCFG, 
     "on|off - determines if IMAP authentication is enabled; default is off" ),

    { NULL }
};






module AP_MODULE_DECLARE_DATA auth_imap_module;


/*******************************************************************************
 * imap_authenticate_basic_user  -    the main function
 * 	connects remotely, verifies if the user/pass combo is correct
 * 	returns "OK", "DECLINED", or "HTTP_UNAUTHORIZED" (see defines)
 *******************************************************************************/
static int imap_authenticate_basic_user (request_rec *r)
{
    imap_config_rec *sec =
      (imap_config_rec *)ap_get_module_config (r->per_dir_config, &auth_imap_module);
    int res,i;
    char *server = sec->imap_server;
    char *port = sec->imap_port;
    const char *sent_pw;

    /* is this module disabled? */
    if (!sec->imap_enabled) return DECLINED;
 
    if ((res = ap_get_basic_auth_pw (r, &sent_pw))) return res;

    if(!sec->imap_server) return DECLINED;

    /* send IMAP authen, server port, username, sent_pw */
    i=imap_do_rfc2060(r,server,r->user,sent_pw,port,sec->imap_log);    

    if (i==1) return (OK);

    ap_note_basic_auth_failure(r);
    return HTTP_UNAUTHORIZED;
}
    

/*******************************************************************************
 * imap_check_user_access
 * 	checks the username against the allowed users in the .htaccess or httpd.conf
 * 	returns "OK", "DECLINED", or "HTTP_UNAUTHORIZED" (see defines)
 *******************************************************************************/    
static int imap_check_user_access (request_rec *r) {
    imap_config_rec *sec =
     (imap_config_rec *)ap_get_module_config (r->per_dir_config, &auth_imap_module);
    char *user = r->user;
    int m = r->method_number;
    int method_restricted = 0;
    register int x;
    const char *t, *w;
    const apr_array_header_t *reqs_arr = ap_requires (r);
    require_line *reqs;

    if (!reqs_arr)
        return (OK);
    reqs = (require_line *)reqs_arr->elts;
  
    for(x=0; x < reqs_arr->nelts; x++) {
      
	if (! (reqs[x].method_mask & (1 << m))) continue;
	
	method_restricted = 1;

        t = reqs[x].requirement;
        w = ap_getword(r->pool, &t, ' ');
        if(!strcmp(w,"valid-user"))
            return OK;
        if(!strcmp(w,"user")) {
            while(t[0]) {
                w = ap_getword_conf (r->pool, &t);
                if(!strcmp(user,w))
                    return OK;
            }
        }
	else if(!strcmp(w,"group"))
	        return DECLINED;
    }
    
    if (!method_restricted)
      return OK;

    if (!(sec->imap_authoritative))
      return DECLINED;

    ap_note_basic_auth_failure (r);
    return HTTP_UNAUTHORIZED;
}

static void imap_register_hooks(apr_pool_t *p) {
    ap_hook_check_user_id(imap_authenticate_basic_user, NULL, NULL, 
                          APR_HOOK_MIDDLE);
    ap_hook_auth_checker(imap_check_user_access, NULL, NULL, APR_HOOK_MIDDLE);
}

/*******************************************************************************
 * module function definitions
 * 	defines the functions called by apache for this module...
 *******************************************************************************/
module AP_MODULE_DECLARE_DATA auth_imap_module = {
   STANDARD20_MODULE_STUFF,
   create_imap_dir_config,       /* dir config creater */
   NULL,			/* dir merger --- default is to override */
   NULL,			/* server config */
   NULL,			/* merge server config */
   imap_cmds,                    /* command table */
   imap_register_hooks		/* register hooks */
};

