/* * mod_spin.c * * Copyright (C) 2003 - 2008 Bojan Smojver, Rexursive * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * */ #include "private.h" #include #include #include #include #include module AP_MODULE_DECLARE_DATA spin_module; #define SPIN_MAGIC_TYPE "httpd/spin-template" #define ERROR_BUF_SIZE 256 /* Private process data */ static rxv_spin_private_t *priv; /* Regular expression for checking session syntax */ static ap_regex_t *ureg; /* Cached template timeout */ #define TMPL_TMOUT (10*1000000) /* Connection pool limits */ #define CONN_MAX 5 #define CONN_TMOUT (60*1000000) static int conn_max=CONN_MAX; /* * Little helper functions... */ /* context creation utility */ static rxv_spin_ctx_t *context_create(request_rec *r){ rxv_spin_ctx_t *ctx=apr_pcalloc(r->pool,sizeof(*ctx)); ctx->r=r; ctx->data=apr_pcalloc(r->pool,sizeof(*ctx->data)); ctx->data->size=RXV_SPIN_ROWS; ctx->data->cols=apr_hash_make(r->pool); ctx->closed=RXV_SPIN_TRUE; return ctx; } /* scanner extra data creation/destruction */ static apr_status_t extra_destroy(void *data){ rxv_spin_extra_t *extra=data; /* cleanup all leftover brigades */ apr_brigade_cleanup(extra->output); if(extra->input) apr_brigade_cleanup(extra->input); if(extra->split) apr_brigade_cleanup(extra->split); if(extra->metabb) apr_brigade_cleanup(extra->metabb); return APR_SUCCESS; } static rxv_spin_extra_t *extra_create(request_rec *r,rxv_spin_ctx_t *ctx){ apr_pool_t *pool; rxv_spin_extra_t *extra=apr_pcalloc(r->pool,sizeof(*extra)); apr_pool_create(&pool,r->pool); extra->pool=pool; extra->loop=apr_pcalloc(r->pool,sizeof(*extra->loop)*RXV_SPIN_MAX_DEPTH); extra->nest=apr_pcalloc(r->pool,sizeof(*extra->nest)*RXV_SPIN_MAX_DEPTH); extra->row=1; extra->col=0; extra->erow=extra->ecol=-1; extra->buf=apr_pcalloc(r->pool,RXV_SPIN_SCAN_MAX+2); extra->ctx=ctx?ctx:context_create(r); extra->output=apr_brigade_create(r->pool,r->connection->bucket_alloc); apr_pool_cleanup_register(r->pool,extra,extra_destroy,apr_pool_cleanup_null); return extra; } /* template cleanups */ static apr_status_t tmpl_cleanup(void *data){ rxv_spin_tmpl_t *tmpl=data; apr_hash_set(tmpl->priv->tmpls,tmpl->key,APR_HASH_KEY_STRING,NULL); APR_RING_REMOVE(tmpl,link); return APR_SUCCESS; } static apr_status_t cache_cleanup(void *data){ rxv_spin_tmpl_t *tmpl=data; rxv_spin_lock(tmpl->priv); tmpl->count--; rxv_spin_unlock(tmpl->priv); return APR_SUCCESS; } /* private process data cleanup */ static apr_status_t private_cleanup(void *data){ #if APR_HAS_THREADS apr_status_t rv; #endif request_rec *r; rxv_spin_tmpl_t *u,*q; rxv_spin_pool_t *t,*p; rxv_spin_trylock(priv,rv); #if APR_HAS_THREADS if(rv!=APR_SUCCESS) return APR_SUCCESS; #endif r=data; /* clean timed out cached templates */ for(q=APR_RING_FIRST(&priv->tmpl); q!=APR_RING_SENTINEL(&priv->tmpl,rxv_spin_tmpl,link); q=u){ /* in use, don't touch */ if(q->count){ u=APR_RING_NEXT(q,link); continue; } if(q->atime+TMPL_TMOUT>=r->request_time) break; u=APR_RING_NEXT(q,link); /* save next - we'll destroy the pool */ apr_pool_destroy(q->pool); /* this will unlink and remove from hash */ } #if APR_HAS_THREADS && !defined(HAVE_APR_RESLIST_ACQUIRED_COUNT) if(priv->mutex){ rxv_spin_unlock(priv); return APR_SUCCESS; } #endif /* clean over limit and timed out connections */ for(p=APR_RING_FIRST(&priv->conn); p!=APR_RING_SENTINEL(&priv->conn,rxv_spin_pool,link); p=t){ /* waiting to acquire resource, don't touch */ if(p->atime==APR_INT64_MIN){ t=APR_RING_NEXT(p,link); continue; } if(p->atime+CONN_TMOUT>=r->request_time #if APR_HAS_THREADS && defined(HAVE_APR_RESLIST_ACQUIRED_COUNT) || (priv->mutex && apr_reslist_acquired_count(p->list)) #endif ) break; t=APR_RING_NEXT(p,link); /* save next - we'll destroy the pool */ apr_pool_destroy(p->pool); /* this will unlink and remove from hash */ } rxv_spin_unlock(priv); return APR_SUCCESS; } /* * Configuration... */ static void *create_config(apr_pool_t *p){ rxv_spin_config_t *conf=apr_pcalloc(p,sizeof(*conf)); conf->appinit="rxv_spin_init"; conf->appfixup="rxv_spin_prepare"; conf->appentry="rxv_spin_service"; conf->storetbl="spinstore"; conf->ckpath="/"; conf->cache=RXV_SPIN_TRUE; conf->connpool=RXV_SPIN_TRUE; return conf; } static void *create_config_server(apr_pool_t *p,server_rec *s){ return create_config(p); } static void *create_config_directory(apr_pool_t *p,char *dummy){ return create_config(p); } #define merge_var(var,flag) (new->set&(flag)?new->var:old->var) static void *merge_config(apr_pool_t *p,void *o,void *n){ rxv_spin_config_t *old=(rxv_spin_config_t *)o,*new=(rxv_spin_config_t *)n, *conf=apr_palloc(p,sizeof(*conf)); conf->application=merge_var(application,RXV_SPIN_APPLICATION_SET); conf->appinit=merge_var(appinit,RXV_SPIN_APPINIT_SET); conf->appfixup=merge_var(appfixup,RXV_SPIN_APPFIXUP_SET); conf->appentry=merge_var(appentry,RXV_SPIN_APPENTRY_SET); conf->cfgfn=merge_var(cfgfn,RXV_SPIN_CFGFN_SET); conf->driver=merge_var(driver,RXV_SPIN_STORE_SET); conf->store=merge_var(store,RXV_SPIN_STORE_SET); conf->appfn=merge_var(appfn,RXV_SPIN_STORE_SET); conf->storetbl=merge_var(storetbl,RXV_SPIN_STORETBL_SET); conf->ckname=merge_var(ckname,RXV_SPIN_CKNAME_SET); conf->ckpath=merge_var(ckpath,RXV_SPIN_CKPATH_SET); conf->ckdomain=merge_var(ckdomain,RXV_SPIN_CKDOMAIN_SET); conf->salt=merge_var(salt,RXV_SPIN_SALT_SET); conf->oslt=merge_var(oslt,RXV_SPIN_OSLT_SET); conf->timeout=merge_var(timeout,RXV_SPIN_TIMEOUT_SET); conf->cache=merge_var(cache,RXV_SPIN_CACHE_SET); conf->connpool=merge_var(connpool,RXV_SPIN_CONNPOOL_SET); conf->set=new->set; return conf; } static const char *set_application(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; if(!(conf->application=ap_server_root_relative(cmd->pool,arg))) return apr_pstrcat(cmd->pool,"Invalid SpinApplication path: ",arg,NULL); conf->set|=RXV_SPIN_APPLICATION_SET; return NULL; } static const char *set_appinit(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; conf->appinit=apr_pstrdup(cmd->pool,arg); conf->set|=RXV_SPIN_APPINIT_SET; return NULL; } static const char *set_appfixup(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; conf->appfixup=apr_pstrdup(cmd->pool,arg); conf->set|=RXV_SPIN_APPFIXUP_SET; return NULL; } static const char *set_appentry(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; conf->appentry=apr_pstrdup(cmd->pool,arg); conf->set|=RXV_SPIN_APPENTRY_SET; return NULL; } static const char *set_appconfig(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; if(!(conf->cfgfn=ap_server_root_relative(cmd->pool,arg))) return apr_pstrcat(cmd->pool,"Invalid SpinAppConfig path: ",arg,NULL); conf->set|=RXV_SPIN_CFGFN_SET; return NULL; } static const char *set_store(cmd_parms *cmd,void *data,const char *arg){ char *p; rxv_spin_config_t *conf=data; /* check if our store is file */ if(!((p=strchr(arg,':')) && *(p+1))) return apr_pstrcat(cmd->pool,"Invalid SpinStore: ",arg,NULL); conf->driver=apr_pstrmemdup(cmd->pool,arg,p-arg); /* database backend */ if(strcmp(conf->driver,"file")){ conf->store=apr_pstrdup(cmd->pool,arg); } else{ /* file backend */ if(!(conf->store=ap_server_root_relative(cmd->pool,p+1))) return apr_pstrcat(cmd->pool,"Invalid SpinStore: ",arg,NULL); else if(apr_filepath_merge(&conf->appfn,conf->store,"__application", APR_FILEPATH_SECUREROOT,cmd->pool)!=APR_SUCCESS) return apr_pstrcat(cmd->pool,"Invalid SpinStore: ",arg,NULL); } conf->set|=RXV_SPIN_STORE_SET; return NULL; } static const char *set_storetbl(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; conf->storetbl=apr_pstrdup(cmd->pool,arg); conf->set|=RXV_SPIN_STORETBL_SET; return NULL; } static const char *set_ckname(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; conf->ckname=apr_pstrdup(cmd->pool,arg); conf->set|=RXV_SPIN_CKNAME_SET; return NULL; } static const char *set_ckpath(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; conf->ckpath=apr_pstrdup(cmd->pool,arg); conf->set|=RXV_SPIN_CKPATH_SET; return NULL; } static const char *set_ckdomain(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; conf->ckdomain=apr_pstrdup(cmd->pool,arg); conf->set|=RXV_SPIN_CKDOMAIN_SET; return NULL; } static const char *set_timeout(cmd_parms *cmd,void *data,const char *arg){ rxv_spin_config_t *conf=data; if((conf->timeout=apr_atoi64(arg)*1000000)<0) conf->timeout=0; conf->set|=RXV_SPIN_TIMEOUT_SET; return NULL; } static const char *set_cache(cmd_parms *cmd, void *data,const char *arg){ rxv_spin_config_t *conf=data; if(!strcasecmp(arg,"on")) conf->cache=RXV_SPIN_TRUE; else if(!strcasecmp(arg,"off")) conf->cache=RXV_SPIN_FALSE; else return apr_pstrcat(cmd->pool,"Invalid SpinCache option",arg,NULL); conf->set|=RXV_SPIN_CACHE_SET; return NULL; } static const char *set_connpool(cmd_parms *cmd, void *data,const char *arg){ rxv_spin_config_t *conf=data; if(!strcasecmp(arg,"on")) conf->connpool=RXV_SPIN_TRUE; else if(!strcasecmp(arg,"off")) conf->connpool=RXV_SPIN_FALSE; else return apr_pstrcat(cmd->pool,"Invalid SpinConnPool option",arg,NULL); conf->set|=RXV_SPIN_CONNPOOL_SET; return NULL; } static const char *set_conncount(cmd_parms *cmd, void *data,const char *arg){ const char *err; if((err=ap_check_cmd_context(cmd,GLOBAL_ONLY))) return err; if((conn_max=atoi(arg))<=0) conn_max=CONN_MAX; return NULL; } static const char *set_salt(cmd_parms *cmd, void *data,const char *arg){ rxv_spin_config_t *conf=data; size_t len; /* nothing to do, we've been here twice already */ if(conf->set&RXV_SPIN_OSLT_SET) return NULL; if((len=strlen(arg))pool,"SpinSalt too short (min 30 char): ",arg,NULL); /* we've already seen salt once, so this must be the old one */ if(conf->set&RXV_SPIN_SALT_SET){ conf->oslt=apr_pcalloc(cmd->pool,RXV_SPIN_SALT_MAX+1); memcpy(conf->oslt,arg,len>RXV_SPIN_SALT_MAX?RXV_SPIN_SALT_MAX:len); conf->set|=RXV_SPIN_OSLT_SET; } else{ conf->salt=apr_pcalloc(cmd->pool,RXV_SPIN_SALT_MAX+1); memcpy(conf->salt,arg,len>RXV_SPIN_SALT_MAX?RXV_SPIN_SALT_MAX:len); conf->set|=RXV_SPIN_SALT_SET; } return NULL; } /* * Session validation functions */ static apr_status_t sesck_valid(apr_pool_t *pool,const char *uniq,char *hash, rxv_spin_config_t *conf){ char *ndig; /* make HMAC from unique ID */ if(!(ndig=rxv_spin_hmac(pool,uniq,conf->salt))) return APR_EGENERAL; if(strcmp(ndig,hash)){ char *odig; /* new salt didn't work, try the old one */ if(!(conf->oslt && (odig=rxv_spin_hmac(pool,uniq,conf->oslt))) || strcmp(odig,hash)) return APR_EGENERAL; /* old salt OK, but we need to replace the hash with the new one */ memcpy(hash,ndig,APR_MD5_DIGESTSIZE*2); } return APR_SUCCESS; } /* * The other hooks... */ static int post_config(apr_pool_t *pconf,apr_pool_t *plog, apr_pool_t *ptemp,server_rec *s){ ap_add_version_component(pconf,RXV_SPIN_PACKAGE_SIGNATURE); return OK; } static void child_init(apr_pool_t *p,server_rec *s){ #if APR_HAS_THREADS int rc; #endif apr_status_t rv; /* initialise private process data */ priv=apr_pcalloc(p,sizeof(*priv)); priv->pool=p; priv->dso=apr_hash_make(p); priv->conns=apr_hash_make(p); priv->tmpls=apr_hash_make(p); priv->count=conn_max; APR_RING_INIT(&priv->tmpl,rxv_spin_tmpl,link); APR_RING_INIT(&priv->conn,rxv_spin_pool,link); #if APR_HAS_THREADS /* see if MPM is thread capable */ if((rv=ap_mpm_query(AP_MPMQ_IS_THREADED,&rc))!=APR_SUCCESS){ ap_log_error(APLOG_MARK,APLOG_CRIT,rv,s, "mod_spin: cannot get MPM threading capability"); return; } if(rc!=AP_MPMQ_NOT_SUPPORTED){ /* create global thread mutex */ if((rv=apr_thread_mutex_create(&priv->mutex, APR_THREAD_MUTEX_DEFAULT,p))!=APR_SUCCESS){ ap_log_error(APLOG_MARK,APLOG_CRIT,rv,s, "mod_spin: cannot create mutex"); return; } } #endif /* initialise apr_dbd */ if((rv=apr_dbd_init(p))!=APR_SUCCESS){ ap_log_error(APLOG_MARK,APLOG_CRIT,rv,s, "mod_spin: cannot initialise APR DBD"); return; } /* regular expression for checking the validity of session cookies */ ureg=ap_pregcomp(p,apr_psprintf(p,"^[a-p]{%d}$",APR_MD5_DIGESTSIZE*4), AP_REG_EXTENDED|AP_REG_NOSUB); /* check version and initialise XML parser */ LIBXML_TEST_VERSION } /* * Configuration creation functions */ static rxv_spin_reqcf_t *req_config(request_rec *r){ rxv_spin_reqcf_t *rcnf; rcnf=apr_pcalloc(r->pool,sizeof(*rcnf)); ap_set_module_config(r->request_config,&spin_module,rcnf); return rcnf; } static int app_config(request_rec *r,rxv_spin_config_t *conf, rxv_spin_reqcf_t *rcnf,rxv_spin_ctx_t *ctx){ apr_status_t rv; apr_dso_handle_t *handle=NULL; apr_dso_handle_sym_t init=NULL; /* load the DSO, either from cache or new */ if((rv=rxv_spin_dso_load(ctx,conf->application,&handle))!=APR_SUCCESS){ if(handle){ char *error=apr_palloc(r->pool,ERROR_BUF_SIZE); apr_dso_error(handle,error,ERROR_BUF_SIZE); ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r,"mod_spin: %s",error); } else{ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: cannot load %s",conf->application); } return HTTP_INTERNAL_SERVER_ERROR; } if((rv=apr_dso_sym(&rcnf->svc,handle,conf->appentry)!=APR_SUCCESS)){ char *error=apr_palloc(r->pool,ERROR_BUF_SIZE); apr_dso_error(handle,error,ERROR_BUF_SIZE); ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r,"mod_spin: %s",error); return HTTP_INTERNAL_SERVER_ERROR; } /* we don't care if fixup doesn't exist, so no error checking */ apr_dso_sym(&rcnf->fix,handle,conf->appfixup); /* initialise the app */ if(apr_dso_sym(&init,handle,conf->appinit)==APR_SUCCESS){ rxv_spin_lock(priv); (*((rxv_spin_init_t)init))(ctx); rxv_spin_unlock(priv); } /* put context into the request configuration */ rcnf->ctx=ctx; return OK; } /* * The fixup hook... */ static int fixup(request_rec *r){ int rc; rxv_spin_config_t *conf; rxv_spin_reqcf_t *rcnf; rxv_spin_ctx_t *ctx; /* if the configuration is missing, big trouble */ if(!(conf=ap_get_module_config(r->per_dir_config,&spin_module))){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r, "mod_spin: missing configuration data"); return HTTP_INTERNAL_SERVER_ERROR; } /* make sure connection pool is cleaned up */ apr_pool_cleanup_register(r->pool,r,private_cleanup,apr_pool_cleanup_null); /* none of our business, don't handle */ if(!(conf->application || conf->store || conf->ckname)) return DECLINED; /* get request configuration, or create it if not there */ if(!(rcnf=ap_get_module_config(r->request_config,&spin_module)) && !(rcnf=req_config(r))) return HTTP_INTERNAL_SERVER_ERROR; /* prepare session identifier if configured */ if(conf->ckname && conf->salt){ apreq_handle_t *req=apreq_handle_apache2(r); /* get/set session identifier and serve it back to the client */ if(req){ char *sesck=NULL,*uniq,*hash; apreq_cookie_t *cookie; /* we have session ID already */ if((cookie=apreq_jar_get(req,conf->ckname))){ /* extract session from he cookie */ if(*cookie->v.data=='"' && *(cookie->v.data+cookie->v.dlen-1)=='"') sesck=apr_pstrmemdup(r->pool,cookie->v.data+1,cookie->v.dlen-2); else sesck=apr_pstrmemdup(r->pool,cookie->v.data,cookie->v.dlen); /* verify session ID */ if(!sesck || ap_regexec(ureg,sesck,0,NULL,0)){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EACCES,r, "mod_spin: invalid session identifier"); return HTTP_INTERNAL_SERVER_ERROR; } /* unique id will be session identifier, hash is the rest */ hash=sesck+APR_MD5_DIGESTSIZE*2; uniq=apr_pstrmemdup(r->pool,sesck,APR_MD5_DIGESTSIZE*2); /* do crypto validation against new and possibly old salt */ if(sesck_valid(r->pool,uniq,hash,conf)!=APR_SUCCESS){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EACCES,r, "mod_spin: invalid session identifier"); return HTTP_INTERNAL_SERVER_ERROR; } /* all clear, we have the session and it is valid */ rcnf->sesid=uniq; rcnf->valid=RXV_SPIN_TRUE; } else if(!r->main){ /* we have to build session ID ourselves */ if((uniq=(char *)apr_table_get(r->subprocess_env,"UNIQUE_ID"))){ if(!((uniq=rxv_spin_hash(r->pool,uniq)) && (hash=rxv_spin_hmac(r->pool,uniq,conf->salt)))){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_ENOMEM,r, "mod_spin: hashing failed"); return HTTP_INTERNAL_SERVER_ERROR; } sesck=apr_pstrcat(r->pool,uniq,hash,NULL); } /* set session, but not valid yet */ rcnf->sesid=uniq; } /* make and bake the cookie */ if(sesck){ char *scookie; if(!(cookie=apreq_cookie_make(r->pool, conf->ckname,strlen(conf->ckname), sesck,APR_MD5_DIGESTSIZE*4))){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_ENOMEM,r, "mod_spin: cannot make session cookie"); return HTTP_INTERNAL_SERVER_ERROR; } cookie->path=conf->ckpath; if(conf->ckdomain) cookie->domain=conf->ckdomain; if(!(scookie=apreq_cookie_as_string(cookie,r->pool))){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_ENOMEM,r, "mod_spin: cannot serialise session cookie"); return HTTP_INTERNAL_SERVER_ERROR; } apr_table_addn(r->err_headers_out,"Set-Cookie",scookie); } } } /* stop processing here for sub-requests */ if(r->main) return DECLINED; /* we have to have minimum configuration to do anything */ if(conf->store || conf->application){ ctx=context_create(r); ctx->priv=priv; ctx->conf=conf; ctx->rcnf=rcnf; ctx->havest=(conf->driver?RXV_SPIN_TRUE:RXV_SPIN_FALSE); ctx->dbstor=(ctx->havest?(strcmp(conf->driver,"file")?RXV_SPIN_TRUE: RXV_SPIN_FALSE): RXV_SPIN_FALSE); /* find and execute prepare function */ if(conf->application){ if((rc=app_config(r,conf,rcnf,ctx))!=OK) return rc; /* call prepare function only if we have one */ if(rcnf->fix){ rc=(*((rxv_spin_prepare_t)rcnf->fix))(ctx); if(ap_is_HTTP_ERROR(rc)){ ctx->havest=RXV_SPIN_FALSE; /* don't save to store on error */ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r, "mod_spin: application error in prepare"); return rc; } return rc; } } } return OK; } /* * The handlers... */ static int handler(request_rec *r){ int rc; char *key; apr_status_t rv; rxv_spin_config_t *conf; rxv_spin_reqcf_t *rcnf; rxv_spin_extra_t *extra; rxv_spin_ctx_t *ctx; apr_file_t *fp; apr_finfo_t *fi; apr_mmap_t *mm; apr_bucket *b; /* don't do what isn't ours */ if(strcmp(r->handler,"spin-template") && strcmp(r->handler,SPIN_MAGIC_TYPE)) return DECLINED; /* if the configuration is missing, big trouble */ if(!(conf=ap_get_module_config(r->per_dir_config,&spin_module))){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r, "mod_spin: missing configuration data"); return HTTP_INTERNAL_SERVER_ERROR; } /* request configuration, or create if not there */ if(!(rcnf=ap_get_module_config(r->request_config,&spin_module)) && !(rcnf=req_config(r))) return HTTP_INTERNAL_SERVER_ERROR; /* build the necessary memory structures */ extra=extra_create(r,rcnf->ctx); /* some important shortcuts before anything else */ ctx=extra->ctx; /* if we didn't populate context during the fixup phase, do it now */ if(!rcnf->ctx){ ctx->priv=priv; ctx->conf=conf; ctx->rcnf=rcnf; ctx->havest=(conf->driver?RXV_SPIN_TRUE:RXV_SPIN_FALSE); ctx->dbstor=(ctx->havest?(strcmp(conf->driver,"file")?RXV_SPIN_TRUE: RXV_SPIN_FALSE): RXV_SPIN_FALSE); } /* grab the apreq handle for cookie and parameter parsing */ ctx->req=apreq_handle_apache2(r); /* run input filters and discard the body, all access via libapreq2 */ if((rc=ap_discard_request_body(r))!=OK){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r, "mod_spin: cannot run input filters"); return rc; } /* find and execute application */ if(conf->application){ if(!rcnf->ctx && (rc=app_config(r,conf,rcnf,ctx))!=OK) return rc; /* call entry function */ rc=(*((rxv_spin_service_t)rcnf->svc))(ctx); if(ap_is_HTTP_ERROR(rc)){ ctx->havest=RXV_SPIN_FALSE; /* don't save to store on error */ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r, "mod_spin: application error in service"); return rc; } /* not a failure, but don't process the template */ if(rc!=OK) return rc; } else{ ap_log_rerror(APLOG_MARK,APLOG_INFO,APR_SUCCESS,r, "mod_spin: application not specified"); } /* check existence of the template */ if(!r->finfo.filetype){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_ENOENT,r, "mod_spin: file does not exist: %s",r->filename); return HTTP_NOT_FOUND; } /* always open file - we want to be atomic at inode level */ if((rv=apr_file_open(&fp,r->filename,APR_FOPEN_READ, APR_FPROT_OS_DEFAULT,r->pool))!=APR_SUCCESS){ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: cannot open file: %s",r->filename); return HTTP_FORBIDDEN; } /* get the size of the file, for the mmap below and mtime for caching */ fi=apr_pcalloc(r->pool,sizeof(*fi)); if((rv=apr_file_info_get(fi,APR_FINFO_SIZE|APR_FINFO_MTIME,fp))!=APR_SUCCESS){ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: cannot stat file: %s",r->filename); return HTTP_FORBIDDEN; } /* mmap the file */ if((rv=apr_mmap_create(&mm,fp, 0,fi->size,APR_MMAP_READ,r->pool))!=APR_SUCCESS){ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: cannot mmap file: %s",r->filename); return HTTP_INTERNAL_SERVER_ERROR; } /* close file to save file descriptors */ apr_file_close(fp); /* mmap bucket that we'll share around */ if(!(b=apr_bucket_mmap_create(mm,0,fi->size,extra->output->bucket_alloc))){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_ENOMEM,r, "mod_spin: cannot create mmap bucket"); return HTTP_INTERNAL_SERVER_ERROR; } /* create brigade for scanning */ extra->input=apr_brigade_create(r->pool,extra->output->bucket_alloc); APR_BRIGADE_INSERT_HEAD(extra->input,b); APR_BUCKET_INSERT_AFTER(b,apr_bucket_eos_create(extra->output->bucket_alloc)); /* get/prepare cache template, if enabled */ if(conf->cache){ rxv_spin_tmpl_t *tmpl; /* do we have this in cache? */ key=apr_psprintf(r->pool,"%" APR_TIME_T_FMT "%" APR_OFF_T_FMT "%s", fi->mtime,fi->size,r->filename); rxv_spin_lock(priv); if((tmpl=apr_hash_get(priv->tmpls,key,APR_HASH_KEY_STRING))){ if(r->request_time>tmpl->atime) tmpl->atime=r->request_time; APR_RING_REMOVE(tmpl,link); APR_RING_INSERT_TAIL(&priv->tmpl,tmpl,rxv_spin_tmpl,link); } else{ apr_bucket_brigade *input; apr_pool_t *tpool,*epool=extra->pool; rxv_spin_root_t **root; apr_pool_create(&tpool,priv->pool); tmpl=apr_pcalloc(tpool,sizeof(*tmpl)); tmpl->pool=tpool; tmpl->key=apr_pstrdup(tpool,key); tmpl->priv=priv; tmpl->atime=r->request_time; input=extra->input; root=&tmpl->root; extra->pool=tpool; /* for the duration of the parse */ while(extra->input){ /* reset root, count of buckets to queue */ extra->root=NULL; extra->count=0; if((rv=rxv_spin_build(extra))!=APR_SUCCESS){ apr_pool_destroy(tpool); if(rv==APR_EACCES){ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: permission denied: %s",r->filename); return HTTP_FORBIDDEN; } else{ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: parsing error: %s: %s", r->filename,extra->error?extra->error:"unspecified"); return HTTP_INTERNAL_SERVER_ERROR; } } /* store parsed root */ if(extra->root){ *root=apr_pcalloc(tpool,sizeof(**root)); (*root)->root=extra->root; root=&(*root)->next; } } extra->pool=epool; /* restore real extra pool */ extra->input=input; APR_RING_INSERT_TAIL(&priv->tmpl,tmpl,rxv_spin_tmpl,link); apr_hash_set(priv->tmpls,tmpl->key,APR_HASH_KEY_STRING,tmpl); apr_pool_cleanup_register(tpool,tmpl,tmpl_cleanup,apr_pool_cleanup_null); } tmpl->count++; rxv_spin_unlock(priv); extra->cache=tmpl; apr_pool_cleanup_register(r->pool,tmpl,cache_cleanup,apr_pool_cleanup_null); } while(extra->input){ /* reset root, count of buckets to queue and clear node pool */ extra->root=NULL; extra->count=0; apr_pool_clear(extra->pool); if((rv=rxv_spin_build(extra))!=APR_SUCCESS){ if(rv==APR_EACCES){ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: permission denied: %s",r->filename); return HTTP_FORBIDDEN; } else{ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: parsing error: %s: %s", r->filename,extra->error?extra->error:"unspecified"); return HTTP_INTERNAL_SERVER_ERROR; } } /* finally, process the template */ if(extra->root){ apr_bucket *b=extra->scan; /* we may have bumped into EOS while scanning, back it up */ if(APR_BUCKET_IS_EOS(extra->scan)) extra->scan=APR_BUCKET_PREV(extra->scan); if((rv=rxv_spin_render(extra->root,extra))!=APR_SUCCESS){ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: template processing error: %s: %s", r->filename,extra->error?extra->error:"unspecified"); return HTTP_INTERNAL_SERVER_ERROR; } extra->scan=b; /* very end, add EOS bucket */ if(extra->eos && !extra->input){ APR_BUCKET_REMOVE(extra->eos); APR_BRIGADE_INSERT_TAIL(extra->output,extra->eos); } if(!APR_BRIGADE_EMPTY(extra->output) && (rv=ap_pass_brigade(r->output_filters,extra->output))!=APR_SUCCESS){ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: cannot pass brigade to filters"); return HTTP_INTERNAL_SERVER_ERROR; } } /* free up some space */ apr_brigade_cleanup(extra->output); } return OK; } /* * The filters... */ static apr_status_t filter(ap_filter_t *f,apr_bucket_brigade *b){ apr_status_t rv; request_rec *r=f->r; rxv_spin_fltcf_t *fcnf=f->ctx; rxv_spin_extra_t *extra; /* fast exit */ if(APR_BRIGADE_EMPTY(b)) return APR_SUCCESS; /* been here before, just pick up existing stuff */ if(fcnf){ extra=fcnf->extra; /* reset positions, EOF flag, error etc. */ extra->offset=0; extra->pos=-1; extra->soff=0; extra->blen=extra->bpos=0; extra->eof=NULL; extra->error=NULL; extra->split=NULL; extra->depth=extra->ldepth=0; /* initialise parsing variables */ memset(extra->loop,0,sizeof(*extra->loop)*RXV_SPIN_MAX_DEPTH); memset(extra->nest,0,sizeof(*extra->nest)*RXV_SPIN_MAX_DEPTH); } else{ /* first time through, create filter config */ int rc; rxv_spin_config_t *conf; rxv_spin_reqcf_t *rcnf; rxv_spin_ctx_t *ctx; /* new filter context */ f->ctx=fcnf=apr_pcalloc(r->pool,sizeof(*fcnf)); /* if the configuration is missing, big trouble */ if(!(conf=ap_get_module_config(r->per_dir_config,&spin_module))){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r, "mod_spin: missing configuration data"); ap_remove_output_filter(f); return ap_pass_brigade(f->next,b); } /* request configuration, or create if not there */ if(!(rcnf=ap_get_module_config(r->request_config,&spin_module)) && !(rcnf=req_config(r))){ ap_remove_output_filter(f); return ap_pass_brigade(f->next,b); } /* build the necessary memory structures */ fcnf->extra=extra=extra_create(r,rcnf->ctx); /* some important shortcuts before anything else */ ctx=extra->ctx; /* if we didn't populate context during the fixup phase, do it now */ if(!rcnf->ctx){ ctx->priv=priv; ctx->conf=conf; ctx->rcnf=rcnf; ctx->havest=(conf->driver?RXV_SPIN_TRUE:RXV_SPIN_FALSE); ctx->dbstor=(ctx->havest?(strcmp(conf->driver,"file")?RXV_SPIN_TRUE: RXV_SPIN_FALSE): RXV_SPIN_FALSE); } /* grab the apreq handle for cookie and parameter parsing */ ctx->req=apreq_handle_apache2(r); /* run input filters and discard the body, all access via libapreq2 */ if((rc=ap_discard_request_body(r))!=OK){ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r, "mod_spin: cannot run input filters"); ap_remove_output_filter(f); return ap_pass_brigade(f->next,b); } /* find and execute application */ if(conf->application){ if(!rcnf->ctx && (rc=app_config(r,conf,rcnf,ctx))!=OK){ ap_remove_output_filter(f); return ap_pass_brigade(f->next,b); } /* call entry function */ rc=(*((rxv_spin_service_t)rcnf->svc))(ctx); /* anything but OK is an error here */ if(rc!=OK){ ctx->havest=RXV_SPIN_FALSE; /* don't save to store on error */ ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r, ap_is_HTTP_ERROR(rc)? "mod_spin: application error in service": "mod_spin: application returned an invalid code"); ap_remove_output_filter(f); return ap_pass_brigade(f->next,b); } } else{ ap_log_rerror(APLOG_MARK,APLOG_INFO,APR_SUCCESS,r, "mod_spin: application not specified"); } /* filter specific knickknacks */ extra->f=f; extra->metabb=apr_brigade_create(r->pool,r->connection->bucket_alloc); } /* concatenate saved and input brigade */ if(extra->input) APR_BRIGADE_CONCAT(extra->input,b); else extra->input=b; /* we are going to modify content, so this must be unset */ apr_table_unset(r->headers_out,"Content-Length"); apr_table_unset(r->headers_out,"Last-Modified"); while(extra->input){ /* reset root, count of buckets to queue and clear node pool */ extra->root=NULL; extra->count=0; apr_pool_clear(extra->pool); if((rv=rxv_spin_build(extra))!=APR_SUCCESS){ if(rv==APR_EACCES) ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: permission denied: %s",r->filename); else ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: parsing error: %s: %s", r->filename,extra->error?extra->error:"unspecified"); ap_remove_output_filter(f); return ap_pass_brigade(f->next,b); } /* finally, process the template */ if(extra->root){ if((rv=rxv_spin_render(extra->root,extra))!=APR_SUCCESS){ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: template processing error: %s: %s", r->filename,extra->error?extra->error:"unspecified"); ap_remove_output_filter(f); return ap_pass_brigade(f->next,b); } /* very end, add EOS bucket */ if(extra->eos && !extra->input){ APR_BUCKET_REMOVE(extra->eos); APR_BRIGADE_INSERT_TAIL(extra->metabb,extra->eos); } /* glue on metadata */ APR_BRIGADE_CONCAT(extra->output,extra->metabb); if(!APR_BRIGADE_EMPTY(extra->output) && (rv=ap_pass_brigade(f->next,extra->output))!=APR_SUCCESS){ ap_log_rerror(APLOG_MARK,APLOG_ERR,rv,r, "mod_spin: cannot pass brigade to filters"); ap_remove_output_filter(f); return ap_pass_brigade(f->next,b); } } /* free up some space */ apr_brigade_cleanup(extra->output); } /* setaside split brigade, if any */ if(extra->split) ap_save_brigade(f,&extra->input,&extra->split,r->pool); /* get rid of the original brigade */ apr_brigade_cleanup(b); return APR_SUCCESS; } /* * Directives... */ static const command_rec spin_commands[]={ AP_INIT_TAKE1("SpinApplication",set_application,NULL,ACCESS_CONF|RSRC_CONF, "path to the shared library of the application"), AP_INIT_TAKE1("SpinAppInit",set_appinit,NULL,ACCESS_CONF|RSRC_CONF, "name of the init function in the shared library"), AP_INIT_TAKE1("SpinAppPrepare",set_appfixup,NULL,ACCESS_CONF|RSRC_CONF, "name of the prepare function in the shared library"), AP_INIT_TAKE1("SpinAppService",set_appentry,NULL,ACCESS_CONF|RSRC_CONF, "name of the service function in the shared library"), AP_INIT_TAKE1("SpinAppConfig",set_appconfig,NULL,ACCESS_CONF|RSRC_CONF, "path to the configuration file"), AP_INIT_TAKE1("SpinStore",set_store,NULL,ACCESS_CONF|RSRC_CONF, "store connect string"), AP_INIT_TAKE1("SpinStoreTable",set_storetbl,NULL,ACCESS_CONF|RSRC_CONF, "store table name"), AP_INIT_TAKE1("SpinCookie",set_ckname,NULL,ACCESS_CONF|RSRC_CONF, "name of the cookie for session tracking"), AP_INIT_TAKE1("SpinCookiePath",set_ckpath,NULL,ACCESS_CONF|RSRC_CONF, "path of the cookie for session tracking"), AP_INIT_TAKE1("SpinCookieDomain",set_ckdomain,NULL,ACCESS_CONF|RSRC_CONF, "domain of the cookie for session tracking"), AP_INIT_TAKE1("SpinTimeout",set_timeout,NULL,ACCESS_CONF|RSRC_CONF, "session timeout in seconds"), AP_INIT_TAKE1("SpinCache",set_cache,NULL,ACCESS_CONF|RSRC_CONF, "should templates be cached"), AP_INIT_TAKE1("SpinConnPool",set_connpool,NULL,ACCESS_CONF|RSRC_CONF, "should connections be pooled"), AP_INIT_TAKE1("SpinConnCount",set_conncount,NULL,RSRC_CONF, "maximum number of connections in the pool"), AP_INIT_TAKE1("SpinSalt",set_salt,NULL,ACCESS_CONF|RSRC_CONF, "cryptographic salt"), {NULL} }; static void register_hooks(apr_pool_t *p){ ap_hook_post_config(post_config,NULL,NULL,APR_HOOK_MIDDLE); ap_hook_child_init(child_init,NULL,NULL,APR_HOOK_MIDDLE); ap_hook_fixups(fixup,NULL,NULL,APR_HOOK_MIDDLE); ap_hook_handler(handler,NULL,NULL,APR_HOOK_MIDDLE); ap_register_output_filter("SPIN-TEMPLATE",filter,NULL,AP_FTYPE_RESOURCE); } module AP_MODULE_DECLARE_DATA spin_module={ STANDARD20_MODULE_STUFF, create_config_directory, /* create per-dir config */ merge_config, /* merge per-dir config */ create_config_server, /* server config */ merge_config, /* merge server config */ spin_commands, /* command apr_table_t */ register_hooks /* register hooks */ };