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