/*
 * kakumei.c
 *
 * Private group web.
 *
 * Author: Dario Rodriguez dario@softhome.net
 * This progran is licensed under the terms of the Affero GPL v1+
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <mhash.h>
#include "gen_res.h"
#include "socklib.h"
#include "loglib.h"
#include "webkernel.h"
#include "kakumei.h"
#include "kakumei_session.h"
#include "kakumei_pass.h"
#include "kakumei_posts.h"
#include "kakumei_email.h"
#include "kakumei_config.h"

#define CONFIGFILE "kakumei.conf"
#define CFLOGFILE "kakumei.log"
#define CFCOOKIENAME "kakumeiauthid"
#define CFCOOKIEDOMAIN "localhost"
#define CFBANNERPATH "default.png"
#define CFSSLPROXY 0

#define DEBUG_HEADERS

static int signal_init(int signum, void (*fn)(int));
static void sigint(int signum);
volatile int sigint_flag=0;

int kakumei_inviteexists(kakumei *ka, char *invite);
int kakumei_invitedel(kakumei *ka, char *invite);
int kakumei_etag(kakumei *ka, char *filename, char *etag, int etagsize);

wk_action callback_http(wk *web, int connid, wk_uri *uri, void *userptr);
wk_action http_login(wk *web, int connid, wk_uri *uri, void *userptr);
wk_action http_newuser(wk *web, int connid, wk_uri *uri, void *userptr);
wk_action http_lastpost(wk *web, int connid, wk_uri *uri, void *userptr,char *user);
wk_action http_getpost(wk *web, int connid, wk_uri *uri, void *userptr,char *user);
wk_action http_newpost(wk *web, int connid, wk_uri *uri, void *userptr,char *user);
wk_action http_newcomment(wk *web, int connid, wk_uri *uri, void *userptr,char *user);
wk_action http_changeemail(wk *web, int connid, wk_uri *uri, void *userptr,char *user);
wk_action http_getemail(wk *web, int connid, wk_uri *uri, void *userptr,char *user);

int
main(int argc, char *argv[])
{
        int timeout=500;
        int serverfd;
        kakumei *ka,kastore;
        memset(&kastore,0,sizeof(kastore));
        ka=&kastore;
        char *hostnameport;
        char hostname[128];
        char *host;
        long hostsize;
        char *ptr,*sep;
        if(argc!=2) {
                printf("Syntax: %s [ip:]port\n",argv[0]);
                return(1);
        }
        hostnameport=argv[1];
        if(kaconfig_exists(CONFIGFILE)!=0) {
                log_setlogfile(CFLOGFILE);
                log_write("INIT","Config file not found, writing default file %s",CONFIGFILE);
                kaconfig_write(CONFIGFILE,CFLOGFILE,CFCOOKIENAME,CFCOOKIEDOMAIN,CFBANNERPATH,CFSSLPROXY);
        }
        if((ka->config=kaconfig_init(CONFIGFILE))==NULL) {
                log_setlogfile(CFLOGFILE);
                log_write("INIT","ERROR: insufficient memory or config file error");
                return(1);
        }
        log_setlogfile((ka->config->logfile!=NULL)?ka->config->logfile:CFLOGFILE);
        if((ka->ssel=sselect_init())==NULL) {
                log_write("INIT","ERROR: insufficient memory");
                return(1);
        }
        if((sep=strchr(hostnameport,':'))!=NULL) {
                serverfd=-1;
                strncpy(hostname,hostnameport,sizeof(hostname));
                hostname[sizeof(hostname)-1]='\0';
                if((ptr=strchr(hostname,':'))!=NULL)
                        *ptr='\0';
                if((host=ipv4_genip(hostname,&hostsize))!=NULL) {
                        serverfd=ipv4_serverbinded(host,hostsize,atoi(sep+1));
                        free(host),host=NULL;
                }
        } else {
                serverfd=ipv4_server(atoi(hostnameport));
        }
        if(serverfd==-1) {
                sselect_free(ka->ssel),ka->ssel=NULL;
                log_write("INIT","ERROR: couldn't listen on port");
                return(2);
        }
        sock_setunsafe(serverfd);
        if((ka->web=wk_init(serverfd,ka->ssel,NULL,callback_http,NULL,NULL,ka))==NULL) {
                sselect_free(ka->ssel),ka->ssel=NULL;
                log_write("INIT","ERROR: couldn't init web server");
                return(2);
        }
        sigint_flag=0;
        signal_init(SIGINT,sigint);
        log_write("INIT","Server initialized, waiting connections...");
        while(!sigint_flag) {
                sselect_wait(ka->ssel,timeout);
                if(sigint_flag)
                        break;
                wk_service(ka->web);
        }
        wk_free(ka->web),ka->web=NULL;
        close(serverfd),serverfd=-1;
        sselect_free(ka->ssel),ka->ssel=NULL;
        kaconfig_free(ka->config),ka->config=NULL;
        log_write("FINI","SIGINT detected, exiting...");
        return(0);
}

int
kakumei_uservalid(kakumei *ka, char *username)
{
        if(username==NULL)
                return(-1);
        if(strchr(username,'/')!=NULL)
                return(-1);
        if(strcmp(username,".")==0 || strcmp(username,"..")==0)
                return(-1);
        return(0);
}

int
kakumei_userexists(kakumei *ka, char *username)
{
        char filename[1024];
        struct stat st;
        if(kakumei_uservalid(ka,username)!=0)
                return(-1);
        snprintf(filename,sizeof(filename),"%s/%s/passwd",USERSDIR,username);
        filename[sizeof(filename)-1]='\0';
        if(stat(filename,&st)!=0 || !S_ISREG(st.st_mode))
                return(-1);
        return(0);
}

int
kakumei_inviteexists(kakumei *ka, char *invite)
{
        char filename[1024];
        struct stat st;
        if(invite==NULL)
                return(-1);
        if(strchr(invite,'/')!=NULL)
                return(-1);
        if(strcmp(invite,".")==0 || strcmp(invite,"..")==0)
                return(-1);
        snprintf(filename,sizeof(filename),"%s/%s",INVITESDIR,invite);
        filename[sizeof(filename)-1]='\0';
        if(stat(filename,&st)!=0 || !S_ISREG(st.st_mode))
                return(-1);
        return(0);
}

int
kakumei_invitedel(kakumei *ka, char *invite)
{
        char filename[1024];
        if(kakumei_inviteexists(ka,invite)!=0)
                return(-1); /* doesn't exist */
        snprintf(filename,sizeof(filename),"%s/%s",INVITESDIR,invite);
        filename[sizeof(filename)-1]='\0';
        unlink(filename);
        if(kakumei_inviteexists(ka,invite)==0)
                return(-1); /* couldn't remove invite */
        return(0);
}

int
kakumei_etag(kakumei *ka, char *filename, char *etag, int etagsize)
{
        struct stat st;
        MHASH td;
        char binhash[20];
        char hash[40];
        int i;
        char c;
        if(filename==NULL || etag==NULL || etagsize<1)
                return(-1);
        if(stat(filename,&st)!=0 || !S_ISREG(st.st_mode))
                return(-1);
        if((td=mhash_init(MHASH_SHA1))==MHASH_FAILED)
                return(-1);
        mhash(td,&(st.st_mtime),sizeof(st.st_mtime));
        mhash(td,&(st.st_size),sizeof(st.st_size));
        mhash_deinit(td,&binhash);
        for(i=0;i<sizeof(binhash);i++) {
                c=(((unsigned char *)binhash)[i]>>4);
                c=(c>=10)?(c-10+'a'):c+'0';
                hash[i<<1]=c;
                c=(((unsigned char *)binhash)[i]&0xf);
                c=(c>=10)?(c-10+'a'):c+'0';
                hash[(i<<1)+1]=c;
        }
        memcpy(etag,hash,(etagsize>sizeof(hash))?sizeof(hash):(etagsize-1));
        etag[(etagsize>sizeof(hash))?sizeof(hash):(etagsize-1)]='\0';
        return(0);
}



wk_action
callback_http(wk *web, int connid, wk_uri *uri, void *userptr)
{
        kakumei *ka=(kakumei *)userptr;
        resindex *res;
        char partialpath[1024];
        char *ptr;
        char etag[ETAGSIZE+1];
        struct {
                char *name;
        } whitelist[]={{"/index.html"},
                       {"/newuser.html"}
                      };
        int len;
        int ishtml;
        int whitelisted;
        int validsession;
        char session[SESSIONSIZE+1];
        char user[MAXUSERSIZE+1];
        char authid[AUTHIDSIZE+1];
        int i;
        if(ka==NULL)
                return(wkact_finished);
        /* log without passwords */
        if(memcmp(uri->path,"/login?",7)==0)
                log_write("HTTP","Request: /login?...");
        else if(memcmp(uri->path,"/newuser?",9)==0)
                log_write("HTTP","Request: /newuser?...");
        else
                log_write("HTTP","Request: %s",uri->path);
#ifdef DEBUG_HEADERS
        {
                int j;
                char *h;
                for(j=0;(h=wk_uri_getheaderbynum(uri,j))!=NULL;j++)
                        log_write("HDR","%s",h);
        }
#endif
        /* extract the name */
        strncpy(partialpath,uri->path,sizeof(partialpath)-1);
        partialpath[sizeof(partialpath)-1]='\0';
        if(strcmp(uri->path,"/")==0)
                strcpy(partialpath,"/index.html");
        if(strcmp(uri->path,"/banner.png")==0)
                strcpy(partialpath,(ka->config->bannerpath!=NULL)?ka->config->bannerpath:"/" CFBANNERPATH);
        if((ptr=strchr(partialpath,'?'))!=NULL)
                *ptr='\0';
        /* check whitelist */
        len=strlen(partialpath);
        ishtml=(len>5 && strcmp(partialpath+len-5,".html")==0)?1:0;
        if(ishtml) {
                for(whitelisted=0,i=0;i<(sizeof(whitelist)/sizeof(whitelist[0]));i++) {
                        if(strcmp(partialpath,whitelist[i].name)==0) {
                                whitelisted=1;
                                break;
                        }
                }
        } else
                whitelisted=1;
        if(wk_uri_copyvar(uri,"s",session,sizeof(session))==NULL)
                session[0]='\0';
        user[0]='\0';
        if(wk_uri_copycookie(uri,ka->config->cookiename,authid,sizeof(authid))==NULL)
                authid[0]='\0';
        validsession=(session_check(ka,session,authid,user,sizeof(user))!=NULL)?1:0;
        /* serve the page */
        if(partialpath[0]=='/' && (res=res_find(resindexdata,partialpath+1))!=NULL) {
                if(whitelisted || validsession) {
                        log_write("HTTP","Serving in-memory file %s",partialpath+1);
                        wk_serve_etagset(web,connid,res->etag);
                        wk_serve_buffer_as_file(web,connid,res->data,res->len,mime_getdefault(res->name,"application/octet-stream"));
                        return(wkact_finished);
                } else if((res=res_find(resindexdata,"index.html"))!=NULL) {
                        log_write("HTTP","Not allowed page, redirecting to login");
                        wk_serve_redirect(web,connid,"/");
                        return(wkact_finished);
                } else {
                        log_write("EINT","%s:%i",__FILE__,__LINE__);
                        wk_serve_error(web,connid,wkerr_internal);
                        return(wkact_finished); /* internal error */
                }
        } else if(strcmp(uri->path,"/banner.png")==0) {
                log_write("HTTP","Serving banner from disk, file %s",partialpath);
                if(kakumei_etag(ka,partialpath,etag,sizeof(etag))==0)
                        wk_serve_etagset(web,connid,etag);
                if(wk_serve_file(web,connid,partialpath,mime_getdefault(partialpath,"image/png"))!=0) {
                        log_write("HTTP","File not found, file %s",partialpath);
                        wk_serve_error(web,connid,wkerr_notfound);
                        return(wkact_finished); /* internal error */
                }
                return(wkact_finished);
        }
        /* check for actions */
        if(memcmp(uri->path,"/login?",7)==0) {
                return(http_login(web,connid,uri,userptr));
        } else if(memcmp(uri->path,"/newuser?",9)==0) {
                return(http_newuser(web,connid,uri,userptr));
        } else if(validsession && user[0]!='\0') {
                if(memcmp(uri->path,"/lastpost?",10)==0)
                        return(http_lastpost(web,connid,uri,userptr,user));
                else if(memcmp(uri->path,"/getpost?",9)==0)
                        return(http_getpost(web,connid,uri,userptr,user));
                else if(memcmp(uri->path,"/newpost?",9)==0)
                        return(http_newpost(web,connid,uri,userptr,user));
                else if(memcmp(uri->path,"/newcomment?",12)==0)
                        return(http_newcomment(web,connid,uri,userptr,user));
                else if(memcmp(uri->path,"/changeemail?",13)==0)
                        return(http_changeemail(web,connid,uri,userptr,user));
                else if(memcmp(uri->path,"/getemail?",10)==0)
                        return(http_getemail(web,connid,uri,userptr,user));
        }
        /* not found */
        log_write("HTTP","URI not found: %s",uri->path);
        wk_serve_error(web,connid,wkerr_notfound);
        return(wkact_finished);
}

static int
signal_init(int signum, void (*fn)(int))
{
        struct sigaction sa;
        sa.sa_handler=fn;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags=0;
        return(sigaction(signum,&sa,0));
}

static void
sigint(int signum)
{
        sigint_flag=1;
}

/* implement the "XmlHttpRequest"s (the reply is the new page to load) */

wk_action
http_login(wk *web, int connid, wk_uri *uri, void *userptr)
{
        char u[MAXUSERSIZE+1],p[MAXPASSWDSIZE+1];
        char reply[1024];
        char session[SESSIONSIZE+1];
        char authid[AUTHIDSIZE+1];
        kakumei *ka=(kakumei *)userptr;
        if(web==NULL || connid<0 || uri==NULL || ka==NULL) {
                log_write("EINT","%s:%i",__FILE__,__LINE__);
                wk_serve_error(web,connid,wkerr_internal);
                return(wkact_finished); /* internal error */
        }
        if(wk_uri_copyvar(uri,"u",u,sizeof(u))==NULL)
                u[0]='\0';
        if(wk_uri_copyvar(uri,"p",p,sizeof(p))==NULL)
                p[0]='\0';
        /* check for invitation */
        if(strcmp(u,INVITATIONUSER)==0 &&
           kakumei_inviteexists(ka,p)==0) {
                /* valid invitation */
                snprintf(reply,sizeof(reply),"/newuser.html?i=%s",p);
                reply[sizeof(reply)-1]='\0';
                wk_serve_buffer_as_file(web,connid,reply,strlen(reply),"text/plain");
                log_write("LGIN","Reply: %s",reply);
                return(wkact_finished);
        } else if(pass_check(ka,u,p)==0 &&
                  session_new(ka,u,session,sizeof(session),authid,sizeof(authid))!=NULL) {
                /* valid login */
                snprintf(reply,sizeof(reply),"/posts.html?s=%s",session);
                reply[sizeof(reply)-1]='\0';
                wk_serve_cookieadd(web,connid,ka->config->cookiename,authid,ka->config->cookiedomain,21600 /* 6h */,(ka->config->sslproxy)?"secure":NULL);
                wk_serve_buffer_as_file(web,connid,reply,strlen(reply),"text/plain");
                log_write("LGIN","Reply: %s",reply);
                return(wkact_finished);
        }
        log_write("LGIN","Serving error");
        wk_serve_error(web,connid,wkerr_internal);
        return(wkact_finished);
}

wk_action
http_newuser(wk *web, int connid, wk_uri *uri, void *userptr)
{
        char u[MAXUSERSIZE+1],p[MAXPASSWDSIZE+1],i[MAXPASSWDSIZE+1];
        char reply[1024];
        char session[SESSIONSIZE+1];
        char authid[AUTHIDSIZE+1];
        kakumei *ka=(kakumei *)userptr;
        if(web==NULL || connid<0 || uri==NULL || ka==NULL) {
                log_write("EINT","%s:%i",__FILE__,__LINE__);

                return(wkact_finished); /* internal error */
        }
        /* get vars */
        if(wk_uri_copyvar(uri,"i",i,sizeof(i))==NULL)
                i[0]='\0';
        if(wk_uri_copyvar(uri,"u",u,sizeof(u))==NULL)
                u[0]='\0';
        if(wk_uri_copyvar(uri,"p",p,sizeof(p))==NULL)
                p[0]='\0';
        /* check validity */
        if(kakumei_inviteexists(ka,i)!=0) {
                /* retry login */
                log_write("NEWU","invalid invite %s, redirecting to login",i);
                wk_serve_buffer_as_file(web,connid,"/",1,"text/plain");
                return(wkact_finished);
        }
        /* create user */
        if(pass_new(ka,u,p)!=0) {
                /* error with username */
                wk_serve_error(web,connid,wkerr_internal);
                log_write("NEWU","invalid user, send error");
                return(wkact_finished);
        }
        /* delete invitation */
        kakumei_invitedel(ka,i);
        /* create session and go to "posts" page */
        if(session_new(ka,u,session,sizeof(session),authid,sizeof(authid))==NULL) {
                /* "autologin" didn't work, ask for login */
                log_write("NEWU","couldn't generate new session, redirecting to login");
                wk_serve_buffer_as_file(web,connid,"/",1,"text/plain");
                return(wkact_finished);
        }
        /* valid login */
        snprintf(reply,sizeof(reply),"/posts.html?s=%s",session);
        reply[sizeof(reply)-1]='\0';
        wk_serve_cookieadd(web,connid,ka->config->cookiename,authid,ka->config->cookiedomain,21600 /* 6h */,(ka->config->sslproxy)?"secure":NULL);
        wk_serve_buffer_as_file(web,connid,reply,strlen(reply),"text/plain");
        log_write("NEWU","Reply: %s",reply);
        return(wkact_finished);
}

wk_action
http_lastpost(wk *web, int connid, wk_uri *uri, void *userptr,char *user)
{
        char reply[64];
        int n;
        kakumei *ka=(kakumei *)userptr;
        if((n=post_last(ka))<0) {
                wk_serve_error(web,connid,wkerr_internal);
                log_write("LSTP","invalid postnum, send error");
                return(wkact_finished);
        }
        /* valid */
        snprintf(reply,sizeof(reply),"%i",n);
        reply[sizeof(reply)-1]='\0';
        wk_serve_buffer_as_file(web,connid,reply,strlen(reply),"text/plain");
        log_write("LSTP","Reply: %s",reply);
        return(wkact_finished);
}

wk_action
http_getpost(wk *web, int connid, wk_uri *uri, void *userptr,char *user)
{
        kakumei *ka=(kakumei *)userptr;
        char filename[1024];
        char n[64];
        int num;
        if(wk_uri_copyvar(uri,"n",n,sizeof(n))==NULL)
                n[0]='\0';
        num=atoi(n);
        if(n[0]=='\0' || num<0 || post_get(ka,num,filename,sizeof(filename))!=0) {
                wk_serve_error(web,connid,wkerr_internal);
                log_write("GETP","invalid postnum, send error");
                return(wkact_finished);
        }
        /* valid login */
        wk_serve_file(web,connid,filename,"application/json");
        log_write("GETP","Reply: contents of %s",filename);
        return(wkact_finished);
}

wk_action
http_newpost(wk *web, int connid, wk_uri *uri, void *userptr,char *user)
{
        char h[8192];
        char t[8192];
        kakumei *ka=(kakumei *)userptr;
        if(wk_uri_copyvar(uri,"h",h,sizeof(h))==NULL)
                h[0]='\0';
        if(wk_uri_copyvar(uri,"t",t,sizeof(t))==NULL)
                t[0]='\0';
        if((t[0]=='\0' && h[0]=='\0') || post_new(ka,user,h,t)!=0) {
                wk_serve_error(web,connid,wkerr_internal);
                log_write("NEWP","invalid post, send error");
                return(wkact_finished);
        }
        email_notify(ka,user,-1);
        log_write("NEWP","Replying with last post");
        return(http_lastpost(web,connid,uri,userptr,user));
}

wk_action
http_newcomment(wk *web, int connid, wk_uri *uri, void *userptr,char *user)
{
        char t[8192];
        kakumei *ka=(kakumei *)userptr;
        char n[64];
        int num;
        if(wk_uri_copyvar(uri,"n",n,sizeof(n))==NULL)
                n[0]='\0';
        num=atoi(n);
        if(wk_uri_copyvar(uri,"t",t,sizeof(t))==NULL)
                t[0]='\0';
        if(n[0]=='\0' || n<0 || t[0]=='\0' || post_addcomment(ka,num,user,t)!=0) {
                wk_serve_error(web,connid,wkerr_internal);
                log_write("NEWC","invalid post, send error");
                return(wkact_finished);
        }
        wk_serve_buffer_as_file(web,connid,n,strlen(n),"text/plain");
        log_write("NEWC","Reply: %s",n);
        email_notify(ka,user,num);
        return(wkact_finished);
}

wk_action
http_changeemail(wk *web, int connid, wk_uri *uri, void *userptr,char *user)
{
        char useremail[128];
        kakumei *ka=(kakumei *)userptr;
        if(wk_uri_copyvar(uri,"a",useremail,sizeof(useremail))==NULL)
                useremail[0]='\0';
        if(email_set(ka,user,useremail)!=0) {
                wk_serve_error(web,connid,wkerr_internal);
                log_write("EMCH","couldn't change email, send error");
                return(wkact_finished);
        }
        /* valid */
        wk_serve_buffer_as_file(web,connid,useremail,strlen(useremail),"text/plain");
        log_write("EMCH","Reply: %s",useremail);
        return(wkact_finished);
}

wk_action
http_getemail(wk *web, int connid, wk_uri *uri, void *userptr,char *user)
{
        char useremail[128];
        kakumei *ka=(kakumei *)userptr;
        if(email_get(ka,user,useremail,sizeof(useremail))!=0) {
                wk_serve_error(web,connid,wkerr_internal);
                log_write("EMGT","no user email, send error");
                return(wkact_finished);
        }
        /* valid */
        wk_serve_buffer_as_file(web,connid,useremail,strlen(useremail),"text/plain");
        log_write("EMGT","Reply: %s",useremail);
        return(wkact_finished);
}